node.js以异步模式和事件队列为标准,基本上每一套与网络和IO相关的API都会设计成异步的。比如一个很常见的请求代码,只能用node.js异步使用。consthttps=require('https');https.get("https://nodejs.org/api/https.html",res=>res.pipe(process.stdout))异步方法不会阻塞进程,充分利用CPU。但是对于一些一次性的脚本和批处理,我们希望使用同步的方式。因为上面的情况,对效率的要求不是很迫切,但是需要更加清晰的代码结构和简洁的代码逻辑。在我之前的markdown-image-size中,有这样一个需求:当浏览器还没有加载完图片数据时,浏览器并不知道它的尺寸,所以默认尺寸为0,除非通过样式设置尺寸在有些时候,当图片加载完毕,浏览器获取到图片的尺寸时,文章会有跳动的感觉。阅读体验不是很好。解决这个问题,替换markdown文本中的和中的src进行匹配,如果是本地文件,则读取文件获取图片大小;或者发送请求获取图片数据,然后获取图片尺寸,最后进行字符串替换/插入,成为如下HTML格式文本。这种情况下同步网络请求比异步请求更适合,代码更清晰,逻辑更简单,对代码效率要求不高.下面是简化的同步请求和文本替换的代码。content.replace(/!\[(.*)\]\((.*?[^\\])\)/g,(matched,alt,src)=>{//从src同步获取图片数据constdata=getData(src);constsize=sizeOf(data);返回``})如果使用异步,替换后的文本不能直接在第二个参数中返回,需要更复杂的代码逻辑(比如标记文本的位置和长度,请求结束后进行替换)。那么如何实现node.js的同步请求呢?Google“syncrequestinnodejs”的搜索结果中出现了一个sync-request。npminstall后可以同步网络请求,一下子引起了我的兴趣:在一个官方没有提供的同步请求API的情况下,第三方包是如何实现请求的同步的?看完源码,发现作者巧妙地将异步问题转化成了同步问题。分析如下。sync-request在自述文件中,作者有这样一段话:这怎么可能?在内部,这使用了一个单独的工作进程,该工作进程使用childProcess.spawnSync运行。工作人员然后使用then-request发出实际请求,因此它具有与那个几乎完全相同的API。这也可以通过browserify在Web浏览器中使用,因为xhr内置了对同步执行的支持。请注意,不建议这样做,因为它会阻塞。总之,作者其实是使用then-request发送请求,用Promise封装了官方的异步API,所以是异步请求方式。异步转同步方式主要是使用childProcess.spawnSync方法创建一个同步进程。看完源码,基本流程如下:首先,需要nc命令的作用,以及标准输入输出如何传递字节数组。manpage中对nc的介绍是:nc--arbitraryTCPandUDPconnectionsandlistensusage:nc[hostname][port[s]]isalow-levelsystemcallforestablishingTCP/UDPconnectionsorlistening对于某个端口,因为是系统调用,所以速度更快,效率更高。标准输入输出如何传递字节数组,需要先将字节数组转成字符串,再转成字节数组再进行处理。默认的nodejs实现是将Buffer序列化为{"type":"Buffer","data":[1,2,3,4,5]},分成2个字段,但是这个不能反序列化回来。然后需要重写JSON序列化的方法,主要是对Buffer的处理。functionstringify(o){if(o&&Buffer.isBuffer(o))//hex,ascii都可用returnJSON.stringify(':base64:'+o.toString('base64'));if('string'===typeofo){//避免将缓冲区误认为是字符串returnJSON.stringify(/^:/.test(o)?':'+o:o)}//其他保持原样}functionparse(o){returnJSON.parse(s,function(key,value){if('string'===typeofvalue){if(/^:base64:/.test(value))返回新缓冲区(value.substring(8),'hex')else//stringreturn/^:/.test(value)?value.substring(1):value}returnvalue})}理解了上面的内容,我们再来看代码find-port详细.js获取空闲端口返回,基本原理如下(仅部分代码)module.exports=function(){returnnewPromise(function(resolve,reject){varserver=net.createServer();server.unref();server.on('error',reject);//port=0,绑定到可用端口server.listen(0,function(){varport=server.address()。港口;server.close(function(){resolve(port);});});});};legacy-work.js使用标准输入输出作为参数来源和返回出口,处理网络请求(then-request)constconcat=require('concat-stream');constrequest=require('then-request');constJSON=require('./json-buffer');functionrespond(data){process.stdout.write(JSON.stringify(data),function(){process.exit(0);});}process.stdin.pipe(concat(function(stdin){varreq=JSON.parse(stdin.toString());请求(req.method,req.url,req.options).done(function(response){respond({success:true,response:response});},function(错误){响应({成功:假,错误:{消息:err.message}});});}));nc-server.js启动一个TCP服务器与nc命令通信constnet=require('net');constconcat=require('concat-stream');constrequest=require('then-request');constJSON=require('./json-buffer');constserver=net.createServer({allowHalfOpen:true},c=>{函数响应(数据){c.end(JSON.stringify(数据));}c.pipe(concat(function(stdin){try{constreq=JSON.parse(stdin.toString());request(req.method,req.url,req.options).done(function(response){respond({success:true,response:response});},function(err){respond({success:false,error:{message:err.message}});});}catch(ex){响应({success:false,error:{message:ex.message}});}}));});server.listen(+process.argv[2]);其中{allowHalfOpen:true}是必不可少的,因为执行时spawnSync('nc',["127.0.0.1",nPort],{input:request}),input是JSON序列后面的字符串,input后到达EOF,相当于中的Ctrl+D控制键外壳和nc客户端套接字已关闭。只有在允许半开套接字的情况下,客户端才能收到服务端的数据,如下图:对应客户端的FIN_WAIT_2~TIME_WAIT时间段,服务端仍然可以发送数据。以上是对部分源码的分析。所以最终请求还是通过then-request实现的,但是then-request不支持multipart/formdata,所以也不支持sync-request。所以在fork之后,提交了一个带有form-data的pr,希望作者能尽快合并。最后我想说:原来可以这样实现同步!