NodeJS和TCP:一本通

  • 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...bash


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位保留位socket

  • 6位控制位 TCP的链接、传输和断开都接受这个六个控制位的指挥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的区别post

紧急URG(urgent):ui

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

推送PSH(push):spa

当两个应用进程进行交互式通讯时,有时客户发一个请求给服务器时但愿当即可以收到对方的响应,这种状况下,客户应用程序通知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); //读
  });
});
复制代码
相关文章
相关标签/搜索