该文章阅读需要5分钟,更多文章请点击本人博客halu886
前言
Node是一个面向网络而生的平台,具有事件驱动,无阻塞,单线程等特性。具有良好的可伸缩性,适合在分布式式网络中扮演各式各样的角色。同时提供的网络基础API非常贴合网络,非常适合基于基础API构建灵活的网络服务。
利用Node可以很容易的搭建Web服务。像其他语言都需要Web服务器作为容器。如ASP,ASP.NET需要IIS作为服务器。PHP需要搭建Apache或Nginx环境等。JSP需要Tomcat等等。但是对于Node来说,只需要仅仅几行代码就可以构建服务器,不需要额外的容器。
Node提供了net,dgram,http,https这4个模块,分别用来处理TCP,UDP,HTTP,HTTPS,适用于服务端和客户端。
构建TCP服务
TCP在网络中十分常见,目前大部分应用都是构建在TCP上的。
TCP
TCP全称是传输控制协议,在OSI模型(分为七层,物理层,数据链路层,网络层,传输层,会话层,表现层,应用层)位于传输层协议,典型的HTTP,SMPT,IMAP等就是构建在TCP上。
TCP是典型的面向连接的协议,特征是在传输之前需要3次握手形成会话。
只有会话形成,客户端和服务端才能开始传输数据。创建过程中,服务端和客户端分别提供一个的套接字共同形成一个链接。服务端和客户端通过套接字形成两者链接的操作。
创建TCP服务器端
现在我们来创建一个TCP服务端接受网络请求。
var net = require('net');
var server = net.createServer(function(socket){
socket.on('data',function(data){
socket.write("你好");
})
socket.on('end',function(){
console.log('连接断开');
})
socket.write("hello world!\n");
});
server.listen(8124,function(){
console.log('server bound');
})
我们通过net.createServer()创建一个TCP服务器,server.listen表示connection的侦听器,也可以用如下方式:
var server = net.createServer();
server.on('connection',function(socket){
// 新的连接
})
server.listen(8124);
我们可以用telnet作为客户端和刚刚创建的简易服务器进行会话交流。
$ telnet 127.0.0.1 8124
Trying 127.0.0.1
Connected to localhost.
Escape character is '^]'.
hello world
hi
除了端口外,我们还可以对Domain Socket进行监听。
server.listen('/tmp/echo.sock');
通过nc工具进行上述的构建的TCP服务的会话测试。
$ nc -U /tmp/echo.sock
hello world
hi
也能通过net模块自行构建客户端进行会话。
var net = require('net')
var client = net.connect({port:8124},function(){
console.log('client connected');
client.write('world!\r\n');
});
client.on('data',function(data){
console.log(data.toString());
client.end();
})
client.on('end',function(data){
console.log('client disconnected');
})
将以上客户端存为client.js,执行如下
$ node client.js
client connected
hello world!
你好
client disconnected
其结果与使用nc和Telnet的结果并无区别。如果是Domain Socket,填写path即可。
var client = net.nonnect({path:'/tmp/echo.sock'});
TCP服务的事件
以上例子中,事件分为服务器事件和连接事件
服务器事件
对于通过net.createServer()创建的服务器而言,它是一个EventEmitter实例,它的自定义事件如下几种。
- listening:再调用server.listen()绑定端口或者Domain Socket后触发。
- connect:每个客户端连接到服务端时触发,简洁用法可以用net.createServer(),最后一个参数传递。
- close:当服务端关闭时触发,当调用sever.close()后,停止接受新的客户端套接字连接,同时等待连接断开后触发该事件。
- error:当服务器发生异常,则会触发这个事件。例如侦听一个正在使用中的端口,则会触发这个事件。如果没有侦听error事件,服务器则会抛出异常。
连接事件
服务器可以和多个客户端保持连接,对于每个连接来说都存在一个可读可写Stream流用于服务端到客户端的通信。可以侦听data事件进行读取另一端的数据,通过write()像另一端发送数据。
- data:当一端通过write发送数据,另一端则会触发data事件。接受到数据则是write传入的数据。
- end:当任何一段发送Fin数据包,则会触发这个事件。
- connect:用于客户端连接服务端,当套接字与服务端连接成功时触发。
- drain:当任意一端调用write()时,当前这端触发该事件。
- error:异常发生时,触发该事件。
- close:当套接字完全关闭时,触发该事件。
- timeout:当连接闲置了一段时间后,触发该事件。
另外,TCP套接字是一个可读可写的Stream流,可以通过pipe()实现管道操作。
var net = require('net');
var server = net.createServe(function(socket){
socket.write('Echo server\r\n');
socket.pipe(socket);
})
server.listen(1337,'127.0.0.1');
不过,TCP在传输小数据包时存在一个优化策略。Nagle算法,如果不存在这个算法的话,网络传输中全是相同的小数据包,十分浪费网络资源。这个算法是将数据缓存在一个缓冲区到一定数据量或一定时间量时再一起发出去,所以小数据包会被Nagle算法合并,能够起到节省宽带的作用。不过这样的副作用则是有可能数据包会被延迟发送。
在Node中,TCP默认采用了启用Nagle算法,可以通过Socket.setNotDaley(true)去掉Nagle算法。使得write()后立即将数据发出到网络中。
不过,通过write()写入数据后会触发data事件,关闭Nagle算法后,也不意味这每次执行write方法都会触发data事件。另一端将多个小数据包合并,然后只触发一次data事件。
构建UDP服务
UDP称为用户数据包协议,和TCP同属于网络传输层。和TCP最大的不同是UDP不是面向连接。TCP连接一旦建立,会话都是基于连接完成,并且每个不同的连接都需要不同的套接字。但在UDP中,一个套接字可以和多个UPD服务端会话。虽然提供的是面向事务的简单不可靠传输服务,在网络情况差的情况下丢包严重,但是无需连接,资源消耗低,处理快速且灵活。所以常常应用于丢一两个包也不影响的场景,例如音频,视频。UDP目前引用非常广泛,CDN服务就是基于UDP实现的。
创建UPD套接字
创建UPD套接字十分简单,UDP套接字一旦建立,既可以作为客户端发送数据,也可以作为服务端接收数据。
var dgram = require('dgram');
var socket = dgram.createSocket('udp4');
创建UDP服务端
若想让UPD套接字接受网络消息,只需要用dgram.bind(port,address)对网卡和端口进行绑定即可。
var dgram = require("dgram");
var server = dgram.createSocket("udp4");
server.on("message",function(msg,rinfo){
console.log(msg +" from " + rinfo.address + ":" +rinfo.port);
})
server.on("listening",function(){
var address = server.address();
console.log(address.address+ ":" + address.port);
})
server.bind(41234);
创建UDP客户端
创建一个UDP客户端与客户端通信。
var dgram = require('dgram');
var message = new Buffer('hello world');
var client = dgram.createSocket('udp4');
client.send(message,0,message.length,41234,"localhost",function(err,bytes){
client.close();
})
保存client.js并执行。
$ node server.js
server listening 0.0.0.0:41234
server got:hello world from 127.0.0.1:58682
当套接字用在客户端时,使用send()方法发送消息到网络中。send()方法参数如下:
socket.send(buf,offset,length,port,address,[callback])
这些参数分别为要发送的Buffer,偏移位,长度,端口,地址以及回调。它与TCP相比,参数相对复杂一点,但是可以随意发送到网络中服务端,但是如果TCP需要重新发送数据另一个服务端的话,则需要通过套接字构造一个新的连接了。
UDP套接字事件
UDP套接字使用起来相对容易一点,只是一个EventEmitter的实例,而非Steam的实例。
- message:当UDP套接字侦听网口端口后,接收到消息后触发该事件,携带的数据为Buffer数据和和一个远程地址信息。
- listening:当UDP套接字开始侦听时触发该事件。
- close:调用close时触发该事件,不在触发message事件,重新侦听地址和端口则可重新触发。
- error:当异常发生时触发该事件,如果不侦听该事件,则异常抛出,使进程退出。
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)
该文章阅读需要5分钟,更多文章请点击本人博客halu886
构建HTTP服务
TCP和UDP都属于传输层的协议,如果需要构想高性能的网络传输,就应该从传输层入手。 如果对于经典的业务场景,应用层协议远远够了,例如http和smtp等。
用Node构建一个http服务通过几行代码就够了。
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200,{'Content-Type':'text/plain'});
res.end('Hello World\n');
}).listen(1337,'127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
虽然只能返回hello world,但是维持的并发量和QPS都不容小觑的。
Http
初识HTTP
HTTP称为超文本传输协议,写作HyperText Transfer Protocol。构建在TCP协议上。属于应用层协议。 用于服务端和客户端通信,著名的B/S模式,
HTTP的发展是W3C和IETF的合作结果,他们最终发布了一系列RFC标准,目前最著名的HTTP标准就是RFC2612。
Http报文
我们通过curl调用上述代码启动的本地http服务器的接口,查看http报文。
$ curl -v http://127.0.0.1:1337
* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1(127.0.0.1) port 1337 (#0)
> Get / HTTP/1.1
> Uesr-Agent: curl/7.24.0(x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.0r zlib/1.2.5
> Host:127.0.0.1:1337
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat,06 Apr 2013 08:01:44 GMT
< Connection:keep-alive
< Transfer-Encoding: chunked
<
Hello World
* Connection #0 to host 127.0.0.1 left intact
* Cloing connection #0
上述的报文分为几个阶段,最经典的就是三次TCP三次握手。
* About to connect() to 127.0.0.1 port 1337(#0)
* Trying 127.0.0.1..
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)
第二部分是向服务端发送请求报头
> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 openSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>
第三部分是服务器端向客户端发送返回内容,包括响应头和响应体。
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
最后是结束会话的阶段
* Connection #0 to host 127.0.0.1 left intact
* Closing connetion #0
从上述报文可以看出,http是基于请求响应式,一问一答实现服务的。虽然基于TCP会话,但是不存在会话的特点。
从协议的角度上来说,浏览器就是一个HTTP代理,将用户的行为转化为请求报文发送给服务端,服务端处理请求,返回响应报文给http代理,代理解析响应报文,将内容显示在界面上。以查看图片为例子,浏览器发送请求报文,服务器解析图片路径,将磁盘中图片文件以报文的形式发送给客户端。浏览器接收到报文后,调用渲染引擎将内容显示给用户。http服务只有两件事,处理http请求,发送http响应。
无论是http请求体还是响应体,都只包含两件部分,报文头和报文体。
上述部分<和>分别为请求头和响应头,由于是get请求,所以没有报文体,hello world是响应的报文体。
http模块
Node的Http模块是继承自TCP模块,基于事件驱动,不会为每一个请求就生成额外线程或者进程,内存占用比非常底,所以能实现高并发。Http模块与TCP模块的区别在于,启动了keepalive后,tcp能够用于多次请求和响应。HTTP服务是以Request为单位进行服务,Tcp服务是以Connection为单位进行服务,http模块即将connection到request进行封装。
HTTP模块将嵌套字的读写抽象为ServerRequest对象和Respond对象,分别对应请求和响应操作。当请求产生时,http拿到连接中的数据,调用二进制模块http_parse解析完报头后,触发Request事件,然后调用用户的业务逻辑。
HTTP请求
对于TCP的连接的读操作,http模块将其封装为ServerRequest对象,如下是经过http_parse的请求报文
> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>
报文头 GET / HTTP/1.1 会被解析为如下属性
- req.method熟悉为请求方法,值为GET,常用的有GET,PUT,POST,DELETE,CONNECT几种。
- req.url属性,值为/
- req.httpVersion熟悉,值为 1.1;
其他报头主要是Key:Value格式,解析完后给业务逻辑处理。
报文体被抽象为一个流对象,只有在报文接收完毕后业务逻辑才可以进行读操作。
function(req,res){
//console.log(req.headers);
var buffers = [];
req.on('data',function(trunk){
buffers.push(trunk);
}).on('end',function(){
var buffer = Buffer.concat(buffers);
res.end('Hello world')
})
}
HTTP响应
HTTP响应是封装流中的可写操作,可以看成一个可写的流对象。
在编辑响应体的头部主要分两步setHeader()和writeHeader(),可以通过setHeader()设置多个参数,但是只有通过writeHeader后才会写入到连接。
并且http模块会自动设置一些头信息。
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding:chunked
这里的keep-alive表示的是当前的连接不关闭,可以用于下一个请求。chunked表示的则是默认使用分块传输协议(chunked transfer encoding),放弃采用缓存的方式传输返回体,而且分块传输从而提升效率,最后一个数据块用0表示传输完毕。
写入响应体的内容则是通过write()和end(),两者的区别在于end()会调用write()将数据写入连接中,然后告诉服务端响应结束。
一旦开始发送报文则writeHeader()和setHeader()不在生效,因为报头在报文发送前发送,这是由于http协议特性决定的。
HTTP事件
HTTP服务如TCP服务一样抽象了一些事件,并且HTTP服务器也是一个EventEmiter实例。
- connection:当开始发送请求和响应时,需要建立底层的TCP连接,有些时候也会开启keep-live用以保持连接。当链接建立完成时触发该事件。
- request:当链接建立完成,请求的报头发送完毕解析完成时触发该事件。
- close:与TCP模块服务类似,调用server.close不再接收新的链接,当所有链接都断开时触发该事件,也可以通过server.close()传入回调快速触发该事件。
- checkContinue:当客户端发送一个较大数据的请求时,会先发送一个报头带有expect:100-continue进行确认,当接收到请求报头时触发该事件。如果没有监听该事件,则默认返回状态码为100 continue表示同意接收,否则返回400 Bad Request拒绝接受。该事件与reqeust事件互斥,当监听了该事件,request事件不会被触发。当客户端接收到100 continue重新发起请求,下一个请求才会触发request事件。
- connect:当客户端发起CONNECT请求时(通常用于代理时发起),触发该事件,当没有监听该事件时,自动关闭该连接。
- upgrade:当请求报文中携带这个字段(用于与服务器端协商升级协议),如果没有监听该事件,自动断开链接
- clientError:当客户端触发error时,会将error传递给服务端,触发该事件。
HTTP客户端
HTTP模块提供了一个底层API http.request(options,connect)用来实例化一个http客户端。
options有如下选项
- host:服务器端口和域名,默认为127.0.0.1
- hostname:服务器名称
- port:服务器端口,默认为80
- localAdress:建立连接的本地网卡
- socketPath:domain套接字路径
- method:http请求方法,默认为Get
- path:http请求路径,默认为/
- header:请求头
- auth:basic认证,用于headers中的Authorization部分
HTTP代理
HTTP模块的ClientReqeust封装的模块类似于服务端。在keep-live的情况下可复用连接,默认最大连接数是5。封装了一个http.globalAgent进行管理连接池。
如果需要修改限制,则需要声明一个agent配置maxSockets配置进行传递,false表示不进行配置
const agent = http.agent({
maxSockets:20
});
http.request({
agent
});
Agent对象中sockets和request表示当前的连接数和等待连接的请求,可以通过观察这两个值进行判断业务繁忙程度。
HTTP客户端事件
- response:在得到服务端端响应时触发该事件
- socket:当底层的连接池分配连接给当前请求时出发该请求
- connect:当发送connect请求时服务端返回200状态码时触发该事件
- upgrede:当客户端发送upgrade请求时,服务端发送101 Switching Protocols时触发该事件
- continue:当客户端发送expect:100-continue时,服务端返回100 continue触发该事件
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)