node.js实战之坑

最近做了一个node.js的中间层项目,为了极致的性能,我们打算尽量少用第三方类库,连HttpServer我们都用原生的来做,没有使用Expressjs。

1、request的body拼接的坑

这个是老生常谈的问题,一般写出如下代码的同学,都是会被喷是Node.js初学者。

1
2
3
4
5
6
7
8
9
10
11
12
13
http.request({
host:'xxx.xxx.xxx',
path:"/xxx",
}, (res)=>{
let body = ''
res.on('data', (chunk) => {
body += chunk.toString()
})
res.once('end', () => {
res.removeAllListeners()
callback(null, res, body)
})
})

为什么上面的代码会被喷?因为chunk是bufer类型,直接进行toString拼接,对于二进制的响应,可能会存在问题,特别是中文会被截断。于是我们就改成了下面这个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
http.request({
host:'xxx.xxx.xxx',
path:"/xxx",
}, (res)=>{
let body = []
res.on('data', (chunk) => {
body.push(chunk)
})
res.once('end', () => {
res.removeAllListeners()
callback(null, res, body.join(''))
})
})

然而事与愿违,body.join(‘’)在拼接buffer块的时候也会存在先toString()再拼接的,所以真确的代码应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
http.request({
host:'xxx.xxx.xxx',
path:"/xxx",
}, (res)=>{
let body = []
res.on('data', (chunk) => {
body.push(chunk)
})
res.once('end', () => {
res.removeAllListeners()
callback(null, res, Buffer.concat(body).toString())
})
})

另外,Node.js在utf-8字节不正确的时候,会把utf-8字节改成\uFFFD,当出现类似?号的乱码时,就需要注意了,是因为某一个utf-8字节缺失导致的。

2、两次callback调用的坑

当我们初次调试我们代码的时候,发生了一个诡异的问题,callback被莫名其妙的调用了两次,我们还是上伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const reqToRemote = (opt, cb)=>{
request(opt, (err, respBody){
if(err){
cb(nul)
return
}

try{
const obj = JSON.parse(respBody)
cb(obj)
return
}catch(e){
cb(null)
return
}

})
}

初步看上去,代码似乎没啥问题,每个callback后面都根了return,保证callback只会被调用一次,没有理由callback会被调用两次呢。但是当cb传入的函数,本身throw error的情况呢?

也就是说,JSON.parse并没有发生异常,而是cb函数本身抛出了同步的异常,这时候try就捕获了错误,同时执行了catch代码块的cb,这个时候我们的callback就被执行了两次。

3、原生reqeust的timeout

我们在代码初次调试的时候,发现无论reqeust的timeout设置多久,都没有出现超时的情况,我们初次写的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const req = http.request({
host: 'xxx.xx.xx',
port: 80,
method: 'GET',
path: '/xx/yy',
headers: {},
timeout: 500, // 超时500ms,但是无论怎么设置都是无效的
}, (res) => {
// 获取resbody
})

req.once('error', (err) => {
callback(err, null, null)
})

req.write('requestBuffer')
req.end()

后来翻阅node.js的api文档,发现原生的http.request函数里的timeout,仅仅是socket的连接超时,原文如下:

A number specifying the socket timeout in milliseconds. This will set the timeout before the socket is connected.

所以我们需要对socket连接之后设置timeout来进行请求的超时限制,伪代码如下:

1
2
3
4
5
6
req.once('socket', (s: Socket) => { 
s.setTimeout(3000, () => {
s.removeAllListeners()
req.destroy(new Error('client request timeout'))
})
});

但是这样的做法,效果是可以,可惜会导致正常响应之后,req的error事件触发,这里我们就做了一个小动作,保证了req的回调只执行一次。