通过node的pipe实现请求代理

2017-03-03  本文已影响0人  墙角儿的花

引言

充分利用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 
    ......

排查

  1. 不通过node的pipe转发,直接发送post请求是没有问题的,如通过postman工具直接发送post请求是没有问题的,且node的转发实现不去pipe请求流,调整为直接创建新的请求并重新发送请求体也是没有问题的

  2. 跟踪spring源码发现,发生问题的测试案例下,查看request的输入流inputstream.available()为0,运行read同样抛出以上异常。也就是说经过node转发过来的流body体为空。

  3. 同样的方式将请求转发到一个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体为空。
该代码用不上,删除掉即可。

上一篇下一篇

猜你喜欢

热点阅读