转自: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
这些话题我们会在后面的内容里不断收入的研究。