NodeJS和TCP:一本通

2,132 阅读10分钟
  • TCP简介
    • TCP格式(Segment)
      • URG和PUSH的区别
    • TCP三次握手四次挥手
      • 三次握手
      • 四次挥手
  • Node.js的tcp实现
    • 基本介绍
    • tcp是长连接(socket)
      • 长连接注意事项
      • 设置超时
      • 关闭socket
        • 方法一:客户端手动关闭
        • 方法二:socket.end(),服务器让客户端关闭连接
      • 控制连接数
        • maxConnections
        • getConnections
    • 关闭服务器
      • server.close()
      • server.unref()
    • socket是一个双工流
      • 双工流简介
      • 关于读取
      • 关于读取
      • 关于pipe
    • socket的其它属性方法
      • socket.bufferSize
    • 端口被占用解决方案

pre-notify

参考与图片来源

维护ing...


TCP简介

TCP格式(Segment)

每一行32位

  • 源端口号(16位) --- 目的端口号(16位) 0~65535 计算机通过端口号识别访问哪个服务,比如http服务或ftp服务

  • 32位序列号,以便达到目的后重新组装数据包 TCP用序列号读数据包进行标记,假设当前的序列号为s,发送数据长度为i,则下次发送数据时的序列号为s+i。在建立连接时通常由计算机生成一个随机数作为序列号初始值。

  • 32位确认应答号 接收方收到数据后的答复信号 它等于下一次应该接受到的数据的序列号。假设发送端的序列号为s,发送数据长度为i,那么接收端返回的确认应答号也是s+i。发送端接收到这个确认应答后,可以认为这个位置以前所有的数据都已被正常接收。

  • 4位首部长度 TCP首部的长度,单位为4字节,如果没有可选字段,那么这里的值就是5(单位为4字节),表示TCP首部的长度为20字节。【1代表4个字节,4位8个状态能代表32个字节】

  • 6位保留位

  • 6位控制位 TCP的连接、传输和断开都接受这个六个控制位的指挥

    • URG 此包包含紧急数据,先读取紧急数据再读取其它
    • ACK(acknowlegement) 为1表示确认号
    • PSH(push急迫位) 缓存区将满(可手动置为1),立刻传输数据 (因为TCP有懒启动的概念,发一个字节不会立马发出去 会攒够一个量 再发)
    • RST(reset重置) 表示连接段了要重新连接
    • SYN(synchronous) 同步序列号位 表示要建立链接 TCP建立链接时要将这个值设为1
    • FIN发送端完成位,提出断开连接的一方把FIN置为1,表示要断开连接
  • 16位窗口值 客户端和服务端沟通好每次发送多少数据


  • 16位TCP校验和 校验数据是否完整 TCP校验和的计算包括TCP首部、数据和其它填充字节。
  • 16位紧急指针 表示标记为URG的数据在TCP数据部分中的位置。

  • 可选项

  • 数据

URG和PUSH的区别

以下引用自 TCP报文段中URG和PSH的区别

紧急URG(urgent):

当URG = 1时表明紧急指针字段有效,他告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送,发送方的TCP就把紧急数据放到本报文段数据的最前面。URG标志位要与首部中的紧急指针字段配合使用,紧急指针指向数据段中的某个字节,(数据从第一个字节到指针所指的字节就是紧急数据)。值得注意的是即使窗口为0时也可以发送紧急数据,紧急数据不进入接收缓冲区直接交给上层进程。

推送PSH(push):

当两个应用进程进行交互式通信时,有时客户发一个请求给服务器时希望立即能够收到对方的响应,这种情况下,客户应用程序通知TCP使用推送(push)操作,TCP就把PSH置为1,并立即创建一个报文段发送过去,类似的服务器的TCP收到一个设了PSH标志的报文段时就尽快将所有收到的数据立即提交给服务进程,而不在等到整个缓存都填满了再向上交付。

TCP三次握手四次挥手

三次握手

Q:为什么要握手?而且要三次?

答:握手是因为要确保真正开始发送数据之前,彼此(客户端,服务端)收、发数据皆正常,而之所以要三次,嗯。。。请接着往下看

接下来我们来看详细的过程


注意:[]中的为1位的信号,后面带=的是16位的序列号和确认号,是具体的编号。

01:客户端 [SYN]seq=0---> 服务端

****** ****** ******

02:客户端 <---[SYN,ACK]seq=0,ack=1 服务端

****** ****** ******

03:客户端 [ACK]seq=1,ack=1---> 服务端


第一次握手,服务端接收到了客户端发来的请求同步的信息,服务端就知道了客户端的发送是正常的。(嘿,我我好喜欢你)

第二次握手,客户端接收到了服务端发来的确认信息和同步信息,客户端就知道了服务端的收发(两样)是正常的。(我也好喜欢你,我们结婚吧)

第三次握手,服务端接收到了客户端发来的确认信息,服务端就知道了客户端的接收也是正常的。(嗯,我们结婚)

以上,就确保了彼此的收发消息都是正常的。

四次挥手

Q:为什么要挥手?而且要四次? 答:挥手是因为要和平分手,嗯。。。给对方以示意,有什么还没做完的搞快做,做完就了事。至于为什么要四次,嗯。。老套路,请看详细过程


首先和同步不一样,分手时哪边都可以提出分手

01:A方 [FIN,ACK]seq=xxx,ack=yyy---> B方

****** ****** ******

02:A方 <---[ACK]seq=yyy,ack=xxx+1 B方

****** ****** ******

03:A方 <---[FIN,ACK]seq=yyy,ack=xxx+1 B方

****** ****** ******

04:A方 [ACK]seq=xxx+1,ack=yyy+1---> B方

注意: 如果B方接受到A方的FIN时,恰巧也没数据要发送给A方了,那么02和03会合并为一次

第一次挥手,A方表示自己已经没有什么要发送给B方了,我要断开连接了

第二次挥手,B方表示我已经知道到你(A方)要断开连接了,稍等一下,我把剩下的数据发完

第三次挥手,B方表示我已经没有数据要发送了,你可以断开连接了

第四次挥手,A方表示我已经收到你最后发送的数据了,并且我已真正断开连接,这是我的遗言,此时若B方接受到就会关闭自己的这边

关于第四次挥手,A方挥手完毕后,还会等待2MSL(4min),如果此间又接收到B方发送的FIN,则表示最后次挥手发送的ACK对方没有收到,就会重新发送,并刷新等待时间,直到2MSL内不再收到B放发来的FIN(表示B放已收到最后的ACK并且关闭),A方彻底断开。

Node.js的tcp实现

基本介绍

Node.js 中用内置的 net 模块实现了 TCP 连接

let net = require('net');
let server = net.createServer(function(socket){
	...
}).lieten(8080);

其中的 socket 俗称为套接字,en...为嘛叫套接字?

我们通过socket能读取到客户端的输入以及能向客户端写入数据。

注意: 默认链接最大个数(backlog)为511 server.listen(handle[, backlog][, callback])

tcp是长连接

长连接注意事项

需要注意的是socket是长连接,这意味着它会一直保持连接直到我们手动去关闭客户端或则服务端表示要关闭连接。

另外因为是长连接,所以即使你每隔一段时间通过tcp连接向服务端发送信息, createServer 里注册的回调函数也只会执行一次。(不像http,一次请求就会执行一次),所以我们一般还会在createServer里包一层on('data')来实时监控客户端的输入以便做出响应。

net.createServer(function(socket){
    socket.on('data',function(buffer){
        console.log(socket._readableState.length);
    })
});

设置超时

因为tcp连接并不像http连接一样会自动中断,So有可能存在一个socket长期不使用却占着位置的情况,一般这种时候我们就会规定一个超时时间来做出一些操作,比如询问下人在不在啊(防挂机),要不要shuttdown啊什么的。

socket.setTimeout(5000);
socket.on('timeout',function(){
	socket.write('喂喂,有人吗?');
});

关闭连接(socket)

方法一:客户端手动关闭
方法二:socket.end(),服务器让客户端关闭连接

此时就相当于四次挥手中服务端向客户端提出分手[FIN,ACK]seq=xxx,ack=yyy

当客户端接到后一般会将第二第三次挥手合并到一起,向服务端回复[FIN,ACK]seq=yyy,ack=xxx+1,并且触发socket.on('end')注册的事件。

[warning] 注意: 这货并不像ws.end,临死之前还有遗言,会直接关掉socket套接字。

控制连接数

maxConnections

设置一个服务器最大的链接数

server.maxConnections = 111;
getConnections
server.getConnections(function(err,count){  //count为当前连接数
    console.log(`当前连接人数${count}人,最大容纳${server.maxConnections}`)
})

关闭服务器

server.close()

调用server.close()后,server并不会立刻关闭所有连接,close只是表示服务端不再接受新的请求了,当前的连接(socket)还能继续用。当所有客户端(socket)全部关闭后服务器才会关闭并触发close事件。

server.unref()

通过调用 server.unref()方法, 当服务器所有连接都关闭后,能让服务器自主关闭。这个方法和server.close的区别在于unref并不阻止新socket的进驻。

socket是一个双工流

双工流简介

socket继承自 Duplex(双工流),Duplex是一个可读可写的流

Duplex长这样

let {Duplex} = require('stream');
let d = Duplex({
    read(){
    	this.push('hello'); //不停止会一直以'hello'作为读取值读取
        this.push(null); //表示停止读取
    }
    ,write(chunk,encoding,callback){
    	console.log(chunk);
        callback(); //clearBuffer
    }
})

So,socket能使用一切可写流和可读流的方法进行读取和写入。

关于读取

我们通过客户端向服务端发送数据照理说很像写入,但在 socket 看来其实是读取。(类似于process.stdin.pipe(transform1).pipe(transform2),其中stdin也是读取

我们可以通过监听 on('data') 事件来读取客户端的输入。

socket.on('data',function(){});

也可以通过socket.pause暂停可读流,以及通过socket.resume继续读。

关于写入

socket的可写流层面和一般的可写流一般无二,可写流有的socket都有,write()flagdrain事件...

有一点要注意的是,socket的end,上面也说过,它是没有遗言的,即是你end('something'),也不会有输出。

关于pipe

let ws = fs.createWriteStream(path.join(__dirname,'./1.txt'));

let server = net.createServer(function(socket){
  socket.pipe(ws,{end:false}); // 第二个参数让文件不自动关闭
  setTimeout(function(){
    ws.end(); //关闭可写流
    socket.unpipe(ws); //取消管道
  },15000);
});

socket的其它属性方法

socket.bufferSize

write()的缓冲区实时大小

端口被占用解决方案

let port = 8080;
server.listen(port,'localhost',function(){
  console.log(`server is running at ${port}`);
})
server.on('error',function(err){
  if(err.code === 'EADDRINUSE'){
    server.listen(++port);
  }
});

server和client

创建一个server

let net = require('net');

let server = net.createServer(function(socket){
  socket.setEncoding('utf8');

  socket.on('data',function(data){
    console.log(data); //读
  })
  
  socket.write('ok'); //写
  socket.end(); //关闭socket
});

server.on('connection',function(){ //注意这个事件和getConnections事件很相似,但getConnections有err和count参数
  console.log('客户端链接');
})

server.listen(8080);

创建一个server

  • net.createConnection(port[, host][, connectListener]) 默认host为localhost
  • net.connect(port[, host][, connectListener]) 是第一种的别名形式

不同于创建tcp服务器时socket是作为回调函数中的参数,创建客户端的的时候,createConnection的返回值才是一个socket

let net = require('net');

// port 要连接到host的哪个端口
let socket = net.createConnection(8080,function(){
  socket.write('hello'); //写
  socket.on('data',function(data){
    console.log(data); //读
  });
});