Node.js聊天服務(wù)器

聊天是一種基于實(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);
    }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、TCP狀態(tài)linux查看tcp的狀態(tài)命令:1)、netstat -nat 查看TCP各個(gè)狀態(tài)的數(shù)量2)、lso...
    北辰青閱讀 9,737評(píng)論 0 11
  • 參考:http://www.2cto.com/net/201611/569006.html TCP HTTP UD...
    F麥子閱讀 3,070評(píng)論 0 14
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,688評(píng)論 19 139
  • 一: 網(wǎng)絡(luò)各個(gè)協(xié)議:TCP/IP、SOCKET、HTTP 網(wǎng)絡(luò)七層由下往上分別為物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層...
    iYeso閱讀 1,512評(píng)論 0 13
  • 18.1 引言 TCP是一個(gè)面向連接的協(xié)議。無論哪一方向另一方發(fā)送數(shù)據(jù)之前,都必須先在雙方之間建立一條連接。本章將...
    張芳濤閱讀 3,536評(píng)論 0 13

友情鏈接更多精彩內(nèi)容