http模块概览
大多数nodejs开发者都是冲着开发web server的目的选择了nodejs。正如官网所展示的,借助http模块,可以几行代码就搞定一个超迷你的web server。
在nodejs中,http可以说是最核心的模块,同时也是比较复杂的一个模块。上手很简单,但一旦深入学习,不少初学者就会觉得头疼,不知从何入手。
本文先从一个简单的例子出发,引出http模块最核心的四个实例。看完本文,应该就能够对http模块有个整体的认识。
一个简单的例子
在下面的例子中,我们创建了1个web服务器、1个http客户端
- 服务器server:接收来自客户端的请求,并将客户端请求的地址返回给客户端。
- 客户端client:向服务器发起请求,并将服务器返回的内容打印到控制台。
代码如下所示,只有几行,但包含了不少信息量。下一小节会进行简单介绍。
var http = require('http');
// http server 例子
var server = http.createServer(function(serverReq, serverRes){
var url = serverReq.url;
serverRes.end( '您访问的地址是:' + url );
});
server.listen(3000);
// http client 例子
var client = http.get('http://127.0.0.1:3000', function(clientRes){
clientRes.pipe(process.stdout);
});
例子解释
在上面这个简单的例子里,涉及了4个实例。大部分时候,serverReq、serverRes 才是主角。
- server:http.Server实例,用来提供服务,处理客户端的请求。
- client:http.ClientReques实例,用来向服务端发起请求。
- serverReq/clientRes:其实都是 http.IncomingMessage实。serverReq 用来获取客户端请求的相关信息,如request header;而clientRes用来获取服务端返回的相关信息,比如response header。
- serverRes:http.ServerResponse实例
关于http.IncomingMessage、http.ServerResponse
先讲下 http.ServerResponse 实例。作用很明确,服务端通过http.ServerResponse 实例,来个请求方发送数据。包括发送响应表头,发送响应主体等。
接下来是 http.IncomingMessage 实例,由于在 server、client 都出现了,初学者难免有点迷茫。它的作用是
在server端:获取请求发送方的信息,比如请求方法、路径、传递的数据等。 在client端:获取 server 端发送过来的信息,比如请求方法、路径、传递的数据等。
http.IncomingMessage实例 有三个属性需要注意:method、statusCode、statusMessage。
- method:只在 server 端的实例有(也就是 serverReq.method)
- statusCode/statusMessage:只在 client 端 的实例有(也就是 clientRes.method)
关于继承与扩展
http.Server
- http.Server 继承了 net.Server (于是顺带需要学一下 net.Server 的API、属性、相关事件)
- net.createServer(fn),回调中的
socket是个双工的stream接口,也就是说,读取发送方信息、向发送方发送信息都靠他。
备注:socket的客户端、服务端是相对的概念,所以其实 net.Server 内部也是用了 net.Socket(不负责任猜想)
// 参考:https://cnodejs.org/topic/4fb1c1fd1975fe1e1310490b
var net = require('net');
var PORT = 8989;
var HOST = '127.0.0.1';
var server = net.createServer(function(socket){
console.log('Connected: ' + socket.remoteAddress + ':' + socket.remotePort);
socket.on('data', function(data){
console.log('DATA ' + socket.remoteAddress + ': ' + data);
console.log('Data is: ' + data);
socket.write('Data from you is "' + data + '"');
});
socket.on('close', function(){
console.log('CLOSED: ' +
socket.remoteAddress + ' ' + socket.remotePort);
});
});
server.listen(PORT, HOST);
console.log(server instanceof net.Server); // true
http.ClientRequest
http.ClientRequest 内部创建了一个socket来发起请求,代码如下。
当你调用 http.request(options) 时,内部是这样的
self.onSocket(net.createConnection(options));
http.ServerResponse
- 实现了 Writable Stream interface,内部也是通过socket来发送信息。
http.IncomingMessage
- 实现了 Readable Stream interface,参考这里
- req.socket –> 获得跟这次连接相关的socket
http-req概览
本文的重点会放在req这个对象上。前面已经提到,它其实是http.IncomingMessage实例,在服务端、客户端作用略微有差异
- 服务端处:获取请求方的相关信息,如request header等。
- 客户端处:获取响应方返回的相关信息,如statusCode等。
服务端例子:
// 下面的 req
var http = require('http');
var server = http.createServer(function(req, res){
console.log(req.headers);
res.end('ok');
});
server.listen(3000);
客户端例子
// 下面的res
var http = require('http');
http.get('http://127.0.0.1:3000', function(res){
console.log(res.statusCode);
});
属性/方法/事件 分类
http.IncomingMessage的属性/方法/事件 不是特别多,按照是否客户端/服务端 特有的,下面进行简单归类。可以看到
- 服务端处特有:url
- 客户端处特有:statusCode、statusMessage
| 类型 | 名称 | 服务端 | 客户端 |
|---|---|---|---|
| 事件 | aborted | ✓ | ✓ |
| 事件 | close | ✓ | ✓ |
| 属性 | headers | ✓ | ✓ |
| 属性 | rawHeaders | ✓ | ✓ |
| 属性 | statusCode | ✕ | ✓ |
| 属性 | statusMessage | ✕ | ✓ |
| 属性 | httpVersion | ✓ | ✓ |
| 属性 | url | ✓ | ✕ |
| 属性 | socket | ✓ | ✓ |
| 方法 | .destroy() | ✓ | ✓ |
| 方法 | .setTimeout() | ✓ | ✓ |
服务端的例子
例子一:获取httpVersion/method/url
下面是一个典型的HTTP请求报文,里面最重要的内容包括:HTTP版本、请求方法、请求地址、请求头部。
GET /hello HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Cache-Control: no-cache
那么,如何获取上面提到的信息呢?很简单,直接上代码
// getClientInfo.js
var http = require('http');
var server = http.createServer(function(req, res){
console.log( '1、客户端请求url:' + req.url );
console.log( '2、http版本:' + req.httpVersion );
console.log( '3、http请求方法:' + req.method );
console.log( '4、http请求头部' + JSON.stringify(req.headers) );
res.end('ok');
});
server.listen(3000);
效果如下:
1、客户端请求url:/hello
2、http版本:1.1
3、http请求方法:GET
4、http headers:{"host":"127.0.0.1:3000","connection":"keep-alive","cache-control":"no-cache","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36","postman-token":"1148986a-ddfb-3569-e2c0-585634655fe4","accept":"*/*","accept-encoding":"gzip, deflate, sdch, br","accept-language":"zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4"}
例子二:获取get请求参数
服务端代码如下:
// getClientGetQuery.js
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var server = http.createServer(function(req, res){
var urlObj = url.parse(req.url);
var query = urlObj.query;
var queryObj = querystring.parse(query);
console.log( JSON.stringify(queryObj) );
res.end('ok');
});
server.listen(3000);
访问地址 http://127.0.0.1:3000/hello?nick=chyingp&hello=world
服务端输出如下
{"nick":"chyingp","hello":"world"}
例子三:获取post请求参数
服务端代码如下
// getClientPostBody.js
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var server = http.createServer(function(req, res){
var body = '';
req.on('data', function(thunk){
body += thunk;
});
req.on('end', function(){
console.log( 'post body is: ' + body );
res.end('ok');
});
});
server.listen(3000);
通过curl构造post请求:
curl -d 'nick=casper&hello=world' http://127.0.0.1:3000
服务端打印如下:
post body is: nick=casper&hello=world
备注:post请求中,不同的Content-type,post body有不小差异,感兴趣的同学可以研究下。
本例中的post请求,HTTP报文大概如下
POST / HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
nick=casper&hello=world
客户端处例子
例子一:获取httpVersion/statusCode/statusMessage
代码如下:
var http = require('http');
var server = http.createServer(function(req, res){
res.writeHead(200, {'content-type': 'text/plain'});
res.end('from server');
});
server.listen(3000);
var client = http.get('http://127.0.0.1:3000', function(res){
console.log('1、http版本:' + res.httpVersion);
console.log('2、返回状态码:' + res.statusCode);
console.log('3、返回状态描述信息:' + res.statusMessage);
console.log('4、返回正文:');
res.pipe(process.stdout);
});
控制台输出:
1、http版本:1.1
2、返回状态码:200
3、返回状态描述信息:OK
4、返回正文:
from server
事件对比:aborted、close
官方文档对这两个事件的解释是:当客户端终止请求时,触发aborted事件;当客户端连接断开时,触发close事件;官方文档传送们:地址
解释得比较含糊,从实际实验对比上来看,跟官方文档有不小出入。此外,客户端处、服务端处的表现也是不同的。
服务端表现
根据实际测试结果来看,当客户端:
- abort请求时,服务端req的aborted、close事件都会触发;(诡异)
- 请求正常完成时,服务端req的close事件不会触发;(也很诡异)
直接扒了下nodejs的源代码,发现的确是同时触发的,触发场景:请求正常结束前,客户端abort请求。
测试代码如下:
var http = require('http');
var server = http.createServer(function(req, res){
console.log('1、收到客户端请求: ' + req.url);
req.on('aborted', function(){
console.log('2、客户端请求aborted');
});
req.on('close', function(){
console.log('3、客户端请求close');
});
// res.end('ok'); 故意不返回,等着客户端中断请求
});
server.listen(3000, function(){
var client = http.get('http://127.0.0.1:3000/aborted');
setTimeout(function(){
client.abort(); // 故意延迟100ms,确保请求发出
}, 100);
});
// 输出如下
// 1、收到客户端请求: /aborted
// 2、客户端请求aborted
// 3、客户端请求close
以下代码来自nodejs源码(_http_server.js)
function abortIncoming() {
while (incoming.length) {
var req = incoming.shift();
req.emit('aborted');
req.emit('close');
}
// abort socket._httpMessage ?
}
再来一波对比,req.on('close')和req.socket.on('close')。
// reqSocketClose.js
var http = require('http');
var server = http.createServer(function(req, res){
console.log('server: 收到客户端请求');
req.on('close', function(){
console.log('server: req close');
});
req.socket.on('close', function(){
console.log('server: req.socket close');
});
res.end('ok');
});
server.listen(3000);
var client = http.get('http://127.0.0.1:3000/aborted', function(res){
console.log('client: 收到服务端响应');
});
输出如下,正儿八经的close事件触发了。
server: 收到客户端请求
server: req.socket close
client: 收到服务端响应
客户端表现
猜测客户端的aborted、close也是在类似场景下触发,测试代码如下。发现一个比较有意思的情况,res.pipe(process.stdout) 这行代码是否添加,会影响close是否触发。
- 没有
res.pipe(process.stdout):close不触发。 - 有
res.pipe(process.stdout):close触发。
var http = require('http');
var server = http.createServer(function(req, res){
console.log('1、服务端:收到客户端请求');
res.flushHeaders();
res.setTimeout(100); // 故意不返回,3000ms后超时
});
server.on('error', function(){});
server.listen(3000, function(){
var client = http.get('http://127.0.0.1:3000/aborted', function(res){
console.log('2、客户端:收到服务端响应');
// res.pipe(process.stdout); 注意这行代码
res.on('aborted', function(){
console.log('3、客户端:aborted触发!');
});
res.on('close', function(){
console.log('4、客户端:close触发!');
});
});
});
信息量略大的 .destroy()
经过前面aborted、close的摧残,本能的觉得 .destroy() 方法的表现会有很多惊喜之处。
测试代码如下:
var http = require('http');
var server = http.createServer(function(req, res){
console.log('服务端:收到客户端请求');
req.destroy(new Error('测试destroy'));
req.on('error', function(error){
console.log('服务端:req error: ' + error.message);
});
req.socket.on('error', function(error){
console.log('服务端:req socket error: ' + error.message);
})
});
server.on('error', function(error){
console.log('服务端:server error: ' + error.message);
});
server.listen(3000, function(){
var client = http.get('http://127.0.0.1:3000/aborted', function(res){
// do nothing
});
client.on('error', function(error){
console.log('客户端:client error触发!' + error.message);
});
});
输出如下。根据 .destroy() 调用的时机不同,error 触发的对象不同。(测试过程比较枯燥,有时间再总结一下)
服务端:收到客户端请求
服务端:req socket error: 测试destroy
客户端:client error触发!socket hang up
不常用属性
- rawHeaders:未解析前的request header。
- trailers:在分块传输编码(chunk)中用到,表示trailer后的header可分块传输。(感兴趣的可以研究下)
- rawTrailers:
关于trailers属性:
The request/response trailers object. Only populated at the ‘end’ event.
写在后面
一个貌似很简单的对象,实际比想的要复杂一些。做了不少对比实验,也发现了一些好玩的东西,打算深入学习的同学可以自己多动手尝试一下 :)
TODO:
- 对close、aborted进行更深入对比
- 对.destroy()进行更深入对比
相关链接
官方文档: https://nodejs.org/api/http.html#http_class_http_incomingmessage
http-res概览
http模块四剑客之一的res,应该都不陌生了。一个web服务程序,接受到来自客户端的http请求后,向客户端返回正确的响应内容,这就是res的职责。
返回的内容包括:状态代码/状态描述信息、响应头部、响应主体。下文会举几个简单的例子。
var http = require('http');
var server = http.createServer(function(req, res){
res.end('ok');
});
server.listen(3000);
例子
在下面的例子中,我们同时设置了 状态代码/状态描述信息、响应头部、响应主体,就是这么简单。
var http = require('http');
// 设置状态码、状态描述信息、响应主体
var server = http.createServer(function(req, res){
res.writeHead(200, 'ok', {
'Content-Type': 'text/plain'
});
res.end('hello');
});
server.listen(3000);
设置状态代码、状态描述信息
res提供了 res.writeHead()、res.statusCode/res.statusMessage 来实现这个目的。
举例,如果想要设置 200/ok ,可以
res.writeHead(200, 'ok');
也可以
res.statusCode = 200;
res.statusMessage = 'ok';
两者差不多,差异点在于
- res.writeHead() 可以提供额外的功能,比如设置响应头部。
- 当响应头部发送出去后,res.statusCode/res.statusMessage 会被设置成已发送出去的 状态代码/状态描述信息。
设置响应头部
res提供了 res.writeHead()、response.setHeader() 来实现响应头部的设置。
举例,比如想把 Content-Type 设置为 text-plain,那么可以
// 方法一
res.writeHead(200, 'ok', {
'Content-Type': 'text-plain'
});
// 方法二
res.setHeader('Content-Type', 'text-plain');
两者的差异点在哪里呢?
- res.writeHead() 不单单是设置header。
- 已经通过 res.setHeader() 设置了header,当通过 res.writeHead() 设置同名header,res.writeHead() 的设置会覆盖之前的设置。
关于第2点差异,这里举个例子。下面代码,最终的 Content-Type 为 text/plain。
var http = require('http');
var server = http.createServer(function(req, res){
res.setHeader('Content-Type', 'text/html');
res.writeHead(200, 'ok', {
'Content-Type': 'text/plain'
});
res.end('hello');
});
server.listen(3000);
而下面的例子,则直接报错。报错信息为 Error: Can't set headers after they are sent.。
var http = require('http');
var server = http.createServer(function(req, res){
res.writeHead(200, 'ok', {
'Content-Type': 'text/plain'
});
res.setHeader('Content-Type', 'text/html');
res.end('hello');
});
server.listen(3000);
其他响应头部操作
增、删、改、查 是配套的。下面分别举例说明下,例子太简单就直接上代码了。
// 增
res.setHeader('Content-Type', 'text/plain');
// 删
res.removeHeader('Content-Type');
// 改
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Type', 'text/html'); // 覆盖
// 查
res.getHeader('content-type');
其中略显不同的是 res.getHeader(name),name 用的是小写,返回值没做特殊处理。
res.setHeader('Content-Type', 'TEXT/HTML');
console.log( res.getHeader('content-type') ); // TEXT/HTML
res.setHeader('Content-Type', 'text/plain');
console.log( res.getHeader('content-type') ); // text/plain
此外,还有不那么常用的:
- res.headersSent:header是否已经发送;
- res.sendDate:默认为true。但为true时,会在response header里自动设置Date首部。
设置响应主体
主要用到 res.write() 以及 res.end() 两个方法。
res.write() API的信息量略大,建议看下官方文档。
response.write(chunk[, encoding][, callback])
- chunk:响应主体的内容,可以是string,也可以是buffer。当为string时,encoding参数用来指明编码方式。(默认是utf8)
- encoding:编码方式,默认是 utf8。
- callback:当响应体flushed时触发。(TODO 这里想下更好的解释。。。)
使用上没什么难度,只是有些注意事项:
- 如果 res.write() 被调用时, res.writeHead() 还没被调用过,那么,就会把header flush出去。
- res.write() 可以被调用多次。
- 当 res.write(chunk) 第一次被调用时,node 会将 header 信息 以及 chunk 发送到客户端。第二次调用 res.write(chunk) ,node 会认为你是要streaming data(WTF,该怎么翻译)。。。
Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory. ‘drain’ will be emitted when the buffer is free again.
response.end([data][, encoding][, callback])
掌握了 res.write() 的话,res.end() 就很简单了。res.end() 的用处是告诉nodejs,header、body都给你了,这次响应就到这里吧。
有点像个语法糖,可以看成下面两个调用的组合。至于callback,当响应传递结束后触发。
res.write(data, encoding);
res.end()
chunk数据
参考这里:http://stackoverflow.com/questions/6258210/how-can-i-output-data-before-i-end-the-response
也就是说,除了nodejs的特性,还需要了解 HTTP协议、浏览器的具体实现。(细思极恐)
如果是 text/html
var http = require('http');
http.createServer(function(req, res) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.write('hello');
setTimeout(function() {
res.write(' world!');
res.end();
}, 2000);
}).listen(3000);
如果是 text/plain
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'X-Content-Type-Options': 'nosniff'
});
res.write('hello');
setTimeout(function(){
res.write('world');
res.end()
}, 2000);
}).listen(3000);
失败例子
var http = require('http');
var server = http.createServer(function(req, res){
res.writeHead(200, 'ok', {
'Content-Type': 'text/html'
});
res.write('hello');
setTimeout(function(){
res.write('world');
res.end();
}, 2000);
});
server.listen(3000);
超时处理
接口:response.setTimeout(msecs, callback)
关于 timeout 事件的说明,同样是言简意赅(WTF),话少信息量大,最好来个demo TODO
If no ‘timeout’ listener is added to the request, the response, or the server, then sockets are destroyed when they time out. If you assign a handler on the request, the response, or the server’s ‘timeout’ events, then it is your responsibility to handle timed out sockets.
事件 close/finish
- close:response.end() 被调用前,连接就断开了。此时会触发这个事件。
- finish:响应header、body都已经发送出去(交给操作系统,排队等候传输),但客户端是否实际收到数据为止。(这个事件后,res 上就不会再有其他事件触发)
其他不常用属性/方法
- response.finished:一开始是false,响应结束后,设置为true。
- response.sendDate:默认是true。是否自动设置Date头部。(按HTTP协议是必须要的,除非是调试用,不然不要设置为false)
- response.headersSent:只读属性。响应头部是否已发送。
- response.writeContinue():发送 HTTP/1.1 100 Continue 消息给客户端,提示说服务端愿意接受客户端的请求,请继续发送请求正文(body)。(TODO 做个demo啥的是大大的好)
相关链接
How can I output data before I end the response? http://stackoverflow.com/questions/6258210/how-can-i-output-data-before-i-end-the-response
8.2.3 Use of the 100 (Continue) Status http://greenbytes.de/tech/webdav/rfc2616.html#use.of.the.100.status
ClientRequest概览
当你调用 http.request(options) 时,会返回 ClientRequest实例,主要用来创建HTTP客户端请求。
在前面的章节里,已经对http模块的的其他方面进行了不少介绍,如http.Server、http.ServerResponse、http.IncomingMessage。
有了前面的基础,详细本文不难理解,本文更多的以例子为主。
简单的GET请求
下面构造了个GET请求,访问 http://id.qq.com/ ,并将返回的网页内容打印在控制台下。
var http = require('http');
var options = {
protocol: 'http:',
hostname: 'id.qq.com',
port: '80',
path: '/',
method: 'GET'
};
var client = http.request(options, function(res){
var data = '';
res.setEncoding('utf8');
res.on('data', function(chunk){
data += chunk;
});
res.on('end', function(){
console.log(data);
});
});
client.end();
当然,也可以用便捷方法 http.get(options) 进行重写
var http = require('http');
http.get('http://id.qq.com/', function(res){
var data = '';
res.setEncoding('utf8');
res.on('data', function(chunk){
data += chunk;
});
res.on('end', function(){
console.log(data);
});
});
简单的post请求
在下面例子中,首先创建了个http server,负责将客户端发送过来的数据回传。
接着,创建客户端POST请求,向服务端发送数据。需要注意的点有:
- method 指定为 POST。
- headers 里声明了 content-type 为 application/x-www-form-urlencoded。
- 数据发送前,用 querystring.stringify(obj) 对传输的对象进行了格式化。
var http = require('http');
var querystring = require('querystring');
var createClientPostRequest = function(){
var options = {
method: 'POST',
protocol: 'http:',
hostname: '127.0.0.1',
port: '3000',
path: '/post',
headers: {
"connection": "keep-alive",
"content-type": "application/x-www-form-urlencoded"
}
};
// 发送给服务端的数据
var postBody = {
nick: 'chyingp'
};
// 创建客户端请求
var client = http.request(options, function(res){
// 最终输出:Server got client data: nick=chyingp
res.pipe(process.stdout);
});
// 发送的报文主体,记得先用 querystring.stringify() 处理下
client.write( querystring.stringify(postBody) );
client.end();
};
// 服务端程序,只是负责回传客户端数据
var server = http.createServer(function(req, res){
res.write('Server got client data: ');
req.pipe(res);
});
server.listen(3000, createClientPostRequest);
各种事件
在官方文档里,http.RequestClient相关的事件共有7个。跟HTTP协议密切相关的有3个,分别是 connect、continue、upgrade,其他4个分别是 abort、aborted、socket、response。
- 其他:abort、aborted、socket、response
- 与HTTP协议相关:connect、continue、upgrade
跟HTTP协议相关的会相对复杂些,因为涉及HTTP协议的设计细节。其他3个相对简单。下面分别进行简单的介绍。
response事件
最容易理解的一个,当收到来自服务端的响应时触发,其实跟 http.get(url, cbk) 中的回调是一样的,看下程序运行的打印信息就知道。
var http = require('http');
var url = 'http://id.qq.com/';
var client = http.get(url, function(res){
console.log('1. response event');
});
client.on('response', function(res){
console.log('2. response event');
});
client.end();
打印信息:
1. response event
2. response event
socket事件
当给client分配socket的时候触发,如果熟悉net模块对这个事件应该不陌生。大部分时候并不需要关注这个事件,虽然内部其实挺复杂的。
abort/aborted 事件
这两个事件看着非常像,都是请求中断时触发,差异在于中断的发起方:
- abort:客户端主动中断请求(第一次调用 client.abort() 时触发)
- aborted:服务端主动中断请求,且请求已经中断时触发。
continue事件
当收到服务端的响应 100 Continue 时触发。熟悉HTTP协议的同学应该对 100 Continue 有所了解。当客户端向服务端发送首部 Expect: 100-continue ,服务端经过一定的校验后,决定对客户端的后续请求放行,于是返回返回 100 Continue,知会客户端,可以继续发送数据。(request body)
upgrade事件
同样是跟HTTP协议密切相关。当客户端向客户端发起请求时,可以在请求首部里声明 'Connection': 'Upgrade' ,以此要求服务端,将当前连接升级到新的协议。如果服务器同意,那么就升级协议继续通信。这里不打算展开太多细节,直接上官方文档的代码
const http = require('http');
// Create an HTTP server
var srv = http.createServer( (req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay');
});
srv.on('upgrade', (req, socket, head) => {
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');
socket.pipe(socket); // echo back
});
// now that server is running
srv.listen(1337, '127.0.0.1', () => {
// make a request
var options = {
port: 1337,
hostname: '127.0.0.1',
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket'
}
};
var req = http.request(options);
req.end();
req.on('upgrade', (res, socket, upgradeHead) => {
console.log('got upgraded!');
socket.end();
process.exit(0);
});
});
其他
除了上面讲解到的属性、方法、事件外,还有下面方法没有讲到。并不是它们不重要,篇幅有限,后面再展开。
- client.abort():中断请求;
- client.setTimeout(timeout):请求超时设置;
- client.flushHeaders() 及早将请求首部发送出去;
- client.setSocketKeepAlive():当内部分配 socket 并连接上时,就会内部调用 socket.keepAlive();
- client.setNoDelay([noDelay]):当内部分配 socket 并连接上时,就会内部调用 socket.setNoDelay();
参考链接
upgrade机制: https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism
官方文档: https://nodejs.org/api/http.html#http_class_http_clientrequest
nodejs源码: https://github.com/nodejs/node/blob/master/lib/_http_client.js
ClientRequest概览
当你调用 http.request(options) 时,会返回 ClientRequest实例,主要用来创建HTTP客户端请求。
在前面的章节里,已经对http模块的的其他方面进行了不少介绍,如http.Server、http.ServerResponse、http.IncomingMessage。
有了前面的基础,详细本文不难理解,本文更多的以例子为主。
简单的GET请求
下面构造了个GET请求,访问 http://id.qq.com/ ,并将返回的网页内容打印在控制台下。
var http = require('http');
var options = {
protocol: 'http:',
hostname: 'id.qq.com',
port: '80',
path: '/',
method: 'GET'
};
var client = http.request(options, function(res){
var data = '';
res.setEncoding('utf8');
res.on('data', function(chunk){
data += chunk;
});
res.on('end', function(){
console.log(data);
});
});
client.end();
当然,也可以用便捷方法 http.get(options) 进行重写
var http = require('http');
http.get('http://id.qq.com/', function(res){
var data = '';
res.setEncoding('utf8');
res.on('data', function(chunk){
data += chunk;
});
res.on('end', function(){
console.log(data);
});
});
简单的post请求
在下面例子中,首先创建了个http server,负责将客户端发送过来的数据回传。
接着,创建客户端POST请求,向服务端发送数据。需要注意的点有:
- method 指定为 POST。
- headers 里声明了 content-type 为 application/x-www-form-urlencoded。
- 数据发送前,用 querystring.stringify(obj) 对传输的对象进行了格式化。
var http = require('http');
var querystring = require('querystring');
var createClientPostRequest = function(){
var options = {
method: 'POST',
protocol: 'http:',
hostname: '127.0.0.1',
port: '3000',
path: '/post',
headers: {
"connection": "keep-alive",
"content-type": "application/x-www-form-urlencoded"
}
};
// 发送给服务端的数据
var postBody = {
nick: 'chyingp'
};
// 创建客户端请求
var client = http.request(options, function(res){
// 最终输出:Server got client data: nick=chyingp
res.pipe(process.stdout);
});
// 发送的报文主体,记得先用 querystring.stringify() 处理下
client.write( querystring.stringify(postBody) );
client.end();
};
// 服务端程序,只是负责回传客户端数据
var server = http.createServer(function(req, res){
res.write('Server got client data: ');
req.pipe(res);
});
server.listen(3000, createClientPostRequest);
各种事件
在官方文档里,http.RequestClient相关的事件共有7个。跟HTTP协议密切相关的有3个,分别是 connect、continue、upgrade,其他4个分别是 abort、aborted、socket、response。
- 其他:abort、aborted、socket、response
- 与HTTP协议相关:connect、continue、upgrade
跟HTTP协议相关的会相对复杂些,因为涉及HTTP协议的设计细节。其他3个相对简单。下面分别进行简单的介绍。
response事件
最容易理解的一个,当收到来自服务端的响应时触发,其实跟 http.get(url, cbk) 中的回调是一样的,看下程序运行的打印信息就知道。
var http = require('http');
var url = 'http://id.qq.com/';
var client = http.get(url, function(res){
console.log('1. response event');
});
client.on('response', function(res){
console.log('2. response event');
});
client.end();
打印信息:
1. response event
2. response event
socket事件
当给client分配socket的时候触发,如果熟悉net模块对这个事件应该不陌生。大部分时候并不需要关注这个事件,虽然内部其实挺复杂的。
abort/aborted 事件
这两个事件看着非常像,都是请求中断时触发,差异在于中断的发起方:
- abort:客户端主动中断请求(第一次调用 client.abort() 时触发)
- aborted:服务端主动中断请求,且请求已经中断时触发。
continue事件
当收到服务端的响应 100 Continue 时触发。熟悉HTTP协议的同学应该对 100 Continue 有所了解。当客户端向服务端发送首部 Expect: 100-continue ,服务端经过一定的校验后,决定对客户端的后续请求放行,于是返回返回 100 Continue,知会客户端,可以继续发送数据。(request body)
upgrade事件
同样是跟HTTP协议密切相关。当客户端向客户端发起请求时,可以在请求首部里声明 'Connection': 'Upgrade' ,以此要求服务端,将当前连接升级到新的协议。如果服务器同意,那么就升级协议继续通信。这里不打算展开太多细节,直接上官方文档的代码
const http = require('http');
// Create an HTTP server
var srv = http.createServer( (req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('okay');
});
srv.on('upgrade', (req, socket, head) => {
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');
socket.pipe(socket); // echo back
});
// now that server is running
srv.listen(1337, '127.0.0.1', () => {
// make a request
var options = {
port: 1337,
hostname: '127.0.0.1',
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket'
}
};
var req = http.request(options);
req.end();
req.on('upgrade', (res, socket, upgradeHead) => {
console.log('got upgraded!');
socket.end();
process.exit(0);
});
});
其他
除了上面讲解到的属性、方法、事件外,还有下面方法没有讲到。并不是它们不重要,篇幅有限,后面再展开。
- client.abort():中断请求;
- client.setTimeout(timeout):请求超时设置;
- client.flushHeaders() 及早将请求首部发送出去;
- client.setSocketKeepAlive():当内部分配 socket 并连接上时,就会内部调用 socket.keepAlive();
- client.setNoDelay([noDelay]):当内部分配 socket 并连接上时,就会内部调用 socket.setNoDelay();
参考链接
upgrade机制: https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism
官方文档: https://nodejs.org/api/http.html#http_class_http_clientrequest
nodejs源码: https://github.com/nodejs/node/blob/master/lib/_http_client.js
http服务端概览
创建server
几行代码搞定
var http = require('http');
var requestListener = function(req, res){
res.end('ok');
};
var server = http.createServer(requestListener);
// var server = new http.Server(requestListener); 跟上面是等价的
server.listen(3000);
获取请求方信息
HTTP版本、HTTP method、headers、url
var http = require('http');
var server = http.createServer(function(req, res){
console.log('客户端请求url:' + req.url);
console.log('http版本:' + req.httpVersion);
console.log('http请求方法:' + req.method);
res.end('ok');
});
server.listen(3000);
效果如下:
客户端请求url:/hello
http版本:1.1
http请求方法:GET
http headers:{"host":"127.0.0.1:3000","connection":"keep-alive","cache-control":"max-age=0","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","accept-encoding":"gzip, deflate, sdch, br","accept-language":"zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4"}
获取get请求参数
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var server = http.createServer(function(req, res){
var urlObj = url.parse(req.url);
var query = urlObj.query;
var queryObj = querystring.parse(query);
console.log( JSON.stringify(queryObj) );
res.end('ok');
});
server.listen(3000);
运行如下命令
curl http://127.0.0.1:3000/hello\?nick\=chyingp\&hello\=world
服务端输出如下
{"nick":"chyingp","hello":"world"}
获取post请求参数
代码如下
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var server = http.createServer(function(req, res){
var body = '';
req.on('data', function(thunk){
body += thunk;
});
req.on('end', function(){
console.log( 'post body is: ' + body );
res.end('ok');
});
});
server.listen(3000);
通过curl构造极简post请求
curl -d 'nick=casper&hello=world' http://127.0.0.1:3000
服务端打印如下。注意,在post请求中,不同的Content-type,post body有不小差异,感兴趣的同学可以自己试下。
post body is: nick=casper&hello=world
比如本例中的post请求,HTTP报文大概如下
POST / HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
nick=casper&hello=world
枯燥的事件
首先,我们来看下有哪些事件
checkContinue、checkExpectation、clientError、close、connect、connection、request、upgrade
error
var http = require('http');
var PORT = 3000;
var noop = function(){};
var svr = http.createServer(noop);
var anotherSvr = http.createServer(noop);
anotherSvr.on('error', function(e){
console.error('出错啦!' + e.message);
});
svr.listen(PORT, function(){
anotherSvr.listen(PORT);
});
运行代码,输出如下
出错啦!listen EADDRINUSE :::3000
connect vs connection
两者差别非常大,虽然字眼看着有点像。
- connect:当客户端的HTTP method为connect时触发。
- connection:当TCP连接建立时触发,大部分时候可以忽略这个事件(目测模块内部自己用到而已)。此外,可以通过 req.connection 来获取这个socket(从nodejs源码来看,req.socket、req.connection 都指向了这个socket)。此外,socket上的readable事件不会触发(具体原因请看模块内部实现,反正我是还没研究)
大部分时候都不会用到,除非你要开发HTTP代理。当客户端发起 connect 请求时触发(注意绕过了 requestListener)
var http = require('http');
var PORT = 3000;
var server = http.createServer(function(req, res){
res.end('ok');
});
// 注意:发起connect请求的例子在 ./httpServerEventConnectClient.js 里
server.on('connect', function(req, socket, head){
console.log('connect事件触发');
socket.end(); // 反正我就只想举个例子,没打算正经处理。。。
});
server.listen(PORT);
request
当有新的连接到来时触发。那跟 connection 有什么区别呢?
好了,keep-alive闪亮登场!在持久化连接的情况下,多个 request 可能对应的是 一个 connection。
先来看下没有keep-alive的场景
var http = require('http');
var PORT = 3000;
var requestIndex = 0;
var connectionIndex = 0;
var server = http.createServer(function(req, res){
res.end('ok');
});
server.on('request', function(req, res){
requestIndex++;
console.log('request event: 第'+ requestIndex +'个请求!');
});
server.on('connection', function(req, res){
connectionIndex++;
console.log('connection event: 第'+ connectionIndex +'个请求!');
});
server.listen(PORT);
通过curl连续发送3个请求,看下效果
for i in `seq 1 3`; do curl http://127.0.0.1:3000; done
服务端输出如下
connection event: 第1个请求!
request event: 第1个请求!
connection event: 第2个请求!
request event: 第2个请求!
connection event: 第3个请求!
request event: 第3个请求!
然后,再来看下有keep-alive的场景。用 postman 构造包含 keep-alive 的请求,最终的HTTP请求报文如下
GET / HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive
Cache-Control: no-cache
Postman-Token: 6027fda7-f936-d3ac-e54f-dafcbf5e58ff
连续发送3个请求,服务端打印日志如下
connection event: 第1个请求!
request event: 第1个请求!
request event: 第2个请求!
request event: 第3个请求!
不常用接口
server.close([callback]);
关闭服务器。其实就是 (new net.Server()).close(),停止接受新的连接。 已经连接上的请求会继续处理,当所有连接结束的时候,server 正式关闭,并抛出 close 事件。 一般提供了callback,就不用监听close; 监听了close,就不用添加callback。
其他server.listen()
其实除了 server.listen(PORT) 这种监听方式外,还有以下几种相对不那么常用的监听方式。用到的时候看看文档就行了。
server.listen(handle[, callback]):监听本地文件描述符(fd)(windows不支持),或者server,或者socket server.listen(path[, callback]):监听本地socket,创建一个 UNIX socket server 。 server.listen([port][, hostname][, backlog][, callback])
网络超时 server.setTimeout(msecs, callback)
设置网络连接的超时时间。当超过 msecs 没有响应时,网络就会自动断开。
如果传了 callback,那么当 timeout 发生时,就会将timeout的socket作为参数传给callback。
注意,一般情况下超时的socket会自动销毁。但当你传了callback后,你就需要手动end或者destroy这个socket。
不常用属性
server.listening:是否在监听连接 server.timeout:设置超时时间(毫秒),注意,修改这个值,只会对新建立的连接产生影响。此外,将timeout设置为0,就会禁用自动超时行为。(目测不推荐) server.maxHeadersCount:客户端最多传送的header数量,默认是1000,如果设置为0,则没有限制。(问题:如果超过1000怎么办??)
https模块概览
这个模块的重要性,基本不用强调了。在网络安全问题日益严峻的今天,网站采用HTTPS是个必然的趋势。
在nodejs中,提供了 https 这个模块来完成 HTTPS 相关功能。从官方文档来看,跟 http 模块用法非常相似。
本文主要包含两部分:
- 通过客户端、服务端的例子,对https模块进行入门讲解。
- 如何访问安全证书不受信任的网站。(以 12306 为例子)
篇幅所限,本文无法对 HTTPS协议 及 相关技术体系 做过多讲解,有问题欢迎留言交流。
客户端例子
跟http模块的用法非常像,只不过请求的地址是https协议的而已,代码如下:
var https = require('https');
https.get('https://www.baidu.com', function(res){
console.log('status code: ' + res.statusCode);
console.log('headers: ' + JSON.stringify(res.headers));
res.on('data', function(data){
process.stdout.write(data);
});
}).on('error', function(err){
console.error(err);
});
服务端例子
对外提供HTTPS服务,需要有HTTPS证书。如果你已经有了HTTPS证书,那么可以跳过证书生成的环节。如果没有,可以参考如下步骤
生成证书
1、创建个目录存放证书。
mkdir cert
cd cert
2、生成私钥。
openssl genrsa -out chyingp-key.pem 2048
3、生成证书签名请求(csr是 Certificate Signing Request的意思)。
openssl req -new \
-sha256
-key chyingp-key.key.pem \
-out chyingp-csr.pem \
-subj "/C=CN/ST=Guandong/L=Shenzhen/O=YH Inc/CN=www.chyingp.com"
4、生成证书。
openssl x509 \
-req -in chyingp-csr.pem \
-signkey chyingp-key.pem \
-out chyingp-cert.pem
HTTPS服务端
代码如下:
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./cert/chyingp-key.pem'), // 私钥
cert: fs.readFileSync('./cert/chyingp-cert.pem') // 证书
};
var server = https.createServer(options, function(req, res){
res.end('这是来自HTTPS服务器的返回');
});
server.listen(3000);
由于我并没有 www.chyingp.com 这个域名,于是先配置本地host
127.0.0.1 www.chyingp.com
启动服务,并在浏览器里访问 http://www.chyingp.com:3000。注意,浏览器会提示你证书不可靠,点击 信任并继续访问 就行了。
进阶例子:访问安全证书不受信任的网站
这里以我们最喜爱的12306最为例子。当我们通过浏览器,访问12306的购票页面 https://kyfw.12306.cn/otn/regist/init 时,chrome会阻止我们访问,这是因为,12306的证书是自己颁发的,chrome无法确认他的安全性。
对这种情况,可以有如下处理方式:
- 停止访问:着急抢票回家过年的老乡表示无法接受。
- 无视安全警告,继续访问:大部分情况下,浏览器是会放行的,不过安全提示还在。
- 导入12306的CA根证书:浏览器乖乖就范,认为访问是安全的。(实际上还是有安全提示,因为12306用的签名算法安全级别不够)
例子:触发安全限制
同样的,通过 node https client 发起请求,也会遇到同样问题。我们做下实验,代码如下:
var https = require('https');
https.get('https://kyfw.12306.cn/otn/regist/init', function(res){
res.on('data', function(data){
process.stdout.write(data);
});
}).on('error', function(err){
console.error(err);
});
运行上面代码,得到下面的错误提示,意思是 安全证书不可靠,拒绝继续访问。
{ Error: self signed certificate in certificate chain
at Error (native)
at TLSSocket.<anonymous> (_tls_wrap.js:1055:38)
at emitNone (events.js:86:13)
at TLSSocket.emit (events.js:185:7)
at TLSSocket._finishInit (_tls_wrap.js:580:8)
at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:412:38) code: 'SELF_SIGNED_CERT_IN_CHAIN' }
ps:个人认为这里的错误提示有点误导人,12306网站的证书并不是自签名的,只是对证书签名的CA是12306自家的,不在可信列表里而已。自签名证书,跟自己CA签名的证书还是不一样的。
类似在浏览器里访问,我们可以采取如下处理:
- 不建议:忽略安全警告,继续访问;
- 建议:将12306的CA加入受信列表;
方法1:忽略安全警告,继续访问
非常简单,将 rejectUnauthorized 设置为 false 就行,再次运行代码,就可以愉快的返回页面了。
// 例子:忽略安全警告
var https = require('https');
var options = {
hostname: 'kyfw.12306.cn',
path: '/otn/leftTicket/init',
rejectUnauthorized: false // 忽略安全警告
};
var req = https.get(options, function(res){
res.pipe(process.stdout);
});
req.on('error', function(err){
console.error(err.code);
});
方法2:将12306的CA加入受信列表
这里包含3个步骤:
- 下载 12306 的CA证书
- 将der格式的CA证书,转成pem格式
- 修改node https的配置
1、下载 12306 的CA证书
在12306的官网上,提供了CA证书的下载地址,将它保存到本地,命名为 srca.cer。
2、将der格式的CA证书,转成pem格式
https初始化client时,提供了 ca 这个配置项,可以将 12306 的CA证书添加进去。当你访问 12306 的网站时,client就会用ca配置项里的 ca 证书,对当前的证书进行校验,于是就校验通过了。
需要注意的是,ca 配置项只支持 pem 格式,而从12306官网下载的是der格式的。需要转换下格式才能用。关于 pem、der的区别,可参考 这里。
openssl x509 -in srca.cer -inform der -outform pem -out srca.cer.pem
3、修改node https的配置
修改后的代码如下,现在可以愉快的访问12306了。
// 例子:将12306的CA证书,加入我们的信任列表里
var https = require('https');
var fs = require('fs');
var ca = fs.readFileSync('./srca.cer.pem');
var options = {
hostname: 'kyfw.12306.cn',
path: '/otn/leftTicket/init',
ca: [ ca ]
};
var req = https.get(options, function(res){
res.pipe(process.stdout);
});
req.on('error', function(err){
console.error(err.code);
});
相关链接
Why is my node.js SSL connection failing to connect?
DER vs. CRT vs. CER vs. PEM Certificates and How To Convert Them
Painless Self Signed Certificates in node.js
自签名证书和私有CA签名的证书的区别 创建自签名证书 创建私有CA 证书类型 证书扩展名
那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)
网络TCP-net模块概览
net模块是同样是nodejs的核心模块。在http模块概览里提到,http.Server继承了net.Server,此外,http客户端与http服务端的通信均依赖于socket(net.Socket)。也就是说,做node服务端编程,net基本是绕不开的一个模块。
从组成来看,net模块主要包含两部分,了解socket编程的同学应该比较熟悉了:
- net.Server:TCP server,内部通过socket来实现与客户端的通信。
- net.Socket:tcp/本地 socket的node版实现,它实现了全双工的stream接口。
本文从一个简单的 tcp服务端/客户端 的例子开始讲解,好让读者有个概要的认识。接着再分别介绍 net.Server、net.Socket 比较重要的API、属性、事件。
对于初学者,建议把文中的例子本地跑一遍加深理解。
简单的 server+client 例子
tcp服务端程序如下:
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp服务端
var server = net.createServer(function(socket){
console.log('服务端:收到来自客户端的请求');
socket.on('data', function(data){
console.log('服务端:收到客户端数据,内容为{'+ data +'}');
// 给客户端返回数据
socket.write('你好,我是服务端');
});
socket.on('close', function(){
console.log('服务端:客户端连接断开');
});
});
server.listen(PORT, HOST, function(){
console.log('服务端:开始监听来自客户端的请求');
});
tcp客户端如下:
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp客户端
var client = net.createConnection(PORT, HOST);
client.on('connect', function(){
console.log('客户端:已经与服务端建立连接');
});
client.on('data', function(data){
console.log('客户端:收到服务端数据,内容为{'+ data +'}');
});
client.on('close', function(data){
console.log('客户端:连接断开');
});
client.end('你好,我是客户端');
运行服务端、客户端代码,控制台分别输出如下:
服务端:
服务端:开始监听来自客户端的请求
服务端:收到来自客户端的请求
服务端:收到客户端数据,内容为{你好,我是客户端}
服务端:客户端连接断开
客户端:
客户端:已经与服务端建立连接
客户端:收到服务端数据,内容为{你好,我是服务端}
客户端:连接断开
服务端 net.Server
server.address()
返回服务端的地址信息,比如绑定的ip地址、端口等。
console.log( server.address() );
// 输出如下 { port: 3000, family: 'IPv4', address: '127.0.0.1' }
server.close(callback])
关闭服务器,停止接收新的客户端请求。有几点注意事项:
- 对正在处理中的客户端请求,服务器会等待它们处理完(或超时),然后再正式关闭。
- 正常关闭的同时,callback 会被执行,同时会触发 close 事件。
- 异常关闭的同时,callback 也会执行,同时将对应的 error 作为参数传入。(比如还没调用 server.listen(port) 之前,就调用了server.close())
下面会通过两个具体的例子进行对比,先把结论列出来
- 已调用server.listen():正常关闭,close事件触发,然后callback执行,error参数为undefined
- 未调用server.listen():异常关闭,close事件触发,然后callback执行,error为具体的错误信息。(注意,error 事件没有触发)
例子1:服务端正常关闭
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
var noop = function(){};
// tcp服务端
var server = net.createServer(noop);
server.listen(PORT, HOST, function(){
server.close(function(error){
if(error){
console.log( 'close回调:服务端异常:' + error.message );
}else{
console.log( 'close回调:服务端正常关闭' );
}
});
});
server.on('close', function(){
console.log( 'close事件:服务端关闭' );
});
server.on('error', function(error){
console.log( 'error事件:服务端异常:' + error.message );
});
输出为:
close事件:服务端关闭
close回调:服务端正常关闭
例子2:服务端异常关闭
代码如下
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
var noop = function(){};
// tcp服务端
var server = net.createServer(noop);
// 没有正式启动请求监听
// server.listen(PORT, HOST);
server.on('close', function(){
console.log( 'close事件:服务端关闭' );
});
server.on('error', function(error){
console.log( 'error事件:服务端异常:' + error.message );
});
server.close(function(error){
if(error){
console.log( 'close回调:服务端异常:' + error.message );
}else{
console.log( 'close回调:服务端正常关闭' );
}
});
输出为:
close事件:服务端关闭
close回调:服务端异常:Not running
server.ref()/server.unref()
了解node事件循环的同学对这两个API应该不陌生,主要用于将server 加入事件循环/从事件循环里面剔除,影响就在于会不会影响进程的退出。
对出学习net的同学来说,并不需要特别关注,感兴趣的自己做下实验就好。
事件 listening/connection/close/error
- listening:调用 server.listen(),正式开始监听请求的时候触发。
- connection:当有新的请求进来时触发,参数为请求相关的 socket。
- close:服务端关闭的时候触发。
- error:服务出错的时候触发,比如监听了已经被占用的端口。
几个事件都比较简单,这里仅举个 connection 的例子。
从测试结果可以看出,有新的客户端连接产生时,net.createServer(callback) 中的callback回调 会被调用,同时 connection 事件注册的回调函数也会被调用。
事实上,net.createServer(callback) 中的 callback 在node内部实现中 也是加入了做为 connection事件 的监听函数。感兴趣的可以看下node的源码。
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
var noop = function(){};
// tcp服务端
var server = net.createServer(function(socket){
socket.write('1. connection 触发\n');
});
server.on('connection', function(socket){
socket.end('2. connection 触发\n');
});
server.listen(PORT, HOST);
通过下面命令测试下效果
curl http://127.0.0.1:3000
输出:
1. connection 触发
2. connection 触发
客户端 net.Socket
在文章开头已经举过客户端的例子,这里再把例子贴一下。(备注:严格来说不应该把 net.Socket 叫做客户端,这里方便讲解而已)
单从node官方文档来看的话,感觉 net.Socket 比 net.Server 要复杂很多,有更多的API、事件、属性。但实际上,把 net.Socket 相关的API、事件、属性 进行归类下,会发现,其实也不是特别复杂。
具体请看下一小节内容。
var net = require('net');
var PORT = 3000;
var HOST = '127.0.0.1';
// tcp客户端
var client = net.createConnection(PORT, HOST);
client.on('connect', function(){
console.log('客户端:已经与服务端建立连接');
});
client.on('data', function(data){
console.log('客户端:收到服务端数据,内容为{'+ data +'}');
});
client.on('close', function(data){
console.log('客户端:连接断开');
});
client.end('你好,我是客户端');
API、属性归类
以下对net.Socket的API跟属性,按照用途进行了大致的分类,方便读者更好的理解。大部分API跟属性都比较简单,看下文档就知道做什么的,这里就先不展开。
连接相关
- socket.connect():有3种不同的参数,用于不同的场景;
- socket.setTimeout():用来进行连接超时设置。
- socket.setKeepAlive():用来设置长连接。
- socket.destroy()、socket.destroyed:当错误发生时,用来销毁socket,确保这个socket上不会再有其他的IO操作。
数据读、写相关
socket.write()、socket.end()、socket.pause()、socket.resume()、socket.setEncoding()、socket.setNoDelay()
数据属性相关
socket.bufferSize、socket.bytesRead、socket.bytesWritten
事件循环相关
socket.ref()、socket.unref()
地址相关
- socket.address()
- socket.remoteAddress、socket.remoteFamily、socket.remotePort
- socket.localAddress/socket.localPort
事件简介
- data:当收到另一侧传来的数据时触发。
- connect:当连接建立时触发。
- close:连接断开时触发。如果是因为传输错误导致的连接断开,则参数为error。
- end:当连接另一侧发送了 FIN 包的时候触发(读者可以回顾下HTTP如何断开连接的)。默认情况下(allowHalfOpen == false),socket会完成自我销毁操作。但你也可以把 allowHalfOpen 设置为 true,这样就可以继续往socket里写数据。当然,最后你需要手动调用 socket.end()
- error:当有错误发生时,就会触发,参数为error。(官方文档基本一句话带过,不过考虑到出错的可能太多,也可以理解)
- timeout:提示用户,socket 已经超时,需要手动关闭连接。
- drain:当写缓存空了的时候触发。(不是很好描述,具体可以看下stream的介绍)
- lookup:域名解析完成时触发。
相关链接
官方文档: https://nodejs.org/api/net.html#net_socket_destroy_exception
网络UDP-dgram模块
dgram模块是对UDP socket的一层封装,相对net模块简单很多,下面看例子。
UDP客户端 vs UDP服务端
首先,启动UDP server,监听来自端口33333的请求。
server.js
// 例子:UDP服务端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
server.on('listening', function () {
var address = server.address();
console.log('UDP Server listening on ' + address.address + ":" + address.port);
});
server.on('message', function (message, remote) {
console.log(remote.address + ':' + remote.port +' - ' + message);
});
server.bind(PORT, HOST);
然后,创建UDP socket,向端口33333发送请求。
client.js
// 例子:UDP客户端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var message = Buffer.from('My KungFu is Good!');
var client = dgram.createSocket('udp4');
client.send(message, PORT, HOST, function(err, bytes) {
if (err) throw err;
console.log('UDP message sent to ' + HOST +':'+ PORT);
client.close();
});
运行 server.js。
node server.js
运行 client.js。
➜ 2016.12.22-dgram git:(master) ✗ node client.js
UDP message sent to 127.0.0.1:33333
服务端打印日志如下
UDP Server listening on 127.0.0.1:33333
127.0.0.1:58940 - My KungFu is Good!
广播
通过dgram实现广播功能很简单,服务端代码如下。
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
var port = 33333;
server.on('message', function(message, rinfo){
console.log('server got message from: ' + rinfo.address + ':' + rinfo.port);
});
server.bind(port);
接着创建客户端,向地址’255.255.255.255:33333’进行广播。
var dgram = require('dgram');
var client = dgram.createSocket('udp4');
var msg = Buffer.from('hello world');
var port = 33333;
var host = '255.255.255.255';
client.bind(function(){
client.setBroadcast(true);
client.send(msg, port, host, function(err){
if(err) throw err;
console.log('msg has been sent');
client.close();
});
});
运行程序,最终服务端打印日志如下
➜ 2016.12.22-dgram git:(master) ✗ node broadcast-server.js
server got message from: 192.168.0.102:61010
相关链接
https://nodejs.org/api/dgram.html