通过node的pipe实现请求代理
引言
充分利用node的高吞吐量和java服务的稳定性,特搭建node+java的开发和运行框架。前端代码采用js,一部分作为界面代码运行于浏览器,一部分做为转发代理运行于node。后端java采用spring实现restful的web service。
从浏览器访问restful service,需要经过node转发,转发请求的代码采用node的pipe,该方法实现流间的管道对接,减少了传统的从一个流中读取内容然后写入目标流中的系统开销。
在实际的实现中遇见一个问题:浏览器发送post请求,经过node代理将请求转发给tomcat下的spring restful api,而spring无法获取到请求体。
问题描述
浏览器通过jquery.ajax发送post请求,请求一个restful api,如:/sh/person,请求体为{"name":"test","age":66},
jquery发送post请求:
var data = {name:"test",age:66};
$.ajax({
type:"post",
url:"/sh/person",
data:JSON.stringify(data),
//processData:false,
success:function(d){
},
dataType:'json',
contentType:"application/json; charset=utf-8"
});
由node7通过pipe将浏览器的api请求转发到后端spring restful web service
代码:
app.use('/sh/', function(req, res, next) {
var options = {
host: "127.0.0.1",
port: "8080",
path: '/sh'+req.url,
method: req.method,
headers: req.headers
};
var request = http.request(options,function(response){
res.statusCode = response.statusCode;
response.pipe(res);
}).on("error",function(){
res.statusCode = 503;
res.end();
});
req.pipe(request);
// request.write(JSON.stringify({name:"test",age:66}));
// request.end();
});
tomcat8下spring4 restful 控制器的方法
@RequestMapping(value = "/person", method = RequestMethod.POST)
public @ResponseBody
Person addPerson(@RequestBody Person person) {
personService.add(person);
return person;
}
spring在DispatchServlet中解析请求体,报如下错误,该错最终是由tomcat读取请求的编码信息时抛出的SocketTimeoutException引起:
org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: null; nested exception is java.net.SocketTimeoutException
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:228)
......
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.SocketTimeoutException
at org.apache.tomcat.util.net.NioBlockingSelector.read(NioBlockingSelector.java:202)
at org.apache.tomcat.util.net.NioSelectorPool.read(NioSelectorPool.java:250)
at org.apache.tomcat.util.net.NioSelectorPool.read(NioSelectorPool.java:231)
at org.apache.coyote.http11.InternalNioInputBuffer.fill(InternalNioInputBuffer.java:133)
at org.apache.coyote.http11.InternalNioInputBuffer$SocketInputBuffer.doRead(InternalNioInputBuffer.java:177)
at org.apache.coyote.http11.filters.IdentityInputFilter.doRead(IdentityInputFilter.java:110)
at org.apache.coyote.http11.AbstractInputBuffer.doRead(AbstractInputBuffer.java:414)
at org.apache.coyote.Request.doRead(Request.java:476)
at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:350)
at org.apache.tomcat.util.buf.ByteChunk.substract(ByteChunk.java:395)
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:375)
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:190)
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.ensureLoaded(ByteSourceJsonBootstrapper.java:489)
at
......
排查
-
不通过node的pipe转发,直接发送post请求是没有问题的,如通过postman工具直接发送post请求是没有问题的,且node的转发实现不去pipe请求流,调整为直接创建新的请求并重新发送请求体也是没有问题的
-
跟踪spring源码发现,发生问题的测试案例下,查看request的输入流inputstream.available()为0,运行read同样抛出以上异常。也就是说经过node转发过来的流body体为空。
-
同样的方式将请求转发到一个jetty server里,读取输入流也报错
org.eclipse.jetty.io.EofException: early EOF at org.eclipse.jetty.server.HttpInput.read(HttpInput.java:65) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) at java.io.InputStreamReader.read(InputStreamReader.java:184)
结论
通过以上排查确定后端问题的可能性非常小,tomcat和jetty读取body都会出错。
那么问题还是在node端。
从后端调试发现node转发post过来的body体为空,说明应该是有动作已经读取了请求的输入流,因为请求的输入流只能读取一次。
最后,发现node端有如下代码:
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
该代码是由express-generator自动生成,因此一直没得到关注。body-parser用于body体的解析,因此其自动读取了请求输入流导致浏览器请求被pipe到后端后,后端读取的body体为空。
该代码用不上,删除掉即可。