先用个“观点图”来描述下全体系统的架构:
嗯,这个是真正的“观点图”,由于我已经把大部分细节都屏蔽了,别笑,由于本文的重点只是全体架构中的一小部分,便是上图中红框内的 http server。
大概你会问,这不便是个 HTTP 做事器吗,而且是只处理一个要求的 HTTP 做事器,搞个java web 项目在 Tomcat 中一启动不就完事儿了,有啥好讲的呀?。莫慌,且听老夫逐步道来为啥要用 netty HTTP 协议栈来实现这个吸收转发做事。

基于以上几点缘故原由,老夫就决定利用 netty HTTP 协议栈开干啦~
本文并非纯理论或纯技能类文章,而是结合理论进而实践(虽然没有特殊深入的实践),浅析 netty 的 HTTP 协议栈,并着重聊聊实践中碰着的问题及办理方案。越今后越精彩哦!
想理解更多可以加 裙 854393687
netty 官方供应了关于 HTTP 的例子,大伙儿可以在 netty 项目中查看。
https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http
https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http2
1.2 关于github项目本人在网上利用 “netty + HTTP” 的关键字搜索了下,创造大部分都是原搬照抄 netty 项目中的 example,很少有“原创性”的实践,也险些没有看到实现一个相对完全的 HTTP 做事器的项目(比如如何解析GET/POST要求、自定义 HTTP decoder、对 HTTP 是非连接的思考等等……),因此就自己整理了一个相对完全一点的项目,项目地址https://github.com/cyfonly/netty-http,该项目实现了基于 netty5 的 HTTP 做事端,暂时实现以下功能:
HTTP GET 要求解析与相应HTTP POST 要求解析与相应,供应 application/json、application/x-www-form-urlencoded、multipart/form-data 三种常见 Content-Type 的 message body 解析示例HTTP decoder实现,供应 POST 要求 message body 解码器的 HttpJsonDecoder 及 HttpProtobufDecoder 实现示例作为做事端吸收浏览器文件上传及保存将来可能会连续实现的功能有:
命名空间uri路由chunked 传输编码如果你也打算利用 netty 来实现 HTTP 做事器,相信这个项目和本文对你是有较大帮助的!
好了,闲话不多说,下面正式进入正题。
二、HTTP 协议知多少要通过 netty 实现 HTTP 做事端(或者客户端),首先你得理解 HTTP 协议【1】。
HTTP 协议是要求/相应式的协议,客户端须要发送一个要求,做事器才会返回相应内容。例如在浏览器上输入一个网址按下 Enter,或者提交一个 Form 表单,浏览器就会发送一个要求到做事器,而打开的网页的内容,便是做事器返回的相应。
下面讲下 HTTP 要乞降相应包含的内容。
HTTP 要求有很多种 method,最常用的便是 GET 和 POST,每种 method 的要求之间会有细微的差异。下面分别剖析一下 GET 和 POST 要求。
2.1 GET要求下面是浏览器对 http://localhost:8081/test?name=XXG&age=23 的 GET 要求时发送给做事器的数据:
可以看出要求包含 request line 和 header 两部分。个中 request line 中包含 method(例如 GET、POST)、request uri 和 protocol version 三部分,三个部分之间以空格分开。request line 和每个 header 各占一行,以换行符 CRLF(即 \r\n)分割。
2.2 POST要求下面是浏览器对 http://localhost:8081/test 的 POST 要求时发送给做事器的数据,同样带上参数 name=XXG&age=23:
可以看出,上面的要求包含三个部分:request line、header、message,比之前的 GET 要求多了一个 message body,个中 header 和 message body 之间用一个空行分割。POST 要求的参数不在 URL 中,而是在 message body 中,header 中多了一项 Content-Length 用于表示 message body 的字节数,这样做事器才能知道要求是否发送结束。这也便是 GET 要乞降 POST 要求的紧张差异。
HTTP 相应和 HTTP 要求非常相似,HTTP 相应包含三个部分:status line、header、massage body。个中 status line 包含 protocol version、状态码(status code)、reason phrase 三部分。状态码用于描述 HTTP 相应的状态,例如 200 表示成功,404 表示资源未找到,500 表示做事器出错。
在上面的 HTTP 相应中,Header 中的 Content-Length 同样用于表示 message body 的字节数。Content-Type 表示 message body 的类型,常日浏览网页其类型是HTML,当然还会有其他类型,比如图片、视频等。
2.3 HTTP POST Content-TypeHTTP/1.1 协议规定的 HTTP 要求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这几种。个中 POST 一样平常用来向做事端提交数据,本文谈论紧张的几种 POST 提交数据办法。
我们知道,HTTP 协议因此 ASCII 码传输,建立在 TCP/IP 协议之上的运用层规范。规范把 HTTP 要求分为三个部分:状态行、要求头、主体。类似于下面这样:
<method> <request-URL> <version><headers><entity-body>
协议规定 POST 提交的数据必须放在主体(entity-body)中,但协议并没有规定数据必须利用什么编码办法。实际上,开拓者完备可以自己决定主体的格式,只要末了发送的 HTTP 要求知足上面的格式就可以。
但是,数据发送出去,还要做事端解析成功才故意义。一样平常做事端措辞如 php、python 等,以及它们的 framework,都内置了自动解析常见数据格式的功能。做事端常日是根据要求头(headers)中的 Content-Type 字段来获知要求中的主体是用何种办法编码,再对主体进行解析。以是说到 POST 提交数据方案,包含了 Content-Type 和主体编码办法 Charset 两部分。下面就正式开始先容它们。
2.3.1 application/x-www-form-urlencoded这该当是最常见的 POST 提交数据的办法了。浏览器的原生 Form 表单,如果不设置 enctype 属性,那么终极就会以 application/x-www-form-urlencoded 办法提交数据。要求类似于下面这样(无关的要求头在本文中都省略掉了):
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
首先,Content-Type 被指定为 application/x-www-form-urlencoded;其次,提交的数据按照 key1=val1&key2=val2 的办法进行编码,key 和 val 都进行了 URL 转码。大部分做事端措辞都对这种办法有很好的支持。
很多时候,我们用 Ajax 提交数据时,也是利用这种办法。例如 JQuery 的 Ajax,Content-Type 默认值都是 application/x-www-form-urlencoded;charset=utf-8 。
2.3.2 multipart/form-data这又是一个常见的 POST 数据提交的办法。我们利用表单上传文件时,必须让 Form 的 enctyped 即是这个值。直接来看一个要求示例:
POST http://www.example.com HTTP/1.1Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA ------WebKitFormBoundaryrGKCBY7qhFd3TrwAContent-Disposition: form-data; name=\"大众text\公众 title------WebKitFormBoundaryrGKCBY7qhFd3TrwAContent-Disposition: form-data; name=\"大众file\"大众; filename=\"大众chrome.png\"大众Content-Type: image/png PNG ... content of chrome.png ...------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
这个例子轻微繁芜点。首先天生了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很繁芜。然后 Content-Type 里指明了数据因此 mutipart/form-data 来编码,本次要求的 boundary 是什么内容。主体里按照字段个数又分为多个构造类似的部分,每部分都因此 –boundary 开始,紧接着内容描述信息,然后是回车,末了是字段详细内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。主体末了以 –boundary– 标示结束。
这种办法一样平常用来上传文件,各大做事端措辞对它也有着良好的支持。
上面提到的这两种 POST 数据的办法,都是浏览器原生支持的,而且现阶段原生 Form 表单也只支持这两种办法。但是随着越来越多的 Web 站点,尤其是 WebApp,全部利用 Ajax 进行数据交互之后,我们完备可以定义新的数据提交办法,给开拓带来更多便利。
2.3.3 application/jsonapplication/json 这个 Content-Type 作为相应头大家肯定不陌生。实际上,现在越来越多的人把它作为要求头,用来见告做事端主体是序列化后的 JSON 字符串。由于 JSON 规范的盛行,除了低版本 IE 之外的各大浏览器都原生支持 JSON.stringify,做事端措辞也都有处理 JSON 的函数,利用 JSON 不会遇上什么麻烦。
JSON 格式支持比键值对繁芜得多的构造化数据,这一点也很有用,当须要提交的数据层次非常深,就可以考虑把数据 JSON 序列化之后来提交的。
var data = {'title':'test', 'sub' : [1,2,3]};$http.post(url, data).success(function(result) { ...});
终极发送的要求是:
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{\"大众title\"大众:\"大众test\公众,\公众sub\公众:[1,2,3]}
这种方案,可以方便的提交繁芜的构造化数据,特殊适宜 RESTful 的接口。各大抓包工具如 Chrome 自带的开拓者工具、Fiddler,都会以树形构造展示 JSON 数据,非常友好。
其他几种 Content-Type 就不一一详细先容了,感兴趣的童鞋请自行理解。下面进入 netty 支持 HTTP 协议的源码剖析阶段。
三、netty HTTP 编解码要通过 netty 处理 HTTP 要求,须要前辈行编解码。
3.1 netty 自带 HTTP 编解码器netty5 供应了对 HTTP 协议的几种编解码器:
3.1.1 HttpRequestDecoderDecodes ByteBuf into HttpRequest and HttpContent.
即把 ByteBuf 解码到 HttpRequest 和 HttpContent。
3.1.2 HttpResponseEncoderEncodes an HttpResponse or an HttpContent into a ByteBuf.
即把 HttpResponse 或 HttpContent 编码到 ByteBuf。
3.1.3 HttpServerCodecA combination of HttpRequestDecoder and HttpResponseEncoder which enables easier server side HTTP implementation.
即 HttpRequestDecoder 和 HttpResponseEncoder 的结合。
因此,基于 netty 实现 HTTP 做事端时,须要在 ChannelPipeline 中加上以上编解码器:
ch.pipeline().addLast(\公众codec\"大众,new HttpServerCodec())
或者
ch.pipeline().addLast(\"大众decoder\公众,new HttpRequestDecoder())
.addLast(\"大众encoder\"大众,new HttpResponseEncoder())
然而,以上编解码器只能够支持部分 HTTP 要求解析,比如 HTTP GET要求所通报的参数是包含在 uri 中的,因此通过 HttpRequest 既能解析出要求参数。但是,对付 HTTP POST 要求,参数信息是放在 message body 中的(对应于 netty 来说便是 HttpMessage),以是以上编解码器并不能完备解析 HTTP POST要求。
这种情形该怎么办呢?别慌,netty 供应了一个 handler 来处理。
3.1.4 HttpObjectAggregatorA ChannelHandler that aggregates an HttpMessage and its following HttpContent into a single FullHttpRequest or FullHttpResponse
(depending on if it used to handle requests or responses) with no following HttpContent.
It is useful when you don't want to take care of HTTP messages whose transfer encoding is 'chunked'.
即通过它可以把 HttpMessage 和 HttpContent 聚合成一个 FullHttpRequest 或者 FullHttpResponse (取决于是处理要求还是相应),而且它还可以帮助你在解码时忽略是否为“块”传输办法。
因此,在解析 HTTP POST 要求时,请务必在 ChannelPipeline 中加上 HttpObjectAggregator。(详细细节请自行查阅代码)
当然,netty 还供应了其他 HTTP 编解码器,有些涉及到高等运用(较繁芜的运用),在此就不一一阐明了,以上只是先容netty HTTP 协议栈最基本的编解码器(相符文章主题——浅析)。
3.2 HTTP GET 解析实践上面提到过,HTTP GET 要求的参数是包含在 uri 中的,可通过以下办法解析出 uri:
HttpRequest request = (HttpRequest) msg;
String uri = request.uri();
特殊把稳的是,用浏览器发起 HTTP 要求时,常常会被 uri = “/favicon.ico” 所滋扰,因此最好对其分外处理:
if(uri.equals(FAVICON_ICO)){
return;
}
接下来便是解析 uri 了。这里须要用到 QueryStringDecoder:
Splits an HTTP query string into a path string and key-value parameter pairs.
This decoder is for one time use only. Create a new instance for each URI:
QueryStringDecoder decoder = new QueryStringDecoder(\"大众/hello?recipient=world&x=1;y=2\"大众);
assert decoder.getPath().equals(\公众/hello\"大众);
assert decoder.getParameters().get(\"大众recipient\"大众).get(0).equals(\"大众world\"大众);
assert decoder.getParameters().get(\"大众x\"大众).get(0).equals(\"大众1\公众);
assert decoder.getParameters().get(\"大众y\"大众).get(0).equals(\公众2\"大众);
This decoder can also decode the content of an HTTP POST request whose
content type is application/x-www-form-urlencoded:
QueryStringDecoder decoder = new QueryStringDecoder(\"大众recipient=world&x=1;y=2\"大众, false);
...
从上面的描述可以看出,QueryStringDecoder 的浸染便是把 HTTP uri 分割成 path 和 key-value 参数对,也可以用来解码 Content-Type = “application/x-www-form-urlencoded” 的 HTTP POST。特殊把稳的是,该 decoder 仅能利用一次。
解析代码如下:
String uri = request.uri();
HttpMethod method = request.method();
if(method.equals(HttpMethod.GET)){
QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8));
Map<String, List<String>> uriAttributes = queryDecoder.parameters();
//此处仅打印要求参数(你可以根据业务需求自定义处理)
for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
for (String attrVal : attr.getValue()) {
System.out.println(attr.getKey() + \"大众=\"大众 + attrVal);
}
}
}
3.3 HTTP POST 解析实践如3.1.4小结所说的那样,解析 HTTP POST 要求的 message body,一定要利用 HttpObjectAggregator。但是,是否一定要把 msg 转换成 FullHttpRequest 呢?答案是否定的,且往下看。
首先阐明下 FullHttpRequest 是什么:
Combinate the HttpRequest and FullHttpMessage, so the request is a complete HTTP request.
即 FullHttpRequest 包含了 HttpRequest 和 FullHttpMessage,是一个 HTTP 要求的完备体。
而把 msg 转换成 FullHttpRequest 的方法很大略:
FullHttpRequest fullRequest = (FullHttpRequest) msg;
接下来便是分几种 Content-Type 进行解析了。
3.3.1 解析 application/json处理 JSON 格式是非常方便的,我们只须要将 msg 转换成 FullHttpRequest,然后将其 content 反序列化成 JSONObject 工具即可,如下:
FullHttpRequest fullRequest = (FullHttpRequest) msg;String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));JSONObject obj = JSON.parseObject(jsonStr);for(Entry<String, Object> item : obj.entrySet()){ System.out.println(item.getKey()+\"大众=\"大众+item.getValue().toString());}3.3.2 解析 application/x-www-form-urlencoded
解析此类型有两种方法,一种是利用 QueryStringDecoder,其余一种便是利用 HttpPostRequestDecoder。
方法一:3.2节中讲 QueryStringDecoder 时提到:QueryStringDecoder 可以用来解码 Content-Type = “application/x-www-form-urlencoded” 的 HTTP POST。因此我们可以用它来解析 message body,剩下的处理就跟 HTTP GET没什么两样了:
FullHttpRequest fullRequest = (FullHttpRequest) msg;
String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
QueryStringDecoder queryDecoder = new QueryStringDecoder(jsonStr, false);
Map<String, List<String>> uriAttributes = queryDecoder.parameters();
for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
for (String attrVal : attr.getValue()) {
System.out.println(attr.getKey()+\"大众=\"大众+attrVal);
}
}
方法二:利用 HttpPostRequestDecoder 解析时,无需先将 msg 转换成 FullHttpRequest。
我们先来理解下 HttpPostRequestDecoder :
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
if (factory == null) {
throw new NullPointerException(\"大众factory\"大众);
}
if (request == null) {
throw new NullPointerException(\公众request\"大众);
}
if (charset == null) {
throw new NullPointerException(\"大众charset\公众);
}
// Fill default values
if (isMultipart(request)) {
decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
} else {
decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
}
}
由它的定义可知,它的内部实现实在有两种办法,一种是针对 multipart 类型的解析,一种是普通类型的解析。这两种办法的详细实现中,我把它们相同的代码提取出来,如下:
if (request instanceof HttpContent) { // Offer automatically if the given request is als type of HttpContent offer((HttpContent) request);} else { undecodedChunk = buffer(); parseBody();}
由于我们利用过 HttpObjectAggregator, request 都是 HttpContent 类型,因此会 Offer automatically,我们就不必自己手动去 offer 了,也不用处理 Chunk,以是利用 HttpObjectAggregator 确实是带来了很多简便的。
好了,接下来便是利用 HttpPostRequestDecoder 来解析了,直接上代码:
HttpRequest request = (HttpRequest) msg;HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request, Charsets.toCharset(CharEncoding.UTF_8));List<InterfaceHttpData> datas = decoder.getBodyHttpDatas();for (InterfaceHttpData data : datas) { if(data.getHttpDataType() == HttpDataType.Attribute) { Attribute attribute = (Attribute) data; System.out.println(attribute.getName() + \"大众=\公众 + attribute.getValue()); }}
是不是很大略?没错。但是这里有点我要解释下, InterfaceHttpData 是一个interface,没有 API 可以直接拿到它的 value。那怎么办呢?莫方,在它的类内部定义了个列举类型,如下:
enum HttpDataType {
Attribute, FileUpload, InternalAttribute
}
这种情形下它是 Attribute 类型,因此你转换一下就能拿到值了。好奇的你可能会问,除 Attribute 外,其他两个是什么时候用呢?没错,接下来立时就讲 FileUpload,至于 InternalAttribute 嘛,老夫就不多说啦,有兴趣可以自己去研究了哈~
3.3.3 解析 multipart/form-data (文件上传)上面说到了 FileUpload,那在这里就来说说如何利用 netty HTTP 协议栈实现文件上传和保存功能。
这里依然利用 HttpPostRequestDecoder,废话就不多少了,直接上代码:
DiskFileUpload.baseDirectory = \"大众/data/fileupload/\"大众;
HttpRequest request = (HttpRequest) msg;
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request, Charsets.toCharset(CharEncoding.UTF_8));
List<InterfaceHttpData> datas = decoder.getBodyHttpDatas();
for (InterfaceHttpData data : datas) {
if(data.getHttpDataType() == HttpDataType.FileUpload) {
FileUpload fileUpload = (FileUpload) data;
String fileName = fileUpload.getFilename();
if(fileUpload.isCompleted()) {
//保存到磁盘
StringBuffer fileNameBuf = new StringBuffer();
fileNameBuf.append(DiskFileUpload.baseDirectory).append(fileName);
fileUpload.renameTo(new File(fileNameBuf.toString()));
}
}
至于效果,你可以直接在本地起个做事搞个大略的页面,向做事器传个文件就行了。如果你很
<form action=\公众http://localhost:8080\"大众 method=\"大众post\"大众 enctype =\公众multipart/form-data\"大众>
<input id=\"大众File1\公众 runat=\"大众server\"大众 name=\"大众UpLoadFile\"大众 type=\"大众file\"大众 />
<input type=\"大众submit\"大众 name=\公众Button\"大众 value=\"大众上传\公众 id=\公众Button\公众 />
</form>
至于其他类型的 Method、其他类型的 Content-Type,我也不打算细无年夜小逐一给大伙儿详细讲解了,看看上面罗列的,实在都很大略是不是?
上面说的都是 netty 自己实现的东西,下面就来讲讲如何实现一个大略的 HTTP decoder。
四、自定义 HTTP POST 的 message body 解码器关于解码器,我也不打算实现很繁芜很牛逼的,只是写了两个粗糙的 decoder,一个是带参数的一个是不带参数的。既然是浅析,那就下面就大略的聊聊。
如果你要实现一个顶层解码器,就要继续 MessageToMessageDecoder 并重写其 decode 方法。
MessageToMessageDecoder 继续了 ChannelHandlerAdapter,也便是说解码器实在便是一个 handler,只不过是专门用来做解码的事情。
下面我们来看看它重写的 channelRead 方法:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
RecyclableArrayList out = RecyclableArrayList.newInstance();
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings(\"大众unchecked\"大众)
I cast = (I) msg;
try {
decode(ctx, cast, out);
} finally {
ReferenceCountUtil.release(cast);
}
} else {
out.add(msg);
}
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
int size = out.size();
for (int i = 0; i < size; i ++) {
ctx.fireChannelRead(out.get(i));
}
out.recycle();
}
}
个中 decode 方法是你实现 decoder 时须要重写的,经由解码之后,会调用 ctx.fireChannelRead() 将 out 通报给给下一个 handler 实行干系逻辑。
4.1 HttpJsonDecoder从名字可以看出,这是个针对 message body 为 JsonString 的解码器。处理过程很大略,只须要把 HTTP 要求的 content (即 ByteBuf)的可读字节转换成 JSONObject 工具,如下:
@Override
protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) throws Exception {
FullHttpRequest fullRequest = (FullHttpRequest) msg;
ByteBuf content = fullRequest.content();
int length = content.readableBytes();
byte[] bytes = new byte[length];
for(int i=0; i<length; i++){
bytes[i] = content.getByte(i);
}
try{
JSONObject obj = JSON.parseObject(new String(bytes));
out.add(obj);
}catch(ClassCastException e){
throw new CodecException(\"大众HTTP message body is not a JSONObject\公众);
}
}
利用方法也很大略,在 Server 的 HttpServerCodec() 和 HttpObjectAggregator() 后面加上:
.addLast(\"大众jsonDecoder\公众, new HttpJsonDecoder())
然后在业务 handler channelRead方法中利用即可:
if(msg instanceof JSONObject){
JSONObject obj = (JSONObject) msg;
......
}
4.2 HttpProtobufDecoder这是一个带参数的 decoder,用来解析利用 protobuf 序列化后的 message body。利用的时候须要通报 MessageLite 进来,直接上代码:
private final MessageLite prototype;
public HttpProtobufDecoder(MessageLite prototype){
if (prototype == null) {
throw new NullPointerException(\公众prototype\"大众);
}
this.prototype = prototype.getDefaultInstanceForType();
}
@Override
protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) {
FullHttpRequest fullRequest = (FullHttpRequest) msg;
ByteBuf content = fullRequest.content();
int length = content.readableBytes();
byte[] bytes = new byte[length];
for(int i=0; i<length; i++){
bytes[i] = content.getByte(i);
}
try {
out.add(prototype.getParserForType().parseFrom(bytes, 0, length));
} catch (InvalidProtocolBufferException e) {
throw new CodecException(\"大众HTTP message body is not \公众 + prototype + \公众type\"大众);
}
}
利用方法跟 HttpJsonDecoder无异。此处以 protobuf 工具 UserProtobuf.User 为例,在 Server 的 HttpServerCodec() 和 HttpObjectAggregator() 后面加上:
.addLast(\"大众protobufDecoder\"大众, new HttpProtobufDecoder(UserProbuf.User.getDefaultInstance()))
然后在业务 handler channelRead方法中利用即可:
if(msg instanceof UserProbuf.User){
UserProbuf.User user = (UserProbuf.User) msg;
......
}
五、聊聊开拓中碰着的问题【推举】如果你没有亲自利用过 netty 却说自己熟习乃至精通 netty,我劝你千万别这么做,由于你的脸会被打肿的。netty 作为一个异步非壅塞的 IO 框架,它到底多牛逼在这就不多扯了,而作为一个首次利用 netty HTTP 协议栈的我来说,踩坑是必不可少的过程。当然了,踩了坑就要填上,我还很乐意在这把我踩过的几个坑给大家分享下,前车之鉴。
5.1 关于内存泄露首先说下经历的情形。在文章开篇提到的吸收做事,经由多轮的单元测试险些没创造什么问题,于是对付接下来的压力测试我是自傲满满。然而,当我第一次跑压测时就抛出一个非常,如下:
[ERROR] 2016-07-24 15:25:46 [io.netty.util.internal.logging.Slf4JLogger:176] - LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel() See http://netty.io/wiki/reference-counted-objects.html for more information.
其实让我愉快了一把,终于涌现非常了!
非常信息表达的是 “ByteBuf 在被 JVM GC 之前没有调用 ByteBuf.release() ,启用高等泄露报告,找出发生泄露的地方”,于是立时google了一把,原来是从 netty4 开始,工具的生命周期由它们的引用计数(reference counts)管理,而不是由垃圾网络器(garbage collector)管理了。
要办理这个问题,先从源头理解开始。
5.1.1 netty 引用计数工具【2】对付 netty Inbound message,当 event loop 读入了数据并创建了 ByteBuf,并用这个 ByteBuf 触发了一个 channelRead() 事宜时,那么管道(pipeline)中相应的ChannelHandler 就卖力开释这个 buffer 。因此,处理接数据的 handler 该当在它的 channelRead() 中调用 buffer 的 release(),如下:
public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; try { ... } finally { buf.release(); }}
而有时候,ByteBuf 会被一个 buffer holder 持有,它们都扩展了一个公共接口 ByteBufHolder。正因如此, ByteBuf 并不是 netty 中唯一一种引用计数工具。由 decoder 天生的工具很可能也是引用计数工具,比如 HTTP 协议栈中的 HttpContent,由于它也扩展了 ByteBufHolder。
public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; ... } if (msg instanceof HttpContent) { HttpContent content = (HttpContent) msg; try { ... } finally { content.release(); } }}
如果你抱有疑问,或者你想简化这些开释的事情,你可以利用 ReferenceCountUtil.release():
public void channelRead(ChannelHandlerContext ctx, Object msg) { try { ... } finally { ReferenceCountUtil.release(msg); }}
或者可以考虑继续 SimpleChannelHandler,它在所有吸收的地方都调用了 ReferenceCountUtil.release(msg)。
对付 netty Outbound message,你的程序所创建的工具都由 netty 卖力开释,开释的机遇是在这些被发送到网络之后。但是,在发送的过程中,如果有 handler 截获(intercept)了你的发送要求并创建了一些中间工具,则这些 handler 要确保精确开释这些中间工具。比如 encoder,此处不赘述。
通过以上信息,自然就很随意马虎找到 OOM 问题的缘故原由所在了。由于在处理 HTTP 要求过程中没有开释 ByteBuf,因此在代码 finally 块中加上 ReferenceCountUtil.release(msg) 就办理啦!
netty 供应了内存泄露的监测机制,默认就会从分配的 ByteBuf 里抽样出大约 1% 的来进行跟踪。如果泄露,就会打印5.1.1节中的非常信息,并提示你通过指定 JVM 选项
-Dio.netty.leakDetectionLevel=advanced
来查看泄露报告。泄露年监测有4个等级:
禁用(DISABLED) – 完备禁止透露检测,省点花费。大略(SIMPLE) – 默认等级,见告我们取样的 1% 的 ByteBuf 是否发生了透露,但统共一次只打印一次,看不到就没有了。高等(ADVANCED) – 见告我们取样的 1% 的 ByteBuf 发生透露的地方。每种类型的泄露(创建的地方与访问路径同等)只打印一次。偏执(PARANOID) – 跟高等选项类似,但此选项检测所有 ByteBuf,而不仅仅是取样的那 1%。在高压力测试时,对性能有明显影响。一样平常情形下我们采取 SIMPLE 级别即可。
5.2 关于 HTTP 长连接按照老例,先说下开拓中踩到的坑。
对付吸收做事,我采取的是 nginx + netty http,个中 nginx 配置如下(阉割隐蔽版):
upstream xxx.com{ keepalive 32; server xxxx.xx.xx.xx:8080;}server{ listen 80; server_name xxx.com; location / { proxy_next_upstream http_502 http_504 error timeout invalid_header; proxy_pass xxx.com; proxy_http_version 1.1; proxy_set_header Connection \"大众\公众; #proxy_set_header Host $host; #proxy_set_header X-Forwarded-For $remote_addr; #proxy_set_header REMOTE_ADDR $remote_addr; #proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; client_max_body_size 1m; } error_page 500 502 503 504 /50x.html; location = /50x.html{ root html; }}
然后编写了一个大略的 HttpClient 发送,如下(截取):
OutputStream outStream = conn.getOutputStream();outStream.write(data);outStream.flush();outStream.close(); if (conn.getResponseCode() == 200) { BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), \公众UTF-8\"大众)); String msg = in.readLine(); System.out.println(\公众msg = \"大众 + msg); in.close();}conn.disconnect();
接着,正常发送 HTTP 要求到做事器,然而,老夫整整等了60多秒才接到相应信息!
而且每次都这样!
!
我首先疑惑是不是 ngxin 出问题了,有一个配置项立马引起了我的疑惑,没错,便是上面赤色的那行 proxy_read_timeout 60s; 。为了验证,我首先把 60s 改成了 10s,效果很明显,发送的要求 10 秒过一点就收到相应了!
更加彻底证明是 nginx 的锅,我去掉了 nginx,让客户端直接发送要求给做事端。然而,蛋疼的事情涌现了,客户端竟然一贯壅塞在 BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) conn.getInputStream(), “UTF-8″)); 处。这解释根本就不是 nginx 的问题啊!
我镇静下来,review 了一下代码同时 search 了干系资料,创造了一个小小的差异,在我的返回代码中,对 ChannelFuture 少了对 CLOSE 事宜的监听器:
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
于是,我加上 Listener 再试一下,立时就得到相应了!
就在这一刻明白了这是 HTTP 长连接的问题。首先从上面的 nginx 配置中可以看到,我显式指定了 nginx 和 HTTP 做事器是用的 HTTP1.1 版本,HTTP1.1 版本默认是长连接办法(也便是 Connection=Keep-Alive),而我在 netty HTTP 做事器中并没有对长、短连接办法做差异处理,并且在 HttpResponse 相应中并没有显式加上 Content-Length 头部信息,适值 netty Http 协议栈并没有在框架上做这件事情,导致做事端虽然把相应发出去了,但是客户端并不知道你是否发送完成了(即没办法判断数据是否已经发送完)。
于是,把相应的处理完善一下即可:
/ 相应报文处理 @param channel 当前高下文Channel @param status 相应码 @param msg 相应 @param forceClose 是否逼迫关闭 /private void writeResponse(Channel channel, HttpResponseStatus status, String msg, boolean forceClose){ ByteBuf byteBuf = Unpooled.wrappedBuffer(msg.getBytes()); response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, byteBuf); boolean close = isClose(); if(!close && !forceClose){ response.headers().add(org.apache.http.HttpHeaders.CONTENT_LENGTH, String.valueOf(byteBuf.readableBytes())); } ChannelFuture future = channel.write(response); if(close || forceClose){ future.addListener(ChannelFutureListener.CLOSE); } }private boolean isClose(){ if(request.headers().contains(org.apache.http.HttpHeaders.CONNECTION, CONNECTION_CLOSE, true) || (request.protocolVersion().equals(HttpVersion.HTTP_1_0) && !request.headers().contains(org.apache.http.HttpHeaders.CONNECTION, CONNECTION_KEEP_ALIVE, true))) return true; return false;}
好了,问题是办理了,那么你对 HTTP 长连接真的理解吗?不理解,好,那就来不补课。
5.2.1 TCP KeepAlive 和 HTTP KeepAlive【4】netty 中有个地方比较让初学者迷惑,便是 childOption(ChannelOption.SO_KEEPALIVE, true)和 HttpRequest.Headers.get(“Connection”).equals(“Keep-Alive”) (非标准写法,仅作示例)的异同。有些人可能会问,我在 ServerBootstrap 中指定了 childOption(ChannelOption.SO_KEEPALIVE, true),是不是就意味着客户端和做事器是长连接了?
答案当然不是。
首先,TCP 的 KeepAlive 是 TCP 连接的探测机制,用来检测当前 TCP 连接是否活着。
它支持三个别系内核参数
tcp_keepalive_timetcp_keepalive_intvltcp_keepalive_probes当网络两端建立了 TCP 连接之后,闲置 idle(双方没有任何数据流发送往来)了 tcp_keepalive_time 后,做事器内核就会考试测验向客户端发送侦测包,来判断 TCP 连接状况(有可能客户端崩溃、逼迫关闭了运用、主机不可达等等)。如果没有收到对方的回答( ACK 包),则会在 tcp_keepalive_intvl 后再次考试测验发送侦测包,直到收到对对方的 ACK,如果一贯没有收到对方的 ACK,一共会考试测验 tcp_keepalive_probes 次,每次的间隔韶光在这里分别是 15s、30s、45s、60s、75s。如果考试测验 tcp_keepalive_probes,依然没有收到对方的 ACK 包,则会丢弃该 TCP 连接。TCP 连接默认闲置韶光是2小时。
而对付 HTTP 的 KeepAlive,则是让 TCP 连接活长一点,在一次 TCP 连接中可以持续发送多份数据而不会断开连接。
通过利用 keep-alive 机制,可以减少 TCP 连接建立次数,也意味着可以减少 TIME_WAIT 状态连接,以此提高性能和提高 TTTP 做事器的吞吐率(更少的 TCP 连接意味着更少的系统内核调用,socket 的 accept() 和 close() 调用)。
对付建立 HTTP 长连接的好处,总结如下【5】:
By opening and closing fewer TCP connections, CPU time is saved in routers and hosts (clients, servers, proxies, gateways, tunnels, or caches), and memory used for TCP protocol control blocks can be saved in hosts.HTTP requests and responses can be pipelined on a connection. Pipelining allows a client to make multiple requests without waiting for each response, allowing a single TCP connection to be used much more efficiently, with much lower elapsed time.Network congestion is reduced by reducing the number of packets caused by TCP opens, and by allowing TCP sufficient time to determine the congestion state of the network.Latency on subsequent requests is reduced since there is no time spent in TCP’s connection opening handshake.HTTP can evolve more gracefully, since errors can be reported without the penalty of closing the TCP connection. Clients using future versions of HTTP might optimistically try a new feature, but if communicating with an older server, retry with old semantics after an error is reported.5.2.2 长连接办法中如何判断数据发送完成【6】回到本节最开始提出的问题,KeepAlive 模式下,HTTP 做事器在发送完数据后并不会主动断开连接,那客户端如何判断数据发送完成了?
对付短连接办法,做事端在发送完数据后会断开连接,客户端过做事器关闭连接能确定的传输长度。(要求端不能通过关闭连接来指明要求体的结束,由于这样让做事器没有机会连续给予相应)。
但对付长连接办法,做事端只有在 Keep-alive timeout 或者达到 max 要求次数时才会断开连接。这种情形下有两种判断方法。
利用头部 Content-Length
Conent-Length 表示实体内容长度,客户端(或做事器)可以根据这个值来判断数据是否吸收完成。但是如果中没有 Conent-Length,那该如何来判断呢?又在什么情形下会没有 Conent-Length 呢?
利用首部字段 Transfer-Encoding
当要求或相应的内容是动态的,客户端或做事器无法预先知道要传输的数据大小时,就要利用 Transfer-Encoding(即 chunked 编码传输)。chunked 编码将数据分成一块一块的发送。chunked 编码将利用多少个chunk 串连而成,由一个标明长度为 0 的 chunk 标示结束。每个 chunk 分为头部和正文两部分,头部内容指定正文的字符总数(十六进制的数字)和数量单位(一样平常不写),正文部分便是指定长度的实际内容,两部分之间用回车换行(CRLF)隔开。在末了一个长度为 0 的 chunk 中的内容是称为footer的内容,是一些附加的Header信息(常日可以直接忽略)。
如果一个要求包含一个主体并且没有给出 Content-Length,那么做事器如果不能判断长度的话该当以400相应(Bad Request),或者以411相应(Length Required)如果它坚持想要收到一个有效的 Content-length。所有的能吸收实体的 HTTP/1.1 运用程序必须能接管 chunked 的传输编码,因此当的长度不能被提前确定时,可以利用这种机制来处理。不能同时都包括 Content-Length 头域和 非identity (Transfer-Encoding)传输编码。如果包括了一个 非identity 的传输编码,Content-Length头域必须被忽略。当 Content-Length 头域涌如今一个具有主体(message-body)的里,它的域值必须精确匹配主体里字节数量。
好了,本章较长,虽然不是很深奥难懂的知识,也不是很牛逼的技能实现,但是耐心看完之后相信你究竟是有所收成的。在此本文就要完结了,后续会对 netty HTTP 协议栈做更深入的研究,至于这个 github 上的项目,后面也会连续完善 TODO LIST。大家可以通过多种办法与我互换,并欢迎大家提出宝贵见地。