Node net模块与http模块一些研究
这周遇到一个有意思的需求,端上同学希望通过 socket 传送表单数据(包含文件内容)到 node 端,根据表单里的文件名、手机号等信息将文件数据保存下来。于是我这样写了一下--socket_server.js:
const net = require('net');
const fs = require('fs');
const server = net.createServer((c) => {
let stream = fs.createWriteStream('test.txt');
c.pipe(stream).on('finish', () => {
console.log('Done');
});
c.on('error', (err) => {
console.log(err);
});
}).listen('4000', '127.0.0.1');
当后端同学发送数据过来后,我保存在 test.txt 里的数据是:
POST / HTTP/1.1
Host: 127.0.0.1:4000
Connection: keep-alive
Content-Length: 513
Accept: */*
Origin: http://localhost:63342
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytjiObRhDyrWvl3QP
Referer: http://localhost:63342/phone-upload/testSocket/index.html?_ijt=f8r6n5990ic71peiekdapbs02r
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.8,zh;q=0.6,ja;q=0.4,zh-TW;q=0.2 ------WebKitFormBoundarytjiObRhDyrWvl3QP
Content-Disposition: form-data; name="phone" 11111111111
------WebKitFormBoundarytjiObRhDyrWvl3QP
Content-Disposition: form-data; name="file"; filename="index.js"
Content-Type: text/javascript var koa = require('koa');
var app = koa();
var statistics = require('../中间件/statistics.js'); app.use(statistics({
whiteList: ['', 'cq']
})); app.use(function *(){
this.body = 'Hello World';
}); app.listen(3000);
------WebKitFormBoundarytjiObRhDyrWvl3QP--
也就是说,我需要在 node 端做解析的工作(实际上就是 http 模块做的事),如果一直发送的是 txt 文件还好说,我可以根据 boundary 和换行解析文本数据,但如果发送的文件内容是 zip 之类的二进制数据,那么我该如何解析?于是,我打算自己好好研究一下这个问题,但也不能一直麻烦端上同学发文件让我调试,于是我不假思索的写出了如下代码--index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src='http://libs.baidu.com/jquery/2.1.1/jquery.min.js'></script>
</head>
<body>
<input type="file" id="file" multiple/>
<input type="button" onclick="PostData()" value="提交">
<script>
function PostData() {
var form = $(this); var files = document.querySelector('#file').files;
var form_data = new FormData();
form_data.append('phone', `111111111111`);
form_data.append('file', files[0]);
$.ajax({
type: 'POST',
url: 'http://127.0.0.1:4000',
data: form_data,
mimeType: "multipart/form-data",
contentType: false,
cache: false,
processData: false
}).success(function () {
//成功提交
console.log('success');
}).fail(function (jqXHR, textStatus, errorThrown) {
//错误信息
console.log('err');
});
}
</script>
</body>
</html>
当我在网页端选定文件,点击提交后,一件有趣的事情发生了:网页端的 AJAX 请求一直在 pending,后端也一直没打出 'Done' 的 log,当我刷新页面后,后端才显示 'Done' 并获取到文件内容。我抱着疑问又写了一份 socket 客户端--socket_client.js:
const client = net.createConnection('4000', '127.0.0.1', () => {
let stream = fs.createReadStream('test2.txt');
stream.pipe(client).on('finish', () => {
console.log('Done');
});
stream.on('error', (err) => {
console.log(err);
});
});
这次发现 socket 客户端和服务端表现正常,都及时打出了 'Done' 的日志,那么问题一定就出在 http 和 tcp 的差异上了。为了验证自己的想法,我又写了一份 http 服务端--http_server.js:
const http = require("http");
const fs = require("fs");
const server = http.createServer((req, res) => {
let stream = fs.createWriteStream('test.txt');
req.pipe(stream).on('finish', () => {
console.log('Done');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Done');
});
});
server.listen(4000);
再次通过网页端上传文件,网页这边 AJAX 立即返回,没有出现 pending 现象,当然去掉第 8、9 行能复现 pending。后端这边也立即打出 'Done'。
于是带着种种疑问参考了源码,
//_http_server.js
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true }); if (requestListener) {
this.on('request', requestListener);
} // Similar option to this. Too lazy to write my own docs.
// http://www.squid-cache.org/Doc/config/half_closed_clients/
// http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false; this.on('connection', connectionListener); this.timeout = 2 * 60 * 1000;
this.keepAliveTimeout = 5000;
this._pendingResponseData = 0;
this.maxHeadersCount = null;
}
上一部分是 http 模块 createServer 函数的代码,发现实际上就是调用 net.Server,并监听 'request' 事件运行 requestListener (对应 http_server.js 就是5-10行)。当有 socket 连接过来的时候会触发 'connection' 事件:
//_http_server.js
function connectionListener(socket) {
//...
var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
socket.parser = parser;
parser.incoming = null; //...
state.onData = socketOnData.bind(undefined, this, socket, parser, state);
//...
} function socketOnData(server, socket, parser, state, d) {
assert(!socket._paused);
debug('SERVER socketOnData %d', d.length); var ret = parser.execute(d);
onParserExecuteCommon(server, socket, parser, state, ret, d);
}
通过 HTTP parser 来解析 TCP 传输过来的数据,而 HTTP parser 来自:
//_http_common.js
//...
const HTTPParser = binding.HTTPParser;
//...
var parsers = new FreeList('parsers', 1000, function() {
var parser = new HTTPParser(HTTPParser.REQUEST); parser._headers = [];
parser._url = '';
parser._consumed = false; parser.socket = null;
parser.incoming = null;
parser.outgoing = null; // Only called in the slow case where slow means
// that the request headers were either fragmented
// across multiple TCP packets or too large to be
// processed in a single run. This method is also
// called to process trailing HTTP headers.
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
parser[kOnExecute] = null; return parser;
}); //_http_server.js
function connectionListener(socket) {
//...
parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);
//...
} function parserOnIncoming(server, socket, state, req, keepAlive) {
//...
server.emit('request', req, res);
//...
}
从上述代码可以看到 parser 解析得到请求头、请求体,触发 'request' 事件,但由于 HTTPParser 是内置的用 C 实现的模块(还有个用 JS 实现的 HTTPParser),具体如何解析以及事件触发还没去细细了解,但总体流程大概清晰了起来。实际上 http 模块本质上就是在 net 模块的基础上添加了 HTTPParser 等功能,
在这里还有一点值得注意,http 模块创建 server 的时候设置 allowHalfOpen 为 true,默认为 false
官网上的解释是:“If allowHalfOpen is set to true, when the other end of the socket sends a FIN packet, the server will only send a FIN packet back when socket.end() is explicitly called, until then the connection is half-closed (non-readable but still writable).”
结合 ‘end’ 事件的解释:“Emitted when the other end of the socket sends a FIN packet, thus ending the readable side of the socket.By default (allowHalfOpen is false) the socket will send a FIN packet back and destroy its file descriptor once it has written out its pending write queue. However, if allowHalfOpen is set to true, the socket will not automatically end() its writable side, allowing the user to write arbitrary amounts of data. The user must call end() explicitly to close the connection (i.e. sending a FIN packet back).”。
大概意思是,当客户端和服务端建立了 socket 连接后,net.Socket 对象是 duplex stream,能读能写。当客户端调用 socket.end 后,触发 end 事件, 并发送 FIN 包给服务端,表示自己不再写数据了,当服务端 allowHalfOpen 设置为 false 时,一旦服务端将所有数据发送完,也会回发 FIN 包给客户端并释放文件描述符(在 linux 上,一切都是文件,socket 实际上也是文件资源)。当服务端 allowHalfOpen 设置为 true 时,只有显式的调用 socket.end 才会关闭连接,此时服务端仍能写数据给客户端。测试如下:
socket_server.js:
const net = require('net');
const fs = require('fs');
const server = net.createServer({allowHalfOpen:false}, listener => {
console.log('connected');
listener.on('data', (data) => {
console.log(data.toString());
listener.write('one');
});
listener.on('end', () => {
console.log('RECV FIN');
listener.write('two');
});
}).listen('4000', '127.0.0.1');
socket_client.js:
const net = require('net');
const client = net.createConnection({ port: 4000 }, () => {
console.log('connected to server!');
client.write('hello');
});
client.on('data', (data) => {
console.log(data.toString());
client.end();
console.log('SEND FIN');
});
client.on('end', () => {
console.log('RECV FIN');
});
client.on('close', () => {
console.log('client closed');
});
运行服务端,再运行客户端后会报错:Error: This socket has been ended by the other party。当客户端调用 socket.end 后,连接就会中断并释放,所以服务端再写数据就会出错。将 allowHalfOpen 设置为 true 后,客户端再发送 FIN 后,仍能接收服务端的数据。但注意此时客户端不会关闭,直到服务端显示的调用 socket.end 后,客户端才会关闭。
这个现象是不是很像最初遇到的网页端 pending 现象?实际上我猜想原因就在于此,具体原因也没有去深究了。
Node net模块与http模块一些研究的更多相关文章
- Node.js权威指南 (4) - 模块与npm包管理工具
4.1 核心模块与文件模块 / 574.2 从模块外部访问模块内的成员 / 58 4.2.1 使用exports对象 / 58 4.2.2 将模块定义为类 / 58 4.2.3 为模块类定义类变量或类 ...
- node.js(七) 子进程 child_process模块
众所周知node.js是基于单线程模型架构,这样的设计可以带来高效的CPU利用率,但是无法却利用多个核心的CPU,为了解决这个问题,node.js提供了child_process模块,通过多进程来实现 ...
- node.js第二天之模块
一.模块的定义 1.在Node.js中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,这时的我们可以将应用程序划分为各个不同的部分. 2.狭义的说,每一个JavaScript文件都是一个 ...
- node之子线程child_process模块
node.js是基于单线程模型架构,这样的设计可以带来高效的CPU利用率,但是无法却利用多个核心的CPU,为了解决这个问题,node.js提供了child_process模块,用于新建子进程,子进程的 ...
- node.js中使用http模块创建服务器和客户端
node.js中的 http 模块提供了创建服务器和客户端的方法,http 全称是超文本传输协议,基于 tcp 之上,属于应用层协议. 一.创建http服务器 const http = require ...
- node学习笔记6——自定义模块
自定义模块三大关键词: require——引入模块: exports——单个输出: module——批量输出. 从例子下手: 1.创建module.js: exports.a=22; exports. ...
- 高性能Web服务器Nginx的配置与部署研究(13)应用模块之Memcached模块+Proxy_Cache双层缓存模式
通过<高性能Web服务器Nginx的配置与部署研究——(11)应用模块之Memcached模块的两大应用场景>一文,我们知道Nginx从Memcached读取数据的方式,如果命中,那么效率 ...
- Node.js学习笔记(一) --- HTTP 模块、URL 模块、supervisor 工具
一.Node.js创建第一个应用 如果我们使用 PHP 来编写后端的代码时,需要 Apache 或者 Nginx 的 HTTP 服务器, 来处理客户端的请求相应.不过对 Node.js 来说,概念完全 ...
- Node.js 实现第一个应用以及HTTP模块和URL模块应用
/* 实现一个应用,同时还实现了整个 HTTP 服务器. * */ //1.引入http模块 var http=require('http'); //2.用http模块创建服务 /* req获取url ...
- node.js创建并引用模块
app.js var express = require('express'); var app = express(); var con = require('./content'); con.he ...
随机推荐
- 在Mac OS X使用Elasticsearch的基本流程
这篇日志的目的非常easy,就是记录一些主要的流程.要在OS X上使用Elasticsearch,事实上非常easy,在这里:https://www.elastic.co/downloads/elas ...
- Oracle DG强制激活 备库
在实际运营环境中,我们经常碰到类似这样的需求,譬如想不影响现网业务评估DB补丁在现网环境中运行的时间,或者是想在做DB切换前想连接Standby DB做实际业务运行的测试,如果在9i版本的时候,想做到 ...
- python全栈开发从入门到放弃之列表的内置方法
1.列表切片 l=['a','b','c','d','e','f'] print(l[1:5]) # 根据索引号来切片,但顾头不顾尾 ['b', 'c', 'd', 'e'] print(l[1:5: ...
- Delphi 正则表达式之TPerlRegEx 类的属性与方法(7): Split 函数
Delphi 正则表达式之TPerlRegEx 类的属性与方法(7): Split 函数 //字符串分割: Split var reg: TPerlRegEx; List: TStrings; ...
- CAS单点登出的原理
单点登出功能跟单点登录功能是相对应的,旨在通过Cas Server的登出使所有的Cas Client都登出. Cas Server的登出是通过请求“/logout”发生的,即如果你的Cas Serve ...
- c语言单元测试框架--CuTest
1.简介 CuTest是一款微小的C语言单元测试框,是我迄今为止见到的最简洁的测试框架之一,只有2个文件,CuTest.c和CuTest.h,全部代码加起来不到一千行.麻雀虽小,五脏俱全,测试的构建. ...
- Sybase数据库:两个特别注意的地方
Sybase数据库:两个特别注意的地方 一.字段别名 字段别名不能为查询条件中的列名,会导致查询出来的数据不准确:最好字段别名为非列名: 二.更新的表名的大小写 update a set .... s ...
- jQuery/CSS3 3D焦点图动画
在线演示 本地下载
- python 异常的引发和捕捉处理
1.什么是异常(exception): 异常是python发现某个地方出现逻辑错误时,抛出一个信号,即异常的引发.如果有捕捉语句在,则异常信号被捕捉,如果没有则会传递到默认异常处理器(终止程序). ...
- 部署私有云网盘owncloud
环境说明: [root@localhost ~]# cat /etc/redhat-release CentOS release 6.9 (Final) [root@localhost ~]# una ...