NodeJS與Django協(xié)同應(yīng)用開發(fā)(1) —— 原型搭建


系列目錄


前文我們介紹了node.js還有socket.io的基礎(chǔ)知識躏升,這篇文章我們來說一下如何將node.js與Django一起使用,并且搭建一個簡單的原型出來唾糯。

原本我們的項目全部都基于Django框架俭正,并且也能夠滿足基本需求了渣蜗,但是后來新增了實時需求,在Django框架下比較難做报嵌,為了少挖點坑,多省點時間熊榛,我們選擇使用node.js锚国。

基本框架

在沒有node.js之前,我們的結(jié)構(gòu)是這樣的:

初始結(jié)構(gòu).png

增加的node.js系統(tǒng)應(yīng)該是與原本的Django系統(tǒng)平行的玄坦,而我們使用node.js的初衷是將它作為實時需求的服務(wù)器血筑,不承擔(dān)或者只承擔(dān)一小部分的業(yè)務(wù)邏輯,且完全不需要和數(shù)據(jù)庫有交互煎楣。所以之后的結(jié)構(gòu)就是這樣的:

nodejs+django結(jié)構(gòu).png

數(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種手段可選:

  1. HTTP Request
  2. Redis publish/subscribe
  3. 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)容我們就下篇文章再說掏击。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卵皂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子砚亭,更是在濱河造成了極大的恐慌灯变,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捅膘,死亡現(xiàn)場離奇詭異添祸,居然都是意外死亡,警方通過查閱死者的電腦和手機寻仗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門刃泌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人署尤,你說我怎么就攤上這事耙替。” “怎么了曹体?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵俗扇,是天一觀的道長。 經(jīng)常有香客問我箕别,道長铜幽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任究孕,我火速辦了婚禮啥酱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘厨诸。我一直安慰自己镶殷,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布微酬。 她就那樣靜靜地躺著绘趋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪颗管。 梳的紋絲不亂的頭發(fā)上陷遮,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機與錄音垦江,去河邊找鬼帽馋。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绽族。 我是一名探鬼主播姨涡,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吧慢!你這毒婦竟也來了涛漂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤检诗,失蹤者是張志新(化名)和其女友劉穎匈仗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逢慌,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡悠轩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了涕癣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哗蜈。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坠韩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炼列,我是刑警寧澤只搁,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站俭尖,受9級特大地震影響氢惋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜稽犁,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一焰望、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧已亥,春花似錦熊赖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捆姜,卻和暖如春传趾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泥技。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工浆兰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓簸呈,卻偏偏與公主長得像宽涌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蝶棋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內(nèi)容