七、网络编程
- Node提供了net,dgram,http,https模块,分别用来处理TCP、UDP、HTTP、HTTPS,适用于服务器端和客户端。
1. 构建TCP服务
1.1 创建TCP服务器端
-
我们可以通过net模块进行TCP服务器端构建: let net = require('net');
const server = net.createServer(function (socket) {
socket.on('data',function(data){
socket.write('hello\n');
})
socket.on('end',function() {
console.log('server out');
})
socket.write('welcome to TCP')
})
server.listen(3000,function(){
console.log('server bound');
})
利用telnet命令作为客户端对服务器进行对话交流: telnet 127.0.0.1 3000
welcome to TCP
hi
hello
-
除了端口,我们还可以对Domain Socket进行监听,然后利用: let net = require('net');
const server = net.createServer(function (socket) {
socket.on('data',function(data){
socket.write('hello\n');
})
socket.on('end',function() {
console.log('server out');
})
socket.write('welcome to TCP')
})
server.listen('/test.sock',function() {
console.log('server bound');
});
然后利用nc命令进行对话: nc -U /test.sock
welcome to TCP
1
hello
1.2 创建TCP客户端
-
通过net模块自行构造客户端: let net = require('net');
let client = net.connect({
port : 3000,
path : '/test.sock'
},function (){
console.log('connected');
client.write('hi');
})
client.on('data',function(data){
console.log(data.toString());
client.end();
})
client.on('end',function() {
console.log('client disconnect');
})
connected
welcome to TCP
hello
client disconnect
1.3 TCP服务事件
1.3.1 服务器事件
1.3.2 连接事件
-
每个服务器可以与多个客户端保持连接,对于每个连接而言是典型的可读可写的Stream对象; -
Stream对象可以用于服务端可客户端之间的通信,通过data事件接收,通过write方法发送数据。 -
主要的事件如下:
data:当一端调用write方法发送数据时,另一端触发。
end:当连接中的任意一端发送了FIN数据时触发。
connect:用于客户端,当套接字与服务器连接成功时会触发。
drain:当任意一端调用write发送数据时, 当前端会触发该事件。
error:异常时触发。
close:当套接字完全关闭后触发。
timeout:当一段时间后连接不在活跃后,触发该事件,通知用户这连接已经被闲置了。
-
由于TCP套接字是Stream对象,可以利用pipe方法事件管道操作:
let net = require('net');
let server = net.createServer(function(socket){
socket.write('echo server');
socket.pipe(socket);
})
server.listen(3000);
1.4 Nagle算法
-
针对网络中的小数据包,TCP有一定的优化策略——Nagle算法。 -
如果每次只发送一个字节的内容而不优化,网络中将充满只有极少数有效数据的数据包,十分浪费网络资源。 -
针对这种情况,Nagle算法要求缓冲区的数据达到一定数量或一定时间后才进行发送,这样可以使得网络带宽被有效使用。但是,可能会造成延迟发送。 -
在Node中,TCP默认开启了此算法,可以调用socket.setNoDelay(true) 来去掉,使得write内容可以立刻发送。
1.5 data事件细节
- 尽管在网络中的一端调用write方法会触发另一端的data事件,但是并不意味着每次write都会触发一次data事件;
- 在关闭Nagle算法后,接收端可能会接收多个小数据包,合并后再触发一次data事件。
2. 构建UDP服务
- UDP又称数据包协议,与TCP一样属于网络传输层。
- UDP不是面向连接的,在UDP中,一个套接字可以与多个UDP服务进行通信;
- UDP虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下,存在丢包严重的问题,但是由于他无需连接,资源消耗低,处理灵活快速,因此常常应用在即使偶尔丢了一两个数据包也无大碍的场景,如音频和视频。
- DNS服务就是基于UDP实现的。
2.1 创建UDP服务器端
const { Server } = require('http');
const server = dgram.createSocket('udp4');
server.on('message',function(msg,rinfo) {
console.log('server got : ' + msg + ' from ' + rinfo.address + ': ' + rinfo.port);
})
server.on('listening',function() {
let address = server.address();
console.log('server listen at ' + address.port);
})
server.bind(3000);
2.2 创建UDP客户端
const dgram = require('dgram');
let msg = new Buffer('i am yivi');
let client = dgram.createSocket('udp4');
client.send(msg,0,msg.length,3000,'localhost',function(err,bytes) {
client.close();
})
分别启动服务端和客户端后,控制台出现以下输出:
server listen at 3000
server got : i am yivi from 127.0.0.1: 56059
- 与TCP的write方法相比,send方法虽然复杂了点,但是它可以灵活地发送数据到网络中的服务器端,而TCP必须建立通过套接字建立新的连接。
3. 构建HTTP服务
- HTTP是超文本传输协议(HyperText Transfer Protocol),属于应用层协议;
- HTTP两端是浏览器和服务器,即B/S模式
3.1 HTTP报文
const http = require('http');
let server = http.createServer((req,res)=>{
res.writeHead(200,{
'Content-Type' : 'text/plain'
})
res.end('hello');
})
server.listen(3000,function(){
console.log('server start');
});
C:\Users\Administrator>curl -v http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.55.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Mon, 26 Apr 2021 08:19:13 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
hello
* Connection
-
该报文可以分为几部分:
-
TCP的三次握手过程 * Rebuilt URL to: http://localhost:3000/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (
-
客户端向服务端发送请求报文 > GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.55.1
> Accept: */*
>
-
服务器端处理完成后,向客户端发送相应内容(响应头和响应体) < HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Mon, 26 Apr 2021 08:19:13 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
hello
- HTTP的特点是请求响应式的,一问一答的模式,虽然是基于TCP会话,但是却无会话特点。
3.2 http模块
- 在Node中,HTTP服务继承于TCP服务器(net模块),它能够与多个客户端保持连接,采用事件驱动的形式,并不为每个连接创建额外的线程或进程,保持很低的内存占用,因此可以实现高并发。
- HTTP服务与TCP服务模型的区别是,当开启keeplive后,一个TCP会话可以用于多次请求和响应。
- http模块是将net模块的connection进行了封装,形成request对象。
- http模块将连接所用的套接字读写抽象成ServerRequest和ServerResponse对象。
- 在请求产生的过程中,http模块拿到连接中传来的数据,然后嗲用二进制模块http_parser进行解析,解析请求报文报头后,触发request事件,调用用户业务逻辑。
3.2.1 HTTP请求
-
请求的报头被解析后放置在req.header属性上 -
报文体则抽象为一个只读流对象,如果业务逻辑需要读取报文体中的数据,则需要在这个数据流结束后才能操作: function (req,res) {
let buffers = [];
req.on('data',function(chunk){
buffers.push(chunk);
}).on('end',function(){
let buffer = Buffer.concat(buffers);
res.end('done');
})
}
3.2.2 HTTP响应
-
设置响应头的API为res.setHeader() 和res.writeHeaders() ; -
我们可以多次调用res.setHeader() 设置,但只有调用writeHeaders() 后才会将头部写到连接中。 -
一旦数据开始发送,请求头的设定将失效。 -
而报文体则是调用res.write() 和res.end() 方法实现的,两者的区别是后者会先调用前者发送数据后再告知服务器这次响应结束。 -
响应结束后,HTTP服务器可能会将当前的连接用于下一个请求,或者关闭链接。 -
无论服务器端在处理业务时是否发生异常,再业务结束时务必调用res.end() 结束请求,否则客户端将一直处于等待状态。
3.2.3 HTTP服务的事件
-
connection事件:在开始HTTP请求和响应前,客户端与服务器需要建立底层的TCP链接,这个链接可能开启了keep-alive,可在多次请求和响应之间使用;当连接建立时会触发这个connection事件。 -
request事件:建立TCP链接后,http模块将从底层抽象出HTTP请求和HTTP响应,当请求数据发送到服务器端,在解析出HTTP请求头后,将会触发该事件;在res.end后,TCP连接可能将用于下一次请求响应。 -
close事件:与TCP服务行为一致,当server.close()调用时,停止接受新的连接,当已有的连接都断开的时候,触发该事件;可以给close()传递一个回调函数来注册该事件。 -
checkContinue事件:当客户端发送大数据时,并不会将数据直接发送,而是会发送一个头部信息带有Expect:100-continue的请求到服务器,服务器将会触发checkContinue事件。若没有为服务器监听这个事件,服务器会自动响应Expect,表示接收数据上传;若不接受,只需要返回400响应状态码即可。注意,此事件与request事件互斥,当客户端收到100Continue后才会触发request事件。 -
connect事件:当客户端发起Connect请求时触发,一般在HTTP代理时出现;若不监听该事件,发起该连接的请求将会关闭。 -
upgrade事件:当客户端要求升级连接的协议的时候,需要和服务器端协商,客户端会在请求头中带上Upgrade字段,服务器将会在接收到该请求的时候触发此事件。若不监听,则发起该连接的请求关闭。 -
clientError事件:连接的客户端发生error时,会传递到服务器端,触发该事件。
3.3 HTTP客户端
const http = require('http');
let option = {
hostname : '127.0.0.1',
port: 3000,
path: '/',
method: 'GET'
}
let req = http.request(option,function(res){
console.log('status:' + res.statusCode);
console.log('headers:' + JSON.stringify(res.headers));
res.setEncoding('utf-8');
res.on('data',function(chunk){
console.log(chunk);
})
})
req.end();
3.3.1 HTTP代理
-
http的ClientRequest对象是基于TCP层实现的,在keepalive情况下,一个底层会话连接可以用于多次请求。 -
为了重新利用TCP连接,http模块提供了一个客户端代理对象——http.globalAgent,对每个服务器端创建的连接进行管理,通过ClientRequest对象向服务器端发送的http请求最多只能5个, -
默认情况下,请求会使用全局代理对象, 默认并发数为5。 -
如果需要设置高并发的请求,可以通过构造新的http.Agent()对象进行设置: let agent = new http.Agent({
maxSockets: 10
})
let options = {
......
agent: agent
}
-
一旦并发的请求数量增多,会导致服务器性能下降,所以要合理设置并发限制。 -
Agent对象的sockets属性代表连接的数量,requests属性代表等待连接的请求数量。
4. 构建Websocket服务
- Websocket客户端基于事件的编程模型与node几乎一致;
- Websocket实现了长连接,而Node的事件驱动十分适合处理大量客户端保持高并发连接。
- 客户端和服务端仅建立一个TCP连接;
- 服务端可以推送数据到客户端,远比HTTP请求响应模式更加灵活;
- 有更加轻量级的协议头,减少数据传输量
4.1 Websocket握手
- 客户端建立连接,通过HTTP发起请求报文,请求头中包含一个特殊的字段——
Sec-Websocket-Key ,用于安全校验。 - 该字段是随机生成的Base64编码的字符串,服务端接收到该字段后,首先进行拼接后,通过sha1散列算法计算出结果后,再进行Base64编码,最后返回给客户端。
- 客户端会校验服务器返回的
Sec-Websocket-Key ,检验通过后,将进行数据传输。 - 一旦握手成功,客户端和服务端是对等的关系,都能接收和发送消息。
4.2 数据传输
- 握手成功后,当前连接将不再进行HTTP交互,开始Websocket数据帧协议。
- 为了安全考虑,客户端需要对传输的数据帧进行掩码处理,一旦服务端接收到无掩码处理的数据帧,连接将关闭;而服务端无须进行掩码处理,如果客户端接收到掩码处理的数据帧,连接将关闭。
5. 网络服务与安全
5.1 TSL/SSL
-
SSL(Secure Socket Layer 安全套接层)是一种安全协议,他在传输层提供了对网络连接加密的功能,对于应用层是透明的,数据传输到应用层前就已经完成了加密和解密的过程。 -
后来被IETF标准化,称为TSL(Transport Layer Secure)。 -
TSL/SSL是一个公钥/私钥的结构,是非对称的,每个服务器端和客户端都拥有自己的公私钥; -
公钥用来加密传输的数据,私钥用来解密。 -
通过公钥加密的数据需要对应的私钥才能解密,因此,在建立连接的时候,客户端和服务器端需要交换公钥。 -
整个数据传输流程是这样的:
客户端传输数据;
客户端使用服务器端公钥进行加密;
服务器端接收数据,使用服务器端私钥进行解密;
服务器端返回数据,使用客户端公钥进行加密;
客户端接收数据,使用客户端私钥进行解密;
-
在Node底层是使用openssl 生成公私钥的; -
公私钥虽然是非对称的,但是网络中仍然存在窃听的行为,例如,中间人攻击。 -
中间人对客户端扮演服务器端的角色,对服务器端扮演客户端的角色,因此两端完全感受不到中间人的存在。 -
因此,为了解决这个问题,数据传输过程中还需要对得到的公钥进行验证,TLS/SSL引入了数字证书来进行验证。 -
数字证书中包含了服务器的名称、主机名、服务器公钥、签名颁发机构的名称、来自签名办法机构的签名等信息,在建立连接的时候,客户端会先通过证书中的签名来确认收到的公钥是来自目标服务器的,从而产生信任关系。
5.2 数字证书
- 为了确保我们的数据安全,现在我们引入了一个第三方:CA(Certificate Authority 认证中心)
- CA的作用是为站点颁发证书,且这个证书中具有CA通过自己的公钥和私钥实现的签名。
- 为了得到签名证书,服务器端需要通过自己的私钥生成CSR(Certificate Signing Request)文件,CA将通过这个文件颁发属于这个服务器的签名证书。
- 自签名证书:自己扮演CA机构,对服务器端颁发证书签名。
|