Node-基于TCP的简易网络聊天室

写程序之前先回顾一下TCP~

有关TCP

传输控制协议(TCP)是一个面向连接的协议,它保证了两台计算机之间数据传输的可靠性和顺序。

如今。网络应用都是用TCP/IP协议进行通信的。
Node HTTP服务器是构建在Node TCP服务器之上的。从编程角度来说,也就是Node中的http.Server继承自net.Server(net是TCP模块)

除了Web浏览器和服务器(HTTP)之外,很多我们日常使用的如邮件客户端(SMTP/IMAP/POP)、聊天程序(IRC/XMPP)以及远程shell(SSH)等都基于HTTP协议。

TCP协议的特点

TCP的首要特性就是它面向连接的。

面向连接的通信和保证顺序的传递

可以将客户端和服务端的通信看作是一个连接或者数据流。这对开发面向服务的应用和流应用是很好的抽象,因为TCP协议做基于的IP协议是面向无连接的。

IP是基于数据报的传输。这些数据报是独立进行传输的,传达的顺序也是无序的。为了保证数据包送达时是有序的,在TCP连接内进行数据传递时,发送的IP数据包包含了标示该连接以及数据流顺序的信息

假设一条信息分为四个部分,当服务器从连接A收到第一部分和第四部分后,它就直到还要等待其他数据段中的第二部分和第三部分

面向字节

TCP允许数据以ASCII字符(每个字符一个字节)或者Unicode(即每个字符四个字节)进行传输。

可靠性

TCP需要基于确认和超时实现一些列的机制达到可靠性。
当数据发送出去后,发送方就会等待一个确认消息(标示数据包已经收到的简短的确认消息)。如果过了指定的窗口事件,还未收到确认消息,发送方就对数据进行重发。

流控制

TCP通过一种叫流控制的方式来保证两台互相通信的计算机之间传输数据的平衡,避免有一台速度远快于另一台。

拥塞控制

TCP有一种内置的机制能够控制数据包的延迟率及丢包率不会太高,以此来保证服务的质量(QoS)。

基于TCP的简易网络聊天室通过Telnet通信

Telnet

Telnet是一种早期的网络协议,旨在提供一种双向的虚拟终端。
telnet到服务器:

// test.js
var http = require('http');
http.createServer(function(req,res){
    res.writeHead(200,{'Content-Type':'text/html'});
    res.end('<h1>hello</h1>');
}).listen(3000);

此时运行代码,可以在浏览器获取相应数据。
但是,当使用telnet时建立连接,会接收不到数据,这是由于:
要往TCP中写入数据,必须首先创建一个HTTP请求,这就是套接字(socket)(在终端写入GET / HTTP/1.1再两次回车即可成功请求到数据。)
在这里插入图片描述

基于TCP的聊天程序
定义需求

创建一个基本的TCP服务器,任何人都可以连接到该服务器,无须实现任何协议或者指令:

  • 成功连接到服务器后,服务器会显示欢迎信息,并要求输入用户名。同时还会告诉你当前还有多少其他客户端也连接到了该服务器上。
  • 输入用户名,按下回车键后,就认为是成功连接上了。
  • 连接后,就可以通过输入信息再次按下回车键,来向其他客户端进行消息的收发。

这里有一个重要的问题就是:
为什么要按下回车键?
事实上,Telnet中输入的任何信息都会立即发送到服务器。按下回车键是为了输入\n字符。在Node端,通过\n判断消息是否已完全到达。所以,回车符作为一个分隔符来使用

细分步骤
  • 创建模块
  • 理解NET.SERVER API
  • 接收连接
  • data事件
  • 状态以及记录连接情况
  • 退出程序的消息显示
创建模块
// package.json
{
    "name": "tcp-chat-room",
    "version": "0.0.1",
    "description": "Our first tcp server"
}
理解NET.SERVER API
// index.js
var net = require('net');
var server = net.createServer(function(cnn){
    console.log('new connection!');
});
server.listen(3000,function(){
    console.log('server listening on 3000');
});

上述代码中为createServer指定了一个回调函数。该回调函数在每次有新连接的时候都会重新执行。

HTTP是建立在TCP之上的。

createServer回调函数会接收一个对象,该对象是Node中一个很常见的实例:流(Stream),本例中,它传递的是net.Stream,该对象是既可读又可写的。

接收连接

添加计数器,追踪连接的数目。

/*
* 计数器
*/
var count = 0;
/*
*   添加模块
*/
var net = require('net');
var server = net.createServer(function(conn){
    conn.write(
        '\n> welcome to \033[92mnode-chat\033[39m'
        + '\n>' + count + 'other people are commected at this time' 
        + '\n> please write your name and press enter:' 
    );
    count++;
});
/*
* 监听
*/
server.listen(3000,function(){
    console.log('server listening on 3000');
});

打开两个终端,输入命令telnet 127.0.0.1 3000连接服务器,结果每次有其他客户端连进去后,计数器就增加了1。
在这里插入图片描述
当客户端请求关闭连接的时候,计数器需要进行递减操作。
当底层套接字关闭时,Nodejs会触发close事件。Nodejs中有两个和连接终止连接的事件:end和close。前者是当客户端显示关闭TCP连接时触发。比如当你关闭telnet时,它会发一个名为“FIN”的包给服务器,意味着结束连接。
当连接发生错误时(触发error事件),end事件不会触发,因为服务器端并未收到“FIN”包信息。不过这两种情况下,close事件都会触发。

data事件

由于net.Stream同时也是一个EventEmitter,可以监听data事件处理客户端输入的信息。

    conn.on('data',function(data) {
        console.log(data);
    });

在这里插入图片描述在这里插入图片描述
可以看到服务器的确是面向字节连接的。
设置utf8编码

conn.setEncoding('utf8');

此时服务器能正确显示客户端输入的信息。

状态以及记录连接情况

此前定义的计数器称为状态。

两个不同连接的用户需要修改同一个状态变量,这在Node中称为共享状态的并发。

conn.on('data',function(data) {
    data = data.replace('\r\n',''); // 删除回车符
    if (!nickname) {            // 未通过验证
        if (users[data]) {
            conn.write('\033[93m> nickname already in use. try again:\033[39m');
            return;
        } else {
            nickname = data;
            users[nickname] = conn;
            for (var i in users) {
                users[i].write('\033[90m>' + nickname + ' joined the room\033[39m\n');
            }
        }
    } else {            //  已经通过验证,则示为聊天信息
        for (var i in users) {
            if (i != nickname) {    // 确保消息只发送给了除了自己以外的其他客户端
                users[i].write('\033[96m>' + nickname + ':\033[39m' + data + '\n');
            }
        }
    }
});

在这里插入图片描述
本例关键在于存储net.Stream对象。
从这里也可以看到,Node单线程的表现了!每次都是使用同一个程序,users数组是共享的。只是连接之后对应了不同的net.Stream对象。

退出程序的消息显示

当有人断开连接时,需要清除users数组中对应的元素

delete users[nickname];

用户断开时候通知其他用户:

// 封装一个广播
function broadcast (msg, exceptMyself) {
    for (var i in users) {
        if (i != exceptMyself || i != nickname) {
            users[i].write(msg);
        }
    }
}

在这里插入图片描述
完整代码:

// index.js
/*
* count:计数器
* users:记录设置了昵称的用户 
*/
var count = 0;
var users = [];
/*
*   添加模块
*/
var net = require('net');
var server = net.createServer(function(conn){
    conn.write(
        '\n> welcome to \033[92mnode-chat\033[39m'
        + '\n>' + count + 'other people are commected at this time' 
        + '\n> please write your name and press enter:' 
    );
    count++;
    conn.setEncoding('utf8');
    var nickname;    // 表示当前连接的昵称,注意这句不能放在监听data的事件里面,因为

    conn.on('data',function(data) {
        data = data.replace('\r\n',''); // 删除回车符
        if (!nickname) {            // 未通过验证
            if (users[data]) {
                conn.write('\033[93m> nickname already in use. try again:\033[39m');
                return;
            } else {
                nickname = data;
                users[nickname] = conn;
                for (var i in users) {
                    users[i].write('\033[90m>' + nickname + ' joined the room\033[39m\n');
                }
            }
        } else {            //  已经通过验证,则示为聊天信息
            for (var i in users) {
                if (i != nickname) {    // 确保消息只发送给了除了自己以外的其他客户端
                    users[i].write('\033[96m>' + nickname + ':\033[39m' + data + '\n');
                }
            }
        }
    });
    conn.on('close',function(){
        count--;
        broadcast('\033[90m >' + nickname + 'left the room\033[39m\n');
        delete users[nickname];
    });

    function broadcast (msg, exceptMyself) {
        for (var i in users) {
            if (i != exceptMyself || i != nickname) {
                users[i].write(msg);
            }
        }
    }
});

/*
* 监听
*/
server.listen(3000,function(){
    console.log('server listening on 3000');
});
IRC

IRC是因特网中继聊天(Internet Relay Chat)的缩写,它也是一项常用的基于TCP的协议。

IRC是一项非常直观、简单的协议。通过一些简单的命令就可以和所有的应用以及服务器进行通信

构建一个实现TCP协议的客户端意味着,需要实现通过一组命令来实现于IRC服务器进行“通信”,进行数据的交换。

参考
《了不起的Nodejs》

一、实验目的 1.掌握通信规范的制定及实现。 2.练习较复杂的网络编程,能够把协议设计思想应用到现实应用中。 二、实验内容和要求 1.进一步熟悉VC++6编程环境; 2.利用VC++6进行较复杂的网络编程,完成网络聊天室的设计及编写; 三、实验(设计)仪器设备和材料 1.计算机及操作系统:PC机,Windows; 2.网络环境:可以访问互联网; 四、 TCP/IP程序设计基础 基于TCP/IP的通信基本上都是利用SOCKET套接字进行数据通讯,程序一般分为服务器端和用户端两部分。设计思路(VC6.0下): 第一部分 服务器端 一、创建服务器套接字(create)。 二、服务器套接字进行信息绑定(bind),并开始监听连接(listen)。 三、接受来自用户端的连接请求(accept)。 四、开始数据传输(send/receive)。 五、关闭套接字(closesocket)。 第二部分 客户端 一、创建客户套接字(create)。 二、与远程服务器进行连接(connect),如被接受则创建接收进程。 三、开始数据传输(send/receive)。 四、关闭套接字(closesocket)。 CSocket的编程步骤:(注意我们一定要在创建MFC程序第二步的时候选上Windows Socket选项,其中ServerSocket是服务器端用到的,ClientSocket是客户端用的。) (1)构造CSocket对象,如下例: CSocket ServerSocket; CSocket ClientSocket; (2)CSocket对象的Create函数用来创建Windows Socket,Create()函数会自行调用Bind()函数将此Socket绑定到指定的地址上面。如下例: ServerSocket.Create(823); //服务器端需要指定一个端口号,我们用823。 ClientSocket.Create(); //客户端不用指定端口号。 (3)现在已经创建完基本的Socket对象了,现在我们来启动它,对于服务器端,我们需要这个Socket不停的监听是否有来自于网络上的连接请求,如下例: ServerSocket.Listen(5);//参数5是表示我们的待处理Socket队列中最多能有几个Socket。 (4)对于客户端我们就要实行连接了,具体实现如下例: ClientSocket.Connect(CString SerAddress,Unsinged int SerPort);//其中SerAddress是服务器的IP地址,SerPort是端口号。 (5)服务器是怎么来接受这份连接的呢?它会进一步调用Accept(ReceiveSocket)来接收它,而此时服务器端还须建立一个新的CSocket对象,用它来和客户端进行交流。如下例: CSocket ReceiveSocket; ServerSocket.Accept(ReceiveSocket); (6)如果想在两个程序之间接收或发送信息,MFC也提供了相应的函数。如下例: ServerSocket.Receive(String,Buffer); //String是你要发送的字符串,Buffer是发送字符串的缓冲区大小。ServerSocket.Send(String,Butter);//String是你要接收的字符串,Buffer是接收字符串的缓冲区大小。
里面包含聊天室的客户端和服务器端的源文件和一份完整的设计报告。 一、 系统概要 本系统能实现基于VC++的网络聊天室系统。有单独的客户端、服务器端。 服务器应用程序能够接受来自客户端的广播,然后向客户端发送本机的IP与服务端口,让客户端接入到服务器进行聊天,检测用户名是否合法(重复),服务器责接收来自客户端的聊天信息,并根据用户的需求发送给指定的人或所有人,能够给出上线下线提示。客户端能够发出连接请求,能编辑发送信息,可以指定发给单人或所有人,能显示聊天人数,上线下线用户等。 二、 通信规范的制定 服务请求规范: 服务器端: (1) 创建一个UDP的套接字,接受来自客户端的广播请求,当请求报文内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”时,接受请求,给客户端发送本服务器TCP聊天室的端口号。 (2) 创建一个主要的TCP协议的套接字负责客户端TCP连接 ,处理它的连接请求事件。 (3)在主要的TCP连接协议的套接字里面再创建TCP套接字保存到动态数组里,在主要的套接字接受请求后 ,就用这些套接字和客户端发送和接受数据。 客户端: (1) 当用户按“连接”按钮时,创建UDP协议套接字,给本地计算机发广播,广播内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”。 (2)当收到服务器端的回应,收到服务器发来的端口号后,关闭UDP连接。根据服务器的IP地址和端口号重新创建TCP连接。 故我思考:客户端一定要知道服务器的一个端口,我假设它知道服务器UDP服务的端口,通过发广播给服务器的UDP服务套接字,然后等待该套接字发回服务器TCP聊天室服务的端口号,IP地址用ReceiveForom也苛刻得到。 通信规范 通信规范的制定主要跟老师给出的差不多,并做了一小点增加: (增加验证用户名是否与聊天室已有用户重复,在服务器给客户端的消息中,增加标志0) ① TCP/IP数据通信 --- “聊天”消息传输格式 客户机 - 服务器 (1)传输“用户名” STX+1+用户名+ETX (2) 悄悄话 STX+2+用户名+”,”+内容+ETX (3) 对所有人说 STX+3+内容+ETX 服务器- 客户机 (0)请求用户名与在线用户名重复 //改进 STX+0+用户名+EXT (1)首次传输在线用户名 STX+1+用户名+ETX (2)传输新到用户名 STX+2+用户名+ETX (3)传输离线用户名 STX+3+用户名+ETX (4)传输聊天数据 STX+4+内容+ETX (注:STX为CHR(2),ETX 为CHR(3)) 三、 主要模块的设计分析 四、 系统运行效果 (要求有屏幕截图) 五、 心得与体会
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值