TCP全名为传输控制协议,在OSI(由七层组成:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)中属于传输层协议。HTTP、SMTP、IMAP协议都是基于TCP构建的。
TCP是面向连接的协议,特点是在传输之前需要3次握手(请求连接、响应、开始传输)形成会话。在创建会话的过程中,服务器端和客户端分别提供一个套接字,这两个套接字共同形成一个连接,服务端与客户端则通过套接字实现两者之间连接的操作。
node内置了net模块用于创建TCP连接。
const net = require("net");
const server = net.createServer(socket=>{
socket.on("data",data=>{
socket.write("你好\n");
});
socket.on("end",()=>{
console.log("连接断开\n");
});
socket.write("你好nodeJs\n");
});
server.listen(8080,()=>{
console.log("server is running~");
});
createServer()用于创建一个TCP服务器。然后利用telnet客户端来创建会话,这里用的是MobaXterm,因为windows的cmd每次需要设置编码比较麻烦:
telnet> open localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
你好nodeJs
键入内容并回车,会在TCP服务端触发data事件,内容作为参数传递给回调。执行quit退出telnet则会触发end事件,标识结束该TCP连接。
上述代码中,分为服务器事件和连接事件。
调用createServer()创建的服务,本身是一个EventEmitter实例,包含以下几种自定义事件:
1 ) listening事件
在调用listen绑定端口或Domain socket触发,简单写法就是listen的第二个回调函数。
server.listen(8080,()=>{
console.log("server is running~");
});
server.on("listening",()=>{
console.log("listening事件触发");
});
触发的顺序取决于注册的顺序,所以这里listen回调会先执行。
2 ) connection事件
TCP连接是可以一对多的,所以每个客户端连接到服务端会触发connection事件。
server.on("connection",()=>{
console.log("connection事件触发");
});
3 ) close事件
服务器关闭时触发。当调用server.close()方法后,将拒绝新的连接请求,原有的连接依然保持,当所有的连接中断后,触发close事件。
server.on("close",()=>{
console.log("所有会话已关闭");
});
4 ) error事件
服务器发生异常会触发该事件,比如侦听一个已使用的端口,如果没有注册该事件,服务器会抛出异常。
server.on("error",()=>{
console.log("error事件触发");
});
2、连接事件
服务器端可以与多个多户端保持连接,对每个连接而言是典型的可写可读Stream对象,该对象可用于服务器端与客户端的通信,可以通过data事件从一端读取另一端发来的数据,反之也可以通过write方法从一端向令一端发送数据。
1 ) data事件
一端调用write发送数据会触发另一端的data事件,事件传递的数据就是data发送的数据。
socket.on("data",(chunk)=>{
console.log("data事件触发",chunk);
});
2 ) end事件
当连接中的任意一端发送了FIN数据时,将会触发该事件。
客户端退出连接会触发该事件。
socket.on("end",()=>{
console.log("end事件触发");
});
3 ) connect事件
该事件用于客户端,当套接字与服务器连接成功时触发。
暂时不知道如何触发,待定。
4 ) drain事件
任意一端调用write()方法会触发该事件。
telnet下直接回车并不会触发,待定。
5 ) error事件
异常发生时触发该事件。
socket.on("error",()=>{
console.log("error事件触发");
});
6 ) close事件
套接字完全关闭时,触发该事件。
socket.on("close",()=>{
console.log("close事件触发");
});
7 ) timeout事件
当一定时间后连接不再活跃时,该事件会被触发。
暂时不知道如何触发,待定。
TCP针对网络中的小数包有一定的优化策略:Nagle算法。缓冲区的数据达到一定数量或一定时间后才将其发出,所以小数据包会被Nagle算法合并,以此来优化网络。但是带来的问题就是,有时候数据可能被延迟发送。
node中,TCP默认启用了Nagle算法,可以调用socket.setNoDelay(tyrue)来去掉该算法。
值得注意的是,尽管网络的一端调用write()方法会触发另一端的data事件,但并不意味着每次调用write只会触发一次data事件,关闭Nagle算法后,接收端可能会接收到多个小数据包的合并,然后只触发一次data事件。
UDP全名为用户数据报协议,同TCP一样也属于传输层。
UDP不是面向连接的,在TCP中每一个会话都是基于连接完成的,客户端如果要与另一个TCP服务通信则需要另一个套接字来完成。但在UDP中,一个套接字可以与多个UDP服务器通信,所以UDP是面向不可靠的连接服务,但由于资源消耗少处理速度快且灵活,所以广泛应用于偶尔丢几个包也无重大影响的场景,如音视频等。DNS服务就是基于UDP实现的。
首先要调用dgram模块,然后调用其createSocket方法。
const dgram = require("dgram");
const socket = dgram.createSocket("udp4");
参数可以是udp4或udp6。
只需要调用bind(port,[address])方法绑定网卡和端口即可。
const dgram = require("dgram");
const socket = dgram.createSocket("udp4");
socket.on("message",(msg,info)=>{
console.log(`server get ${msg} from ${info.address}:${info.port}`);
});
socket.on("listening",()=>{
const address = socket.address();
console.log(`server listening ${address.address}:${address.port}`);
});
socket.bind(8080,"127.0.0.1");
绑定完成后将触发listening事件。如果缺省address,将监听所有网卡的8080端口。
const dgram = require("dgram");
const client = dgram.createSocket("udp4");
const message = new Buffer("hello world");
client.send(message,0,message.length,8080,"192.168.1.1",()=>{
client.close();
});
保存并执行,服务端会输出:
server get hello world from 127.0.0.1:57367
send()方法参数如下:
send(buf,start,length,port,address,[callback]);
分别指要发送的buffer、buffer的偏移位置,buffer的长度,目标端口,目标地址,完成后的回调函数。
UDP套接字只是一个EventEmitter实例,而非stream实例。
接收到消息后将触发该事件,传递2个参数:Buffer对象和一个远程地址信息。
socket.on("message",(msg,info)=>{
console.log(`server get ${msg} from ${info.address}:${info.port}`);
console.log(msg);
console.log(info);
});
打印结果:
server get hello world from 127.0.0.1:55649
<Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
{ address: '127.0.0.1', family: 'IPv4', port: 55649, size: 11 }
UDP套接字开始侦听时触发该事件。如上调用bind方法后就会触发listening事件。
socket.on("listening",()=>{
const address = socket.address();
console.log(`server listening ${address.address}:${address.port}`);
});
调用close()方法会触发close事件,然后将不再触发message事件,除非再次绑定。
socket.on("close",()=>{
console.log("close事件被触发");
});
当异常发生时会触发该事件,如果没有监听该事件,异常将直接抛出使进程退出。
socket.on("error",()=>{
console.log("error事件被触发");
});
HTTP全称为超文本传输协议,构建在TCP上,属于应用层协议。node内置了http和https模块。
创建一个http服务:
const http = require("http");
const server = http.createServer((req,res)=>{
res.writeHead(200,{
"Content-Type":"text/plain"
});
res.end("Hello World\n");
});
server.listen(8080,"127.0.0.1");
然后通过命令行的curl来访问这个地址:
curl -v http://127.0.0.1:8080
打印结果:
* Rebuilt URL to: http://127.0.0.1:8080/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.50.3
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Tue, 12 Mar 2019 05:55:08 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact
整个请求过程由3部分组成:
1、经典的TCP三次握手
* Rebuilt URL to: http://127.0.0.1:8080/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
2、客户端向服务器端发送请求数据
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.50.3
> Accept: */*
3、服务器端向客户端返回响应信息,包括响应头和响应体
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Tue, 12 Mar 2019 05:55:08 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
最后是结束会话的信息
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact
可以看到,http是基于请求响应式的,以一问一答的方式实现服务,虽然是基于TCP的,但本身并无会话的特点。
http服务只做2件事:处理http请求和发送http响应。
http模块继承自net模块,http模块将连接所用的套接字的读写抽象成ServerRequest和ServerResponse对象,分别对应请求和响应操作。
请求的报文头会通过http_parser进行解析。比如对于报文头:
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.50.3
> Accept: */*
第一行解析出来会包含如下属性:
其余报头的信息会以key:value的形式存储在req.headers属性中。如:
{ host: '127.0.0.1:8080',
'user-agent': 'curl/7.50.3',
accept: '*/*' }
而报文体部分会抽象为一个只读流对象,这个数据流结束后才能进行操作,如:
const buffer = [];
req.on("data",chunk=>{
buffer.push(chunk);
}).on("end",()=>{
console.log(Buffer.concat(buffer));
res.end("Hello World\n");
});
res可以看成一个可写的流对象,可以调用setHeader()和writeHead()方法来设置响应头报文信息。
res.writeHead(200,{
"Content-Type":"text/plain"
});
可以多次调用setHeader(),但只有调用writeHead()后,报头才会写进连接中。
报文体部分则是通过res.write()和res.end()方法实现,同样res.write()可以多次调用,但只有调用res.end()才会真正结束响应。
res.write("hello\n");
res.write("World\n");
res.end("end\n");
注意,不能在end调用结束后继续调用write,否则会抛出异常。同时,报文头是先于报文体发送的,所以一旦开始了数据的发送(调用write方法),writeHead()和setHeader()将不再生效。
另外,无论如何,结束时一定要调用res.end()结束请求,否则客户端将一直处于等待状态。
1 ) connection事件
当连接建立时会触发一次connection事件。
server.on("connection",()=>{
console.log("connection事件触发");
});
2 ) request事件
当请求数据发送到服务器,在解析出http请求后,将会触发该事件。
server.on("request",()=>{
console.log("request事件触发");
});
3 ) close事件
同TCP服务器行为一致,调用server.close()会终止新的连接,当已有的连接都中断时,触发该事件。
也可以通过给close()传递一个回调函数来快速注册该事件。
server.close(()=>{
console.log("close事件触发");
});
4 ) checkContinue事件
某些客户端在发送较大的数据时,会先发送一个头部带Expect:100-continue的请求到服务器,然后触发checkContinue事件。如果没有注册该事件,则默认响应100 Continue状态码表示接受数据上传。如果不接受较多数据响应400拒绝即可。该事件发生不会触发request事件,收到100 Continue再次请求才会触发request事件。
5 ) connect事件
发起connect请求会触发connect事件,通常在http代理时出现。
如果不监听该事件,发起该请求的连接将会关闭。
6 ) upgrade事件
客户端要求升级连接的协议时需要与服务器端协商,客户端会在请求头中携带Upgrade字段,服务端会在接收到这样的请求时触发upgrade事件。在后续的webSocket中会有介绍。
7 ) clientError事件
连接的客户端触发error事件时,这个错误会传递到服务器端,此时触发该事件。
http模块除了创建服务端以外,还可以创建客户端来发起请求。
const http = require("http");
const client = http.request({
hostname:"127.0.0.1",
port:8080,
path:"/",
method:"GET"
},(res)=>{
console.log(`Status:${res.statusCode}`);
console.log(`Headers:${JSON.stringify(res.headers)}`);
res.setEncoding("utf8");
res.on("data",chunk=>{
console.log(chunk);
})
});
client.end();
回调打印结果:
Status:200
Headers:{"content-type":"text/plain","date":"Tue, 12 Mar 2019 07:42:44 GMT","connection":"close","transfer-encoding":"chunked"}
hello
World
end
request()的第一个参数对象定义了这个http请求头中的内容:
调用http客户端同时对一个服务器发起10次http请求时,实质上只有5个请求处于并发状态,后续的请求会在某个请求结束后才会继续,这与浏览器对同一域名下的连接数的限制时相同的行为。
传递agent参数可以改变这个行为,默认采用的是全局代理,这个连接限制是5。
const options = {
hostname:"127.0.0.1",
port:8080,
path:"/",
method:"GET",
agent:new http.Agent({
maxSockets:10
})
};
或者直接将agent设为false,脱离连接池的管理,使得请求不受并发的限制。
http客户端事件
SSL(Secure Sockets Layer)全称安全套接层协议,在传输层提供对网络连接加密的功能。对于应用层而言时透明的。数据在传递到应用层之前就已经完成了加密解密的过程。
随后SSL被标准化,称为TLS(Transport Layer Security)安全传输层协议。
node提供了3个模块:
TLS/SSL是一个公钥/私钥的结构,是非对称的。每个服务器端和客户端都有自己的公私钥,公钥用来加密数据,私钥用来解密数据。公钥和私钥是配对的,公钥加密的数据只有对应的私钥才可以解密。所以在建立安全传输之前,服务器端和客户端需要互换公钥,服务器端用客户端的公钥加密数据然后发给客户端,客户端用服务器端的公钥加密数据发给服务器端。
node底层采用openssl实现TSL/SSL,为此要生成公钥和私钥可以通过openssl完成。
不过首先得安装openssl,并添加进环境变量。这里提供一个不需要编译的方式:
现在就可以通过命令行来生成密钥了。
1 ) 首先生成客户端和服务端私钥(1024位RSA私钥):
openssl genrsa -out server.key 1024
openssl genrsa -out client.key 1024
我本地生成的密钥文件是这样的(server.key):
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQC/h9li/0qKRxZfWQObwMG8LxgaVxcjg3YKyMT+eY2tMlODryFg
my7eRF+HidbBU7GM/Z/528MyB7WabmFsF6mdM9+oHLIaC7/H82hOAevC6w/yOSLP
EXkGy3YI6WurooMmghnicWal/Np/U/sl/ofY70pPJHGVQmwYN+s486W/RwIDAQAB
AoGAbzNOcX3LJ1FymdUylSFq2fl1wwVBd+sBg+1hAmZMbXxEpLXvaQlwQrfrxuOu
ffw7n6I5WXXQdKGpPIpNodZzMMJoI1jO9yKvpXxU40G7BX84+MDW66uHNiLOAP3m
pSpOdgyK0m5abn70ToPzwxefgOp3eWGJd8UNomN87uMbUKECQQDu+Afu+YOXzUJP
0zde5wnwymoi9lK/VHj181Ji/0RWZz0tdGxbSEVTUeee6ifnTwRuxV8ry8rlLdnU
+eXhHIofAkEAzS5PxdNI5R7N85pCH1KqMBelmMsMJhhutdIhrPoulmAF8dwxkviM
D9jxG2kiuNZ2l8umSwGGSXQSRS6/nxv12QJAI6zzwkGN28PQ+onV4l0rpr8RSVbs
05OQ22cQDad+VEflYjvXUWlgsCeyJI9gla++QatFogwypjRKKPmF0C2qkQJAMC3o
w34qhsql98bIMgy6M9LJqsg7ERL5pC40hCa3G85udu2KooVEdlAtxY75fUe2z0wd
v00bWFIuHBqvGlB5eQJATl7MHvQ26hDxPZepIOc6Y5d5Gzo+kgHVF7zsb1N46B0h
RgBTkGhrmi6rihxPhBtjisryUfHhoyQRurVdXk3AeA==
-----END RSA PRIVATE KEY-----
2 ) 接着生成公钥:
openssl rsa -in server.key -pubout -out server.pem
openssl rsa -in client.key -pubout -out client.pem
公钥看起来应该是这样子的(server.pem):
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/h9li/0qKRxZfWQObwMG8Lxga
Vxcjg3YKyMT+eY2tMlODryFgmy7eRF+HidbBU7GM/Z/528MyB7WabmFsF6mdM9+o
HLIaC7/H82hOAevC6w/yOSLPEXkGy3YI6WurooMmghnicWal/Np/U/sl/ofY70pP
JHGVQmwYN+s486W/RwIDAQAB
-----END PUBLIC KEY-----
但其实还是有风险,假设有个中间人,中间人对服务器端是客户端,对客户端是服务器端。为了解决这个问题,数据传输过程中还需要对得到的公钥进行认证,以确认得到的公钥来自目标服务器。所以TLS/SSL引入了数字证书来认证。
与直接使用公钥不同,数字证书包含了服务器的名称和主机名、服务器的公钥、签名颁发机构的名称、来自签名颁发机构的签名。在连接建立前,会通过证书中的签名确认收到的公钥是来自目标服务器,从而产生信任关系。
为了确保数据的安全,需要引入一个CA(Certificate Authority,数字证书认证中心)。CA的作用用于给站点颁发证书,且这个证书具有CA通过自己的公钥和私钥实现的签名。
为了得到签名证书,服务器端需要通过自己的私钥生成CSR(Certificate Signing Request,证书签名请求)文件,CA机构通过这个文件颁发属于该服务器端的签名证书,只要通过CA机构就能验证证书是否合法。
通过CA机构颁发证书是个繁琐的过程,需要花钱。所以可以采用自签名证书来构建安全网络,自签名证书就是自己扮演CA机构,给自己服务器端颁发签名证书。
接着还是利用openssl来生成自签名证书。
1 ) 首先生成CA私钥文件:
openssl genrsa -out ca.key 1024
2 ) 接着用私钥生成CSR文件:
openssl req -new -key ca.key -out ca.csr
期间需要填一些东西:
3 ) 生成自签名证书
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
4 ) 回到服务端,先创建自己的CSR文件
openssl req -new -key server.key -out server.csr
5 ) 通过CA的证书和CA的私钥,结合服务端的CSR生成用于CA签名的证书
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
至此,便完成了自签名证书。
客户端在发起安全连接前会去获取服务器端的证书,并通过CA的证书验证服务器端证书的真伪。同时还含有对服务器名称、IP地址等进行验证的过程。
CA机构将证书颁发给服务器端后,证书在请求的过程中会被发给客户端,客户端需要通过CA的证书验证真伪。对于知名的CA机构,其证书一般会预装到浏览器中,自己扮演CA机构,客户端需要获取该CA证书才能进行验证。
可以看到签名是一环一环颁发的,但是CA的证书是不需要上级证书参与签名的,这个证书称为根证书。
证书都准备好了,接着通过tls模块来创建一个安全的TCP服务。
const tls = require("tls");
const fs = require("fs");
const options = {
key:fs.readFileSync("./keys/server.key"),
cert:fs.readFileSync("./keys/server.crt"),
requestCert:true,
ca:[fs.readFileSync("./keys/ca.crt")]
};
const server = tls.createServer(options,function(stream){
console.log(`server connected`,stream.authorized?"authorized":"unauthoirzed");
stream.write("welcome\n");
stream.setEncoding("utf8");
stream.pipe(stream);
});
server.listen(8080,function(){
console.log("server is running");
});
启动服务,然后通过以下命令测试证书是否正常:
openssl s_client 127.0.0.1:8080
利用node来模拟客户端,tls模块提供connect()来构建客户端。首先还是按照上述的方法生成客户端自己的签名:
openssl req -new -key client.key -out client.csr
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt
创建客户端:
const tls = require("tls");
const fs = require("fs");
const options = {
key:fs.readFileSync("./keys/client.key"),
cert:fs.readFileSync("./keys/client.crt"),
ca:[fs.readFileSync("./keys/ca.crt")],
host:"127.0.0.1",
path:"/"
};
const stream = tls.connect(8080,options,function(){
console.log(`client connected`,stream.authorized?"authorized":"unauthoirzed");
process.stdin.pipe(stream);
});
stream.setEncoding("utf8");
stream.on("data",data=>{
console.log(data);
});
stream.on("error",err=>{
console.log(err);
});
不幸的是,这个client启动会抛出异常:
{ Error: connect EPERM /
at Object._errnoException (util.js:1022:11)
at _exceptionWithHostPort (util.js:1044:20)
at PipeConnectWrap.afterConnect [as oncomplete] (net.js:1198:14)
code: 'EPERM',
errno: 'EPERM',
syscall: 'connect',
address: '/' }
哎,暂时不知道为什么,先记录一下。
const fs = require("fs");
const https =require("https");
const options = {
key:fs.readFileSync("./keys/server.key"),
cert:fs.readFileSync("./keys/server.crt")
};
const server = https.createServer(options,function(req,res){
res.writeHead(200);
res.end("hello world\n");
});
server.listen(8080,function(){
console.log("server is running");
});
const fs = require("fs");
const https =require("https");
const options = {
key:fs.readFileSync("./keys/client.key"),
cert:fs.readFileSync("./keys/client.crt"),
ca:[fs.readFileSync("./keys/ca.crt")],
host:"localhost",
path:"/",
port:8080,
method:"GET"
};
options.agent = new https.Agent(options);
const req = https.request(options,res=>{
res.setEncoding("utf8");
res.on("data",chunk=>{
console.log(chunk);
})
});
req.end();
req.on("error",err=>{
console.log(err);
});
再次不幸的是,会抛出异常:
{ Error: self signed certificate
at TLSSocket.<anonymous> (_tls_wrap.js:1105:38)
at emitNone (events.js:106:13)
at TLSSocket.emit (events.js:208:7)
at TLSSocket._finishInit (_tls_wrap.js:639:8)
at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:469:38) code: 'DEPTH_ZERO_SELF_SIGNED_CERT' }
究其原因是私有证书的问题,需要加这么一句:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
让node.js规避非授信证书的问题。
本章End~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。