系列目錄
- NodeJS與Django協(xié)同應(yīng)用開發(fā)(0) node.js基礎(chǔ)知識
- NodeJS與Django協(xié)同應(yīng)用開發(fā)(1)原型搭建
- NodeJS與Django協(xié)同應(yīng)用開發(fā)(2)業(yè)務(wù)框架
- NodeJS與Django協(xié)同應(yīng)用開發(fā)(3)測試與優(yōu)化
- NodeJS與Django協(xié)同應(yīng)用開發(fā)(4)部署
前文我們介紹了node.js還有socket.io的基礎(chǔ)知識躏升,這篇文章我們來說一下如何將node.js與Django一起使用,并且搭建一個簡單的原型出來唾糯。
原本我們的項目全部都基于Django框架俭正,并且也能夠滿足基本需求了渣蜗,但是后來新增了實時需求,在Django框架下比較難做报嵌,為了少挖點坑,多省點時間熊榛,我們選擇使用node.js锚国。
基本框架
在沒有node.js之前,我們的結(jié)構(gòu)是這樣的:
增加的node.js系統(tǒng)應(yīng)該是與原本的Django系統(tǒng)平行的玄坦,而我們使用node.js的初衷是將它作為實時需求的服務(wù)器血筑,不承擔(dān)或者只承擔(dān)一小部分的業(yè)務(wù)邏輯,且完全不需要和數(shù)據(jù)庫有交互煎楣。所以之后的結(jié)構(gòu)就是這樣的:
數(shù)據(jù)庫依然只有Django負責(zé)連接豺总,這和一般的系統(tǒng)并沒有什么區(qū)別,所以文章里就不涉及具體讀寫數(shù)據(jù)庫的實現(xiàn)了转质。
于是問題的關(guān)鍵就在于django和node.js該如何交互园欣。
Django和node.js幾乎是兩種風(fēng)格的網(wǎng)絡(luò)框架,語言也不同休蟹,所以我們需要一個通信手段沸枯。而系統(tǒng)間通信不外乎就是靠網(wǎng)絡(luò)請求(部署在本機的不同系統(tǒng)不在此列,也不值得討論)赂弓,或是另一個可以用作通信的系統(tǒng)绑榴。通常來說對于node.js和django之間交互的話,一般有3種手段可選:
- HTTP Request
- Redis publish/subscribe
- RPC
三種都是可行的方案盈魁,但是也有各自的應(yīng)用場景翔怎。
原型實現(xiàn)(1) HTTP Request
首先是http request。先來看一下django代碼:
[urls.py]
from django.conf.urls import url
urlpatterns = [
url(r'^get_data/$', 'backend.views.get_data'),
]
[backend.views.py]
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET"])
def get_data(request):
data = {
'data1': 123,
'data2': 'abc',
}
return JsonResponse(data, safe=False)
這里我們定義了一個叫get_data
的api杨耙,方便起見我們使用JSON格式作為返回類型赤套,返回一個整型一個字符串。
然后再來看一下node.js代碼:
[django_request.js]
var http = require('http');
var default_protocol = 'http://'
var default_host = 'localhost';
var default_port = 8000;
exports.get = function get(path, on_data_callback, on_err_callback) {
var url = default_protocol + default_host + ':' + default_port + path;
var req = http.get(url, function onDjangoRequestGet(res) {
res.setEncoding('utf-8');
res.on('data', function onDjangoRequestGetData(data) {
on_data_callback(JSON.parse(data));
});
res.resume();
}).on('error', function onDjangoRequestGetError(e) {
if (on_err_callback)
on_err_callback(e);
else
throw "error get " + url + ", " + e;
});
}
[app.js]
var django_request = require('./django_request');
django_request.get('/get_data/', function(data){
console.log('get_data response: %j',data);
}, function(err) {
console.log('error get_data: '+e);
});
在django_request.js里面我們寫了一個通用的get方法珊膜,可以用來向django發(fā)起http get請求容握。運行app.js以后我們就看到結(jié)果了。
alfred@workstation:~/Documents/node_django/nodeapp$ node app.js
get_data response: {"data1":123,"data2":"abc"}
非常簡單车柠,但是別急剔氏,還有post請求。
普通的post請求和get類似竹祷,非常簡單谈跛,用過http庫的同學(xué)都應(yīng)該會寫,但是這年頭已經(jīng)沒有普通的post了塑陵,大家的安全意識越來越高感憾,沒有哪個網(wǎng)站會不防跨域請求了,所以我們的post還需要解決跨域的問題猿妈。
默認配置下django的中間件是包含CsrfViewMiddleware的吹菱,也就是會在用戶訪問網(wǎng)頁時向cookie中添加csrf_token巍虫。所以我們就寫一個簡單的頁面彭则,順便把socket.io也使用起來鳍刷。
在django的views中添加名為post_data
的api,以及為頁面準(zhǔn)備的view函數(shù)俯抖。
[backend.views.py]
import json
def index(request):
return render_to_response('index.html', RequestContext(request, {}))
def get_post_args(request, *args):
try:
args_info = json.loads(request.body)
except Exception, e:
args_info = {}
return [request.POST.get(item, None) or args_info.get(item, None) for item in args]
@require_http_methods(["POST"])
def post_data(request):
data1, data2 = get_post_args(request, 'data1', 'data2')
response = {
'status': 'success',
'data1': data1,
'data2': data2,
}
return JsonResponse(response, safe=False)
[urls.py]
urlpatterns = [
url(r'^$', 'backend.views.index'),
url(r'^get_data/$', 'backend.views.get_data'),
url(r'^post_data/$', 'backend.views.post_data'),
]
socket.io監(jiān)聽9000端口输瓜。
[app.js]
var http = require('http');
var sio = require('socket.io');
var chatroom = require('./chatroom');
var server = http.createServer();
var io = sio.listen(server, {
log: true,
});
chatroom.init(io);
var port = 9000;
server.listen(9000, function startapp() {
console.log('Nodejs app listening on ' + port);
});
定義通用的post方法。
[django_request.js]
var cookie = require('cookie');
exports.post = function post(user_cookie, path, values, on_data_callback, on_err_callback) {
var cookies = cookie.parse(user_cookie);
var values = querystring.stringify(values);
var options = {
hostname: default_host,
port: default_port,
path: path,
method: 'POST',
headers: {
'Cookie': user_cookie,
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': values.length,
'X-CSRFToken': cookies['csrftoken'],
}
};
var post_req = http.request(options, function onDjangoRequestPost(res) {
res.setEncoding('utf-8');
res.on('data', function onDjangoRequestPostData(data) {
on_data_callback(data);
});
}).on('error', function onDjangoRequestPostError(e) {
console.log(e);
if (on_err_callback)
on_err_callback(e);
else
throw "error get " + url + ", " + e;
});
post_req.write(values);
post_req.end();
}
為get和post事件設(shè)定handler芬萍。
[chatroom.js]
var cookie_reader = require('cookie');
var django_request = require('./django_request');
function initSocketEvent(socket) {
socket.on('get', function() {
console.log('event: get');
django_request.get('/get_data/', function(res){
console.log('get_data response: %j',res);
}, function(err) {
//經(jīng)指正這里應(yīng)該是err而不是e尤揣,保留BUG以此為鑒
console.log('error get_data: '+e);
});
});
socket.on('post', function(data) {
console.log('event: post');
django_request.post(socket.handshake.headers.cookie, '/post_data/', {'data1':123, 'data2':'abc', function(res){
console.log('post_data response: %j', res);
}, function(err){
console.log('error post_data: '+e);
});
});
};
exports.init = function(io) {
io.on('connection', function onSocketConnection(socket) {
console.log('new connection');
initSocketEvent(socket);
});
};
簡單的html頁面。
[index.html]
...
<div>
<button id="btn" style="width:200px;height:150px;">hit me</button>
</div>
<div id="content"></div>
<script type="text/javascript" src="/static/backend/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="/static/backend/js/socket.io.min.js"></script>
<script type="text/javascript">
(function() {
socket = io.connect('http://localhost:9000/');
socket.on('connect', function() {
console.log('connected');
});
$('#btn').click(function() {
socket.emit('get');
socket.emit('post');
});
})();
</script>
實現(xiàn)post的重點在于cookie的設(shè)置柬祠。socket.io在客戶端連接的時候默認就會帶上瀏覽器的cookie北戏,這幫我們省去了不少功夫,也省去了顯示傳遞csrftoken的煩惱漫蛔。但是在node.js中向django發(fā)起post請求時不能只設(shè)定X-CSRFToken嗜愈,也不能只設(shè)定cookie∶Ч辏看一下django的源碼(django.middleware.csrf)就能夠了解到是同時獲取cookie和HTTP_X_CSRFTOKEN的蠕嫁。所以我們必須把cookie傳給post函數(shù),這樣才能成功發(fā)起請求毯盈。
順便一提剃毒,這同時也解決了sessionid的問題,如果是登錄用戶搂赋,django是能夠獲取到user信息的赘阀。
以上是node.js端向django端發(fā)起請求,但是這僅僅只是由node.sj主動而已脑奠,還缺少django向node.js發(fā)起HTTP請求的部分基公。
所以我們在app.js中添加如下代碼
[app.js]
function onGetData(request, response){
if (request.method == 'GET'){
response.writeHead(200, {"Content-Type": "application/json"});
jsonobj = {
'data1': 123,
'data2': 'abc'
}
response.end(JSON.stringify(jsonobj));
} else {
response.writeHead(403);
response.end();
}
}
function onPostData(request, response){
if (request.method == 'POST'){
var body = '';
request.on('data', function (data) {
body += data;
if (body.length > 1e6)
request.connection.destroy();
});
request.on('end', function () {
var post = qs.parse(body);
response.writeHead(200, {'Content-Type': 'application/json'});
jsonobj = {
'data1': 123,
'data2': 'abc',
'post_data': post,
}
response.end(JSON.stringify(jsonobj));
});
} else {
response.writeHead(403);
response.end();
}
}
然后我們寫一小段python代碼來測試一下
[http_test.py]
import urllib
import urllib2
httpClient = None
try:
headers = {"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/plain"}
data = urllib.urlencode({'post_arg1': 'def', 'post_arg2': 456})
get_request = urllib2.Request('http://localhost:9000/node_get_data/', headers=headers)
get_response = urllib2.urlopen(get_request)
get_plainRes = get_response.read().decode('utf-8')
print(get_plainRes)
post_request = urllib2.Request('http://localhost:9000/node_post_data/', data, headers)
post_response = urllib2.urlopen(post_request)
post_plainRes = post_response.read().decode('utf-8')
print(post_plainRes)
except Exception, e:
print e
然后就能看到成功的輸出:
[nodejs]
Nodejs app listening on 9000
url: /node_get_data/, method: GET
url: /node_post_data/, method: POST
[python]
{"data1":123,"data2":"abc"}
{"data1":123,"data2":"abc","post_data":{"post_arg1":"def","post_arg2":"456"}}
到此雙向的HTTP Request就建立起來了。只不過node.js端并沒有csrf認證捺信。而在我們的django端酌媒,csrf認證和api都是已經(jīng)部署了的線上模塊,所以不需要在這方面花精力迄靠。
然而如果最終決定采用雙向HTTP Reqeust的話秒咨,那node.js端的csrf認證必須要做好,因為HTTP API都是向外暴露的掌挚,這是這種方式最大的缺點雨席。并不是所有的系統(tǒng)間調(diào)用都需要向公網(wǎng)露接口,一旦被他人知道了一些非公開的api路徑吠式,那很有可能引發(fā)安全問題陡厘。
并且HTTP是要走外網(wǎng)的抽米,這還帶來了一些額外的開銷。
原型實現(xiàn)(2) Redis Publish/Subscribe
相比HTTP Request糙置,這種方式的代碼量要少的多云茸。(關(guān)于Redis Pub/Sub,請移步相關(guān)文檔)
要實現(xiàn)雙向通信谤饭,無非是兩邊同時建立pub與sub channel标捺。而subscribe需要持續(xù)監(jiān)聽,關(guān)于這一點揉抵,我們先看代碼再說亡容。
首先是node.js端,npm安裝redis庫冤今,庫里已經(jīng)包含了所有我們需要的了闺兢。
[app.js]
var redis = require('redis');
// subscribe
var sub = redis.createClient();
sub.subscribe('test_channel');
sub.on('message', function onSubNewMessage(channel, data) {
console.log(channel, data);
});
// publish
var pub = redis.createClient();
pub.publish('test_channel', 'nodejs data published to test_channel');
node.js是事件驅(qū)動的異步非阻塞框架,pub/sub這種方式的實現(xiàn)和它本身的代碼風(fēng)格非常相近戏罢,所以8行代碼就實現(xiàn)了sub與pub的功能屋谭。
再來看python代碼
[redis_test.py]
import redis
r = redis.StrictRedis(host='localhost', port=6379)
# publish
r.publish('test_channel', 'python data published to test_channel');
# subscribe
sub = r.pubsub()
sub.subscribe('test_channel')
for item in sub.listen():
if item['type'] == 'message':
print(item['data'])
代碼中的channel名是可以自定義的。實際應(yīng)用中可以按照不同的需求管理不同的channel帖汞,這樣就不會造成消息的混亂戴而。
多看幾眼代碼,細心的同學(xué)會發(fā)現(xiàn)翩蘸,python的sub代碼只會執(zhí)行一次所意,也就是說如果需要持續(xù)監(jiān)聽的話,至少要新開一個線程催首。也就是說對于django扶踊,我們還需要額外做線程間通信的工作。這種做法并不是說不可以郎任,只是與django原本的風(fēng)格不太吻合秧耗,并不是非常推薦。
(順便一提舶治,不要將開啟線程的工作放在views函數(shù)中分井,因為views的執(zhí)行是多線程的,線程數(shù)量會隨著訪問壓力增大而增加霉猛,放在views中會導(dǎo)致重復(fù)開心線程尺锚,這個坑我爬過。)
原型實現(xiàn)(3) RPC
在我的另一篇文章(ZeroRPC應(yīng)用)中提到過項目所使用的RPC系統(tǒng)惜浅。這個系統(tǒng)的建立是在node.js應(yīng)用之前的瘫辩,非常慶幸當(dāng)時選用的是zerorpc,正好可以無縫接合node.js。伐厌。
類似于HTTP Request承绸,如果要實現(xiàn)雙向通信那就需要在兩端同時建立server。
python端的代碼可以看我的那篇文章里所寫的內(nèi)容挣轨,這邊我們就來說一下node.js端的調(diào)用和建立server军熏。
[app.js]
var zerorpc = require("zerorpc");
var client = new zerorpc.Client();
client.connect("tcp://127.0.0.1:4242");
client.invoke("test_connection", "arg1", "arg2", function(error, res, more) {
if (!error){
console.log(res, more);
} else {
console.log(error);
}
});
var rpcserver = new zerorpc.Server({
test_connection: function(arg1, arg2, reply) {
reply(null, True, arg1, arg2);
}
});
rpcserver.bind("tcp://0.0.0.0:5353");
和python一樣,在node.js里寫zerorpc也可以返回多個值刃唐,這就是invoke的回調(diào)函數(shù)里的more參數(shù)的作用羞迷。res表示返回的第一個值界轩,而more包含了其他的返回值画饥。
rpc方式的概念和HTTP Request的方式一樣,不過比HTTP Request好在不需要暴露API浊猾,因為完全可以在內(nèi)網(wǎng)下部署抖甘,并把外網(wǎng)端口禁封。但是他們又有一個共同的缺點葫慎,那就是對于node.js來說衔彻,我們需要一個額外的消息分發(fā)機制。為什么呢偷办?因為我們接受消息的入口是統(tǒng)一的艰额。
考慮這個情況:
在node.js里我們有2個子系統(tǒng),子系統(tǒng)A和子系統(tǒng)B椒涯,他們分別為功能I和功能II服務(wù)柄沮,各自也都有需要和django交互的地方。如果此時功能I和功能II分別有一條消息到來废岂,那我們就必須要區(qū)分消息的送達對象祖搓。這里就又是額外的工作量了。
這個情況在使用redis時就不會出現(xiàn)湖苞。redis下我們可以只subscribe自己關(guān)心的channel拯欧,也就是說只會收到與自身系統(tǒng)相關(guān)的消息。
總結(jié)
對于三種方式的優(yōu)缺點财骨,我們總結(jié)如下:
實現(xiàn)方式 | 優(yōu)點 | 缺點 |
---|---|---|
HTTP Request | 方便和現(xiàn)有系統(tǒng)集成 | 暴露外網(wǎng)API镐作,流量走外網(wǎng),需要額外安全工作 |
Redis | 切合node.js風(fēng)格隆箩,容易按channel名管理 | django端subscribe需要額外工作量 |
RPC | 流量走內(nèi)網(wǎng)该贾,不暴露API | node.js端分發(fā)消息需要額外工作量 |
工作中我們可以按照實際需求來組合使用,我的項目里原本是使用HTTP Request實現(xiàn)的原型摘仅,后來也是因為其暴露API的缺點以及node.js端需要csrf認證才放棄用django向node.js發(fā)起HTTP請求靶庙。
目前我們項目中django向node.js發(fā)消息使用的是redis,node.js向django請求數(shù)據(jù)或發(fā)送消息使用的是rpc。這么做沒有什么額外的工作量六荒,可以讓我專注于業(yè)務(wù)邏輯护姆。
業(yè)務(wù)邏輯涉及到node.js端的架構(gòu)設(shè)計,關(guān)于這部分的內(nèi)容我們就下篇文章再說掏击。