聊天是一種基于實(shí)時(shí)的服務(wù),編寫基于TCP的聊天服務(wù)器,支持Telnet連接。
基于TCP的聊天服務(wù)器
$ vim chat01.js
// 加載net模塊,包含Node所需TCP功能。
var net = require('net');
// 創(chuàng)建TCP服務(wù)器
var srv = net.createServer();
// 添加事件監(jiān)聽器,每當(dāng)新客戶端通過網(wǎng)路連接服務(wù)器時(shí),觸發(fā)connection事件。
// 連接事件在調(diào)用回調(diào)函數(shù)時(shí),會(huì)傳送給新客戶端對(duì)應(yīng)的TCP socket對(duì)象的引用,此引用命名為client。
srv.on('connection', function(client){
//向新客戶端發(fā)送消息
client.write("hello world\n");
// 關(guān)閉連接
client.end();
});
// 讓Node監(jiān)聽端口
srv.listen(9001);
# 啟動(dòng)Node TCP服務(wù)器
$ node chat01.js
# 使用telnet連接Node TCP服務(wù)器
$ telnet 127.0.0.1 9001
收到客戶端發(fā)送的消息
Node TCP服務(wù)器需要能收到客戶端發(fā)送的消息
$ vim chat02.js
// 加載net模塊,net模塊包含Node所需的TCP功能
var net = require('net');
// 創(chuàng)建Node TCP服務(wù)器
var srv = net.createServer();
// 為Node TCP服務(wù)器添加事件監(jiān)聽器,每當(dāng)新客戶端通過網(wǎng)絡(luò)連接到服務(wù)器時(shí),會(huì)觸發(fā)connection事件。
// 連接事件在調(diào)用回調(diào)函數(shù)時(shí),會(huì)傳送給新客戶端對(duì)應(yīng)的TCP socket對(duì)應(yīng)的引用(client)。
srv.on('connection', function(client){
client.write("Hi, welcome!\n");
// 在connection回調(diào)函數(shù)的作用域中添加事件監(jiān)聽器,即可訪問到連接事件所對(duì)應(yīng)的client對(duì)象。
// 新監(jiān)聽器關(guān)注的是data事件,每當(dāng)client發(fā)送數(shù)據(jù)給服務(wù)器時(shí),事件即被觸發(fā)。
client.on('data', function(data){
// 在終端打印出客戶端發(fā)送的消息
// JS無法處理二進(jìn)制數(shù)據(jù),Node提供Buffer庫。
// Node不知道Telnet發(fā)送的是什么類型的數(shù)據(jù),只能保存原始的二進(jìn)制格式。
// 打印的字符信息實(shí)際是十六進(jìn)制的字節(jié)數(shù)據(jù),每個(gè)字節(jié)對(duì)應(yīng)著字符串中的一個(gè)字母或字符。
console.log(data);
});
});
srv.listen(9002);
# 啟動(dòng)Node TCP服務(wù)器
$ 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無法處理二進(jìn)制數(shù)據(jù),Node提供Buffer庫。Node不知道Telnet發(fā)送的是什么類型的數(shù)據(jù),只能保存原始的二進(jìn)制格式。 打印的字符信息實(shí)際是十六進(jìn)制的字節(jié)數(shù)據(jù),每個(gè)字節(jié)對(duì)應(yīng)著字符串中的一個(gè)字母或字符。
# Telnet連接服務(wù)器
$ telnet 127.0.0.1 9002
Hi, welcome!
hello, world!
相互發(fā)送消息
Telnet客戶端與Node TCP服務(wù)端相互通信,對(duì)于多個(gè)客戶端通信,可創(chuàng)建列表將希望與之通信的客戶端都添加進(jìn)去。
$ vim chat03.js
var net = require('net');
var srv = net.createServer();
// 客戶端列表
var clients = [];
srv.on('connection', function(client){
client.write('Hi\n');
// 添加新客戶端進(jìn)入列表
clients.push(client);
client.on('data', function(data){
// 將列表中每位客戶端輪詢一遍后將消息轉(zhuǎn)發(fā)。
for(var i=0; i<clients.length; i++){
// 發(fā)送消息時(shí)并未檢查發(fā)送者是誰,只是轉(zhuǎn)發(fā)給所有的客戶端。
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?
區(qū)分發(fā)送者
var net = require('net');
var srv = net.createServer();
var clients = [];
srv.on('connection', function(client){
var host = client.remoteAddress;//客戶端所在的IP地址
var port = client.remotePort;//客戶端接收從服務(wù)器返回?cái)?shù)據(jù)的TCP端口
// 為每個(gè)client對(duì)象新增name屬性,閉包中綁定每個(gè)client對(duì)象和相應(yīng)的請(qǐng)求。
client.name = host+':'+port;
// 當(dāng)不同客戶端從同一個(gè)IP發(fā)起連接時(shí),各自會(huì)有唯一的端口。
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);
}
}
}
致命缺陷
若某終端發(fā)送消息,調(diào)用服務(wù)器broadcast()時(shí),服務(wù)器會(huì)向一個(gè)已經(jīng)斷開的客戶端寫入數(shù)據(jù)。而斷開的中終端所對(duì)應(yīng)socket已經(jīng)無法寫入或讀取。此時(shí)針對(duì)已經(jīng)關(guān)閉的socket進(jìn)行write()操作時(shí),Node程序會(huì)拋出異常。這將導(dǎo)致所有客戶端掉線。
這個(gè)問題應(yīng)從兩個(gè)方面來解決,首先必須保證在一個(gè)客戶端斷開時(shí)要把它從客戶端列表中移除,并釋放相應(yīng)的內(nèi)存。其次,要采用更保險(xiǎn)的方式調(diào)用write()。要確保socket從上次被寫入到現(xiàn)在,沒有發(fā)生任何阻礙調(diào)用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);
});
// 客戶端斷開時(shí)將其從客戶端列表中移除
// 一個(gè)socket斷開連接時(shí)會(huì)觸發(fā)end事件,表示客戶端要關(guān)閉。
client.on('end', function(){
// Array.splice()將客戶端從列表中移除
// Array.indexOf()找到客戶端在列表中的位置
clients.splice(clients.indexOf(client), 1);
console.log(client.name+' quit');
});
// 記錄錯(cuò)誤
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是否可寫以確保不會(huì)因?yàn)槿魏我粋€(gè)不可寫的socket導(dǎo)致異常
if(clients[i].writable){
clients[i].write(data);
}else{
cleanup.push(clients[i]);
// 發(fā)現(xiàn)不可寫的socket后,通過Socket.destroy()將其關(guān)閉并從列表中移除
// 遍歷clients時(shí)并未移除socket,因?yàn)椴幌朐诒闅v過程中出現(xiàn)任何未知的副作用。
clients[i].destroy();
}
}
}
// 在寫入循環(huán)中刪除死節(jié)點(diǎn),消除垃圾索引。
for(var i=0; i<cleanup.length; i++){
clients.splice(clients.indexOf(cleanup[i]), 1);
}
}