聊天是一种基于实时的服务,编写基于TCP的聊天服务器,支持Telnet连接。
基于TCP的聊天服务器
$ vim chat01.js
// 加载net模块,包含Node所需TCP功能。
var net = require('net');
// 创建TCP服务器
var srv = net.createServer();
// 添加事件监听器,每当新客户端通过网路连接服务器时,触发connection事件。
// 连接事件在调用回调函数时,会传送给新客户端对应的TCP socket对象的引用,此引用命名为client。
srv.on('connection', function(client){
//向新客户端发送消息
client.write("hello world\n");
// 关闭连接
client.end();
});
// 让Node监听端口
srv.listen(9001);
# 启动Node TCP服务器
$ node chat01.js
# 使用telnet连接Node TCP服务器
$ telnet 127.0.0.1 9001
收到客户端发送的消息
Node TCP服务器需要能收到客户端发送的消息
$ vim chat02.js
// 加载net模块,net模块包含Node所需的TCP功能
var net = require('net');
// 创建Node TCP服务器
var srv = net.createServer();
// 为Node TCP服务器添加事件监听器,每当新客户端通过网络连接到服务器时,会触发connection事件。
// 连接事件在调用回调函数时,会传送给新客户端对应的TCP socket对应的引用(client)。
srv.on('connection', function(client){
client.write("Hi, welcome!\n");
// 在connection回调函数的作用域中添加事件监听器,即可访问到连接事件所对应的client对象。
// 新监听器关注的是data事件,每当client发送数据给服务器时,事件即被触发。
client.on('data', function(data){
// 在终端打印出客户端发送的消息
// JS无法处理二进制数据,Node提供Buffer库。
// Node不知道Telnet发送的是什么类型的数据,只能保存原始的二进制格式。
// 打印的字符信息实际是十六进制的字节数据,每个字节对应着字符串中的一个字母或字符。
console.log(data);
});
});
srv.listen(9002);
# 启动Node TCP服务器
$ node chat02.js
<Buffer 68>
<Buffer 65 6c 6c 6f>
<Buffer 20>
<Buffer 2c>
<Buffer 08>
<Buffer 08>
<Buffer 2c>
<Buffer 20>
<Buffer 77>
<Buffer 6f>
<Buffer 72>
<Buffer 6c>
<Buffer 64>
<Buffer 21>
<Buffer 0d 0a>
JS无法处理二进制数据,Node提供Buffer库。Node不知道Telnet发送的是什么类型的数据,只能保存原始的二进制格式。 打印的字符信息实际是十六进制的字节数据,每个字节对应着字符串中的一个字母或字符。
# Telnet连接服务器
$ telnet 127.0.0.1 9002
Hi, welcome!
hello, world!
相互发送消息
Telnet客户端与Node TCP服务端相互通信,对于多个客户端通信,可创建列表将希望与之通信的客户端都添加进去。
$ vim chat03.js
var net = require('net');
var srv = net.createServer();
// 客户端列表
var clients = [];
srv.on('connection', function(client){
client.write('Hi\n');
// 添加新客户端进入列表
clients.push(client);
client.on('data', function(data){
// 将列表中每位客户端轮询一遍后将消息转发。
for(var i=0; i<clients.length; i++){
// 发送消息时并未检查发送者是谁,只是转发给所有的客户端。
clients[i].write(data);
}
});
});
srv.listen(9003);
$ node chat03.js
$ telnet 127.0.0.1 9003
Hi
what is your name?
$ telnet 127.0.0.1 9003
what is your name?
区分发送者
var net = require('net');
var srv = net.createServer();
var clients = [];
srv.on('connection', function(client){
var host = client.remoteAddress;//客户端所在的IP地址
var port = client.remotePort;//客户端接收从服务器返回数据的TCP端口
// 为每个client对象新增name属性,闭包中绑定每个client对象和相应的请求。
client.name = host+':'+port;
// 当不同客户端从同一个IP发起连接时,各自会有唯一的端口。
clients.push(client);
client.write('Hi '+client.name+'\n');
client.on('data', function(data){
broadcast(data,client);
});
});
srv.listen(9004);
function broadcast(data,client){
for(var i=0; i<clients.length; i++){
// 从接收消息的客户端列表中排除掉自身
if(client !== clients[i]){
clients[i].write(client.name+' : '+data);
}
}
}
致命缺陷
若某终端发送消息,调用服务器broadcast()时,服务器会向一个已经断开的客户端写入数据。而断开的中终端所对应socket已经无法写入或读取。此时针对已经关闭的socket进行write()操作时,Node程序会抛出异常。这将导致所有客户端掉线。
这个问题应从两个方面来解决,首先必须保证在一个客户端断开时要把它从客户端列表中移除,并释放相应的内存。其次,要采用更保险的方式调用write()。要确保socket从上次被写入到现在,没有发生任何阻碍调用write()的事情。
var net = require('net');
var srv = net.createServer();
var clients = [];
srv.on('connection', function(client){
var host = client.remoteAddress;
var port = client.remotePort;
client.name = host+':'+port;
clients.push(client);
console.log(client.name + ' connected');
client.on('data', function(data){
broadcast(data,client);
});
// 客户端断开时将其从客户端列表中移除
// 一个socket断开连接时会触发end事件,表示客户端要关闭。
client.on('end', function(){
// Array.splice()将客户端从列表中移除
// Array.indexOf()找到客户端在列表中的位置
clients.splice(clients.indexOf(client), 1);
console.log(client.name+' quit');
});
// 记录错误
client.on('error', function(error){
console.log(error);
});
});
srv.listen(9005);
function broadcast(data,client){
var cleanup = [];
for(var i=0; i<clients.length; i++){
if(client !== clients[i]){
// 检查socket是否可写以确保不会因为任何一个不可写的socket导致异常
if(clients[i].writable){
clients[i].write(data);
}else{
cleanup.push(clients[i]);
// 发现不可写的socket后,通过Socket.destroy()将其关闭并从列表中移除
// 遍历clients时并未移除socket,因为不想在遍历过程中出现任何未知的副作用。
clients[i].destroy();
}
}
}
// 在写入循环中删除死节点,消除垃圾索引。
for(var i=0; i<cleanup.length; i++){
clients.splice(clients.indexOf(cleanup[i]), 1);
}
}