一般来讲,HTTP 是基于文本的“单向”通讯机制。这里所谓的“单向”,乃相对于“双向”而言,因为 HTTP 服务器只需根据请求返还恰当的 HTML 给客户端即可,不涉及客户端向服务端的通讯。这种单向的机制比较简单,对网络质量要求也不高。而更多的场景则是需要可靠、稳定的端到端连接。一般这种服务是实时的、有态的而且是长连接,长连接则暗示两段须达致相向通讯的能力,也就说是服务端客户端两者间能够实时地相互间通信。毫无疑问,能够实时通信的服务器正是我们对服务器基本要求之一。区别于 HTTP 服务器以 HTTP 为通讯协议, 实时服务器一般采用较为底层的 TCP/IP 为协议通讯,实现了“套字节 Socket”的双向机制。
Socket 是根据博克莱 (U.C.Berkley) 大学早期发展的 Socket 概念写成的,其设计理念是是将网络传输类比成文件的读取与写入 (传送的动作被视为是写入/接收的动作被视为是读取),如此、传送与接收就简化为编程人员比较容易懂的 读取与写入,降低了网络编程的学习困难度。
聊天室服务器
聊天室的实时连接基于底层的 TCP 直接连接,为此我们须调用 Node 的 TCP 模块。如果不太熟悉所谓 TCP 网络编程?太底层了是不是?没关系,我也不熟悉,边学边做嘛,只不过千万不必因为遇到陌生的词汇而害怕,其实这样原理并不深奥,而且下面的例子也十分的简单易懂!咱们就从最简单的开始吧,下面代码仅仅十行,它的作用是服务器向客户端输出一段文本,完成 Sever --> Client 的单向通讯。
// Sever --> Client 的单向通讯 var net = require('net'); var chatServer = net.createServer(); chatServer.on('connection', function(client) { client.write('Hi!\n'); // 服务端向客户端输出信息,使用 write() 方法 client.write('Bye!\n'); client.end(); // 服务端结束该次会话 }); chatServer.listen(9000);
客户端可以是系统自带的 Telnet:
telnet 127.0.0.1 9000
执行 telnet 后,与服务点连接,反馈 Hi! Bye! 的字符,并立刻结束服务端程序终止连接。如果我们要服务端接到到客户端的信息?可以监听 server.data 事件并且不要中止连接(否则会立刻结束无法接受来自客户端的消息):
// 在前者的基础上,实现 Client --> Sever 的通讯,如此一来便是双向通讯 var net = require('net'); var chatServer = net.createServer(), clientList = []; chatServer.on('connection', function(client) { // JS 可以为对象自由添加属性。这里我们添加一个 name 的自定义属性,用于表示哪个客户端(客户端的地址+端口为依据) client.name = client.remoteAddress + ':' + client.remotePort; client.write('Hi ' + client.name + '!\n'); clientList.push(client); client.on('data', function(data) { broadcast(data, client);// 接受来自客户端的信息 }); }); function broadcast(message, client) { for(var i=0;i<clientList.length;i+=1) { if(client !== clientList[i]) { clientList[i].write(client.name + " says " + message); } } } chatServer.listen(9000);
这里要说明一下的是,不不同操作系统对端口范围的限制不一样,有可能是随机的。
那么上面是不是一个完整功能的代码呢?我们说还有一个问题没有考虑进去:那就是一旦某个客户端退出,却仍保留在 clientList 里面,这明显是一个空指针(NullPoint)。如果是在这样的话我们写程序太脆弱了,能不能更健壮一些?——请接着看。
首先我们简单地把 client 从数组 clientList 中移除掉。完成这工作一点都不困难。Node TCP API 已经为我们提供了 end 事件,即客户端中止与服务端连接的时候发生。移除 client 对象的代码如下:
chatServer.on('connection', function(client) { client.name = client.remoteAddress + ':' + client.remotePort client.write('Hi ' + client.name + '!\n'); clientList.push(client) client.on('data', function(data) { broadcast(data, client) }) client.on('end', function() { clientList.splice(clientList.indexOf(client), 1); // 删除数组中的制定元素。这是 JS 基本功哦~ }) })
但是我们还不敢说上述代码很健壮,因为一旦 end 没有被触发,异常仍然存在着。下面我们看看解决之道:重写 broadcast():
function broadcast(message, client) { var cleanup = [] for(var i=0;i<clientList.length;i+=1) { if(client !== clientList[i]) { if(clientList[i].writable) { // 先检查 sockets 是否可写 clientList[i].write(client.name + " says " + message) } else { cleanup.push(clientList[i]) // 如果不可写,收集起来销毁。销毁之前要 Socket.destroy() 用 API 的方法销毁。 clientList[i].destroy() } } } //Remove dead Nodes out of write loop to avoid trashing loop index for(i=0;i<cleanup.length;i+=1) { clientList.splice(clientList.indexOf(cleanup[i]), 1) } }
TCP API 中还提供一个 error 事件,用于捕捉客户端的异常:
client.on('error', function(e) { console.log(e); });
Node 网络编程的 API 还丰富,此次仅仅是个入门,更多的内容请接着看,关于浏览器 Socket 应用。
Socket.IO