net 模块提供了用于底层网络通信的工具,包含创建服务器/客户端的方法。
若传输的数据交互不通过 HTTP 协议,则可以使用 net 模块,如 WebSocket。
HTTP 协议本质上是以文本形式传输数据,它的传输数据量较大,而且它的传输需要二进制和文本之间进行转换和解析。
在 NodeJS 中,http 模块是继承自 net 模块的。
从组成来看,net 模块主要包含两部分:
net.Server 通常用于创建一个 TCP 或本地服务器。
通过 net.createServer()
创建的服务器为 EventEmitter 实例,它的定义事件有如下几种:
listening
:当调用 server.listen()
绑定服务器之后触发connection
:当新的连接建立时触发。socket 是一个 net.Socket
实例。close
:当服务器关闭时触发。如果连接存在,直到所有的连接结束才会触发该事件。error
:当服务器发生异常时触发。不同于 net.Socket
,close
事件不会在该事件触发后继续触发,除非 server.close()
是手动调用。监听指定端口 port 和 主机 host ac 连接。 默认情况下 host 接受任何 IPv4 地址(INADDR_ANY)的直接连接。端口 port 为 0 时,则会分配一个随机端口。
server.listen(port[,host][,backlog][,callback])
通过指定路径(path)的连接,启动一个本地 socket 服务器。
server.listen(path[, callback])
通过指定句柄连接。
server.listen(handle[,backlog][,callback])
options 的属性:
相当于调用 server.listen(port,[host],[backlog],[callback])
。还有,参数 path 可以用来指定 UNIX socket。
server.listen(options[, callback])
server.close([callback]);
关闭服务器,停止接收新的客户端请求。
callback
会被执行,同时会触发 close
事件callback
也会执行,同时将对应的 error
作为参数传入。(比如还没调用 server.listen(port)
之前),就调用了 server.close()
服务器可以同时与多个客户端保持连接,对于每个连接而言是典型的可写可读 Stream 对象。Stream 对象可以用于服务器端和客户端之间的通信,既可以通过 data
事件从一端读取另一端发来的数据,也可以通过 write()
方法从一端向另一端发送数据。它具有如下自定义事件:
allowHalfOpen == false
),socket 会自我销毁(如果写入待处理队列里面还没正式响应回包),但是我们可以设置 allowHalfOpen
参数为 true
,这样可以继续往该 socket 里面写数据,但是我们需要自己去调用 end
方法去消耗这个 socket,不然可能会造成句柄泄漏。error
。connect
事件将会被触发。如果连接中出现问题,error
事件将会代替 connect
事件被触发,并将错误信息传递给 error
监听器。最后一个 connectListener
,如果指定了,将会被添加为 connect
事件的监听器。true
。如果全部或部分数据在用户内中排队,则返回 false
。当缓冲再次空闲的时候将触发 drain
时间。data
事件将不会再被触发。可以用作对数据上传限制。data
参数,则相当于调用 socket.write(data, encoding)
之后再调用 socket.end()
。error
事件,任何监听器都将收到 exception
作为一个参数。true
的话在每次 socket.write()
的时候会立即发送数据,默认为 true
。end()
或者 destroy()
来断开连接。可选的 callback
参数将会被当作一个时间监听器被添加到 timeout
事件。0.0.0.0
,而客户端链接是 192.168.1.1
,最后的值是后者undefined
默认情况下,TCP 连接会启用延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们。如果短时间有多个数据发送, 会缓冲到一起作一次发送(缓冲大小见 socket.bufferSize
), 这样可以减少 I/O 消耗提高性能。
如果是传输文件的话,那么根本不用处理粘包的问题,来一个包拼一个包就好了。但是如果是多条消息,或者是别的用途的数据那么就需要处理粘包。
可以参见网上流传比较广的一个例子,连续调用两次 send
分别发送两段数据 data1
和 data2
, 在接收端有以下几种常见的情况:
data1
, 然后接收到 data2
data1
的部分数据, 然后接收到 data1
余下的部分以及 data2
的全部.data1
的全部数据和 data2
的部分数据, 然后接收到了 data2
的余下的数据.data1
和 data2
的全部数据.其中的 BCD 就是我们常见的粘包的情况. 而对于处理粘包的问题, 常见的解决方案有:
方案一
只需要等上一段时间再进行下一次 send
就好, 适用于交互频率特别低的场景。缺点也很明显,对于比较频繁的场景而言传输效率实在太低。不过几乎不用做什么处理。
方案二
关闭 Nagle 算法,在 Node.js 中你可以通过 socket.setNoDelay()
方法来关闭 Nagle 算法, 让每一次 send
都不缓冲直接发送。
该方法比较适用于每次发送的数据都比较大(但不是文件那么大),并且频率不是特别高的场景。如果是每次发送的数据量比较小,并且频率特别高的,关闭 Nagle 纯属自废武功。
另外,该方法不适用于网络较差的情况,因为 Nagle 算法是在服务端进行的包合并情况,但是如果短时间内客户端的网络情况不好,或者应用层由于某些原因不能及时将 TCP 的数据 recv
,就会造成多个包在客户端缓冲从而粘包的情况。如果是在稳定的机房内部通信那么这个概率是比较小可以选择忽略的。
方案三
封包/拆包是目前业内常见的解决方案了。即给每个数据包在发送之前,于其前/后放一些有特征的数据,然后收到数据的时候根据特征数据分割出来各个数据包。
为每一个发送的数据包分配一个序列号(SYN, Synchronize packet),每一个包在对方收到后要返回一个对应的应答数据包(ACK, Acknowledgement)。发送方如果发现某个包没有被对方 ACK,则会选择重发。接收方通过 SYN 序号来保证数据的不会乱序(reordering),发送方通过 ACK 来保证数据不缺漏,以此参考决定是否重传。关于具体的序号计算, 丢包时的重传机制等可以参见阅读陈皓的 《TCP的那些事儿(上)》 此处不做赘述。
创建 TCP 服务器,可以通过构造函数和工厂方法实现,两者都会返回一个 net.Server
类,可接收两个可选参数。
new net.Server
net.createServer()
const net = require('net');const server = net.createServer();// const server = new net.Server()server.on('connection', socket => {socket.pipe(process.stdout);socket.write('data from server');});server.listen(3000, () => {console.log(`server is on ${JSON.stringify(server.address())}`);});
在 listen 监听的时候没有指定端口的话会自动随意监听一个端口,创建完成一个 TCP 服务器后,使用 tenlent 0.0.0.0 3000
,链接后可与服务器进行数据通信。通过 createServer
实例化一个服务后,服务会去监听客户端请求,与客户端建立了链接之后会在回调里面抛出建链的 net.Socket
对象。
Server 对象的特点:
createServer
是 new net.Server
的语法糖_handle
时 Server 处理的句柄,属性值最终由 C++ 部分的 TCP
和 Pipe
类创建connectionListener
是 connection 事件回调函数的语法糖创建一个 TCP 客户端,同样可以采用构造函数和工厂方法实现,创建成功后都会返回一个 net.Socket
实例。
new net.Socket
net.createConnection()
-> net.connect()
const net = require('net');const client = net.connect({ port: 3000 });client.on('connect', () => {client.write('data from client');});client.on('data', chunk => {console.log(chunk.toString());client.end();});