Node.js聊天服务器

聊天是一种基于实时的服务,编写基于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);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值