轉自:http://blog.csdn.net/otangba/article/details/8273952
終於到了服務器端,第三篇的手機客戶端如果已經下載了的話,沒有服務器是不能正常運行的。
服務器端要做得事很多,雖然邏輯不是很復雜,但是我們必須要分析清楚我們要做哪些事,請看下圖:
通過這張圖,我們看出,服務器端的接口一共有6個,分別處理:
- 手機客戶端登錄
- 首頁
- 二維碼圖片流
- long polling維持
- 接收手機客戶端已掃描的通知
- 接收手機客戶端已確認登錄的通知
那么一個一個解決
首先是手機客戶端登錄,在上一篇我們介紹的手機客戶端登錄我們僅僅模擬一下,因此用戶只需要提交一個用戶名,服務器則通過SHA1對用戶名加密,將密文返回作為token。為了將來驗證這個密文是否OK,我們將用戶名和密文保存在redis內供將來驗證使。
需要引用的包:
- var http = require('http'), url = require('url'), fs = require('fs'), querystring = require('querystring'),qrcode = require('qrcode'), UUID = require('uuid-js'), sha1 = require('sha1'), redis = require('redis'), redisClient = redis
- .createClient('10087', '192.168.111.122'), redisKey = 'QRCODE_LOGIN_TOKEN';
redis 的客戶端也一並創建了,並設置了key
web服務的基礎結構如下:
- http.createServer(function(req, res) {
- // parse URL
- var url_parts = url.parse(req.url);
- var path = url_parts.pathname;
- var uuid4 = UUID.create();
- var _sessionID = uuid4.toString();
- if (path == '/') {
- //...
- } else if (path == '/poll') {
- // console.log('polling');
- } else if (path == '/qrcodeimage') {
- // 二維碼的請求,參數為sessionID
- } else if (path == '/moblogin') {
- // 返回用戶名對應的token,簡單采用sha1加密
- } else if (path == '/scanned') {
- console.log('scanned');
- } else if (path == '/confirmed') {
- console.log('confirmed');
- } else {
- res.writeHead(200, {
- 'Content-Type' : 'text/html; charset=UTF-8'
- });
- res.end();
- }
- }).listen(9999, '192.168.111.109');
- console.log('服務器已運行在端口9999.');
通過分析,我們無非就是為這6個分支添加邏輯。
這次案例是一個試驗,因此我們代碼編寫的也比較簡單,如果使用類似express等框架的話,會更加方便一些。
先看看第一個接口,登錄,返回sha1的token
- if (path == '/moblogin') {
- <span style="white-space:pre"> </span>// 返回用戶名對應的token,簡單采用sha1加密
- var userName = urlDecode(url_parts.query);
- var token = sha1(userName);
- // userHash.set(token, userName);
- // 保存token到redis
- redisClient.hset(redisKey, token, userName);
- res.writeHead(200, {
- 'Content-Type' : 'text/html; charset=UTF-8'
- });
- res.end(token);
- <span style="white-space:pre"> </span>}
下面是首頁,如果用戶敲擊的url是一個不帶參數的地址,事實上,用戶初次訪問肯定不帶任何參數,而我們這個頁面的目的是必須要有sessionID,因為首頁內包含的2個子請求是必須具備sessionID參數的。因此我們要做url做一個分析和強制跳轉:
- if (path == '/') {
- var sessionID = url_parts.query;
- if (typeof (sessionID) == "undefined" || sessionID == "") {
- // 訪問首頁沒有參數,自動跳轉
- res.writeHead(200, {
- 'Refresh' : '0; url=/?' + _sessionID,
- 'Content-Type' : 'text/html; charset=UTF-8'
- });
- res.end();
- } else {
- // 處理首頁,刷新一條sessionID和二維碼
- generateIndex(sessionID, req, res);
- }
- }
也就是說當直接訪問/的時候,服務器強制將請求重定向並包含sessionID信息
- function generateIndex(sessionID, req, res) {
- fs.readFile('./index.html', 'UTF-8', function(err, data) {
- data = data.replace(/SESSIONID/g, sessionID);
- res.writeHead(200, {
- 'Content-Type' : 'text/html; charset=UTF-8'
- });
- res.end(data);
- });
- }
當訪問的地址符合/?sessionID的時候,服務器讀取一個html頁面,並將其中的二維碼和long polling需要的參數替換為sessionID
- <html>
- <head>
- <script src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
- <script>
- var poll = function() {
- $.getJSON('/poll?SESSIONID', function(response) {
- var cmd = response.cmd;
- if (cmd == 'scanned') {
- scanned();
- } else if (cmd == 'pclogin') {
- var username=response.username;
- pclogin(username);
- }
- poll();
- });
- }
- var pclogin = function(username) {
- $('#output').text('歡迎您:' + username + ',您已成功登錄');
- }
- var scanned = function() {
- $('#output').text('已成功掃描,等待手機確認登錄');
- }
- poll();
- </script>
- </head>
- <body>
- <p align="center"><img src="/qrcodeimage?SESSIONID">
- </p>
- </body>
- </html>
那么維持long polling的接口
- if (path == '/poll') {
- // console.log('polling');
- var sessionID = url_parts.query;
- var sessionObj = {
- 'sessionID' : sessionID,
- 'res' : res
- };
- clients.push(sessionObj);
- console.log('client added' + sessionObj);
- }
在處理接收客戶端完成掃描和確認登錄的時候,邏輯比較類似,都是先驗證用戶的token是否存在,商用的話可能還要有些更安全的考慮
然后根據sessionID找到維持long polling的客戶端對象,並且返回相關的操作指令
- function handleScanned(res, token, sessionID) {
- // console.log(">>>" + token + "," + sessionID);
- var success = false;
- if (typeof (token) != "undefined") {
- // 驗證是否包含用戶信息已確認是登錄的用戶
- var userName;
- redisClient.hget(redisKey, token, function(err, reply) {
- userName = reply;
- // console.log("username=" + userName);
- if (typeof (userName) != "undefined") {
- // 用戶存在
- for ( var int = 0; int < clients.length; int++) {
- var clientobj = clients[int];
- var savedSession = clientobj.sessionID;
- var client = clientobj.res;
- if (savedSession == sessionID) {
- // 頁面存在
- client.end(JSON.stringify({
- cmd : 'scanned'
- }));
- clients.splice(int, 1);
- success = true;
- break;
- }
- }
- }
- res.writeHead(200, {
- 'Content-Type' : 'text/html; charset=UTF-8'
- });
- if (success) {
- res.end("scanned");
- } else {
- res.end("error");
- }
- });
- }
- }
至此,我們的完整的二維碼掃描登錄的流程就已經走完了。
放在服務器上運行一下,完全OK,如果想作為daemon的話可以使用forever包。
經過這幾篇的介紹,我們不難發現其實這個效果的實現並不是很復雜,關鍵在於你要把整個邏輯理順和想清楚。
同時由於這個案例涉及的技術也較多,技術不全面的話也很難形成完成的解決方案。
思考:
這個案例中還存在哪些問題
- 微信27秒是事出有因的,考慮到http請求有可能在客戶端因為長時間無響應而被終止,因此27秒自動刷新long polling可以有效的防治連接斷掉,而在我們這個案例里,並沒有去實現這個功能。首先我覺得實現起來沒有問題,不難,另外,這些點應該由你們自己去實現,我更加關注的是分析業務。
- 關於頁面session的內涵,應該可以附加一些加密的信息,對於客戶端只是傳遞這些信息,因此不涉及解密操作,而服務器端就可以驗證sessionID的合法性,目前如果你訪問/?的時候自己宿便敲sessionID也是可以的,服務器沒有做任何驗證和限制。
- 關於long polling客戶端的response對象的維持和清理,在本例中我們直接采用了js的數組進行存儲,因此每次都是遍歷。如果商用,必然用采用哈希的方式來存儲,同時可能還必須存儲在數據庫內。
- 本例只是在客戶端確認登錄之后在頁面上顯示確認登錄,並沒有跳轉到某頁面,但是實際應用的時候,可能會攜帶某個服務器生成的鑰匙去redirect到某個url,只有目標地址確認這個鑰匙是登錄確認信息之后才會以某用戶方式登錄,這個還是希望大家能實現,邏輯很簡單,只是本例略掉。
后續的文章,就不再就這個話題討論了,我們會回到XMPP上,但是可能會把XMPP和我們這個案例做些結合。
例如客戶端登錄的不是web系統,而是XMPP
客戶端確認登錄以及提交掃描成功不是通過http,而是通過XMPP
web頁面要實現BOSH連XMPP
這些話題我們會在后面的內容里不斷收入的研究。