Netty之Http
Http协议
HTTP是一个属于应用层的面向对象的协议,由于其使用简捷和快速的方式,非常适用于分布式超媒体信息系统。
HTTP协议特点
- 支持Server/Client模式;
简单——客户向服务器请求服务时,只需要指定服务的URL(统一资源定位符)并且携带对应的请求参数或消息体即可;
灵活——HTTP允许传输任意类型的数据对象,内容类型由HTTP消息头中的Content-Type来进行标记;
无状态——HTTP协议是无状态协议,也就是说该协议对于事务处理没有记忆能力。无状态意味着如果后续处理需要之前的信息,则必须重传,这样就会导致每次连接传送的数据量增大,另外一方面,在服务器不需要先前信息的时候它的应答就较大,负载较轻。
标示 | ASCII | 描述 | 字符 |
---|---|---|---|
CR | 13 | Carriage return (回车) | \n |
LF | 10 | Line feed character(换行) | \r |
SP | 32 | Horizontal space(空格) | |
COLON | 58 | COLON(冒号) : |
http协议主要使用CRLF进行分割。
请求包
三部分组成:
- 请求行(Line)
主要包含三部分:Method ,URI ,协议/版本。 各部分之间使用空格(SP)分割。整个请求头使用CRLF分割。(比如:POST /1.0.0/_health_check HTTP/1.1 CRLF)
- 请求头(header)
格式为(name :value),用于客户端请求的描述信息。header之间以CRLF进行分割。最后一个header会多加一个CRLF。( 比如:Connection: keep-alive CRLF CRLF)
- 请求正文
里面主要是Post提交的数据(可支持多种格式,格式在Content-Type定义,长度是在Content-Length里面定义)。
响应包
三部分组成:
- 状态行(line)
包含三部分:http版本,服务器返回状态码,描述信息。以CRLF进行分割。 ( 比如:HTTP/1.1 200 OK CRLF)
- 响应头(header)
格式为(name :value),用于服务器返回的描述信息。header之间以CRLF进行分割。最后一个header会多加一个CRLF (比如:Content-Type: text/html CRLF Content-Encoding:gzip CRLF CRLF)
- 响应正文(body)
里面主要是返回数据(可支持多种格式,格式在Content-Type定义,长度是在Content-Length里面定义)。
chunked
HTTP协议通常使用Content-Length来标识body的长度,在服务器端,需要先申请对应长度的buffer,然后再赋值。如果需要一边生产数据一边发送数据,就需要使用"Transfer-Encoding: chunked" 来代替Content-Length,也就是对数据进行分块传输。
Content-Length
1:http server接收数据时,发现header中有Content-Length属性,则读取Content-Length 的值,确定需要读取body的长度。
2:http server发送数据时,根据需要发送byte的长度,在header中增加 Content-Length 项,其中value为byte的长度,然后将byte 数据当做body发送到客户端。
chunked
1:http server接收数据时,发现header中有Transfer-Encoding: chunked,则会按照truncked协议分批读取数据。
2:http server发送数据时,如果需要分批发送到客户端,则需要在header中加上 Transfer-Encoding: chunked,然后按照truncked协议分批发送数据
truncked协议
主要三部分:
- chunk主要包含大小和数据,大小表示这个这个trunck包的大小,使用16进制标示。其中trunk之间的分隔符为CRLF。
通过last-chunk来标识chunk发送完成。 一般读取到last-chunk(内容为0)的时候,代表chunk发送完成。
trailer 表示增加header等额外信息,一般情况下header是空。通过CRLF来标识整个chunked数据发送完成。
优点
1:假如body的长度是10K,对于Content-Length则需要申请10K连续的buffer,而对于Transfer-Encoding: chunked可以申请1k的空间,然后循环使用10次。节省了内存空间的开销。
2:如果内容的长度不可知,则可使用trunked方式能有效的解决Content-Length的问题
- 3:http服务器压缩可以采用分块压缩,而不是整个快压缩。分块压缩可以一边进行压缩,一般发送数据,来加快数据的传输时间。
缺点
1:truncked 协议解析比较复杂。
2:在http转发的场景下(比如nginx) 难以处理,比如如何对分块数据进行转发。
http针对拆包粘包解决方案
1:请求行的边界是CRLF,如果读取到CRLF,则意味着请求行的信息已经读取完成。
2:Header的边界是CRLF,如果连续读取两个CRLF,则意味着header的信息读取完成。
- 3:body的长度是有Content-Length 来进行确定。如果没有Content-Length ,则是chunked协议
Netty对Http的处理
对请求request的抽象:
HttpMethod:主要是对method的封装,包含method序列化的操作
HttpVersion: 对version的封装,netty包含1.0和1.1的版本
QueryStringDecoder: 主要是对url进行封装,解析path和url上面的参数。(Tips:在tomcat中如果提交的post请求是application/x-www-form-urlencoded,则getParameter获取的是包含url后面和body里面所有的参数,而在netty中,获取的仅仅是url上面的参数)
HttpHeaders:包含对header的内容进行封装及操作
HttpContent:是对body进行封装,本质上就是一个ByteBuf。如果ByteBuf的长度是固定的,则请求的body过大,可能包含多个HttpContent,其中最后一个为LastHttpContent(空的HttpContent),用来说明body的结束。
HttpRequest:主要包含对Request Line和Header的组合
FullHttpRequest: 主要包含对HttpRequest 和httpContent 的组合
request处理流程
只需要在netty 的pipeLine 中配置 HttpRequestDecoder 和HttpObjectAggregator。
1:如果把解析这块理解是一个黑盒的话,则输入是ByteBuf,输出是FullHttpRequest。通过该对象便可获取到所有与http协议有关的信息。
2:HttpRequestDecoder 先通过 RequestLine 和Heade解析成HttpRequest对象,传入到HttpObjectAggregator。然后再通过body解析出httpConten对象,传入到HttpObjectAggregator。当HttpObjectAggregator 发现是LastHttpContent ,则代表http协议解析完成,封装FullHttpRequest。
- 3:对于body内容的读取涉及到Content-Length和trunked两种方式。两种方式只是在解析协议时处理的不一致,最终输出是一致的。
HttpObjectAggregator
public class HttpObjectAggregator
extends MessageAggregator<HttpObject,HttpMessage,HttpContent,FullHttpMessage>
一个ChannelHandler,它将HttpMessage及其后续的HttpContents聚合为一个没有后续HttpContents的单个FullHttpRequest或FullHttpResponse(取决于它是否用于处理请求或响应)。 当您不想处理传输编码为“块状”的HTTP消息时,此功能很有用。 如果用于处理响应,则将此处理程序插入到ChannelPipeline中的HttpResponseDecoder之后;如果用于处理请求,则将其插入到ChannelPipeline中的HttpRequestDecoder和HttpResponseEncoder之后。
ChannelPipeline p = ...;
...
p.addLast("decoder", new HttpRequestDecoder());
p.addLast("encoder", new HttpResponseEncoder());
p.addLast("aggregator", new HttpObjectAggregator(1048576));
...
p.addLast("handler", new HttpRequestHandler());
为了方便起见,请考虑将HttpServerCodec放在HttpObjectAggregator之前,因为它既充当HttpRequestDecoder,又充当HttpResponseEncoder。
请注意,HttpObjectAggregator可能最终会发送HttpResponse:
Response Status | Condition When Sent |
---|---|
100 Continue | A '100-continue' expectation is received and the 'content-length' doesn't exceed maxContentLength |
417 Expectation Failed | A '100-continue' expectation is received and the 'content-length' exceeds maxContentLength |
413 Request Entity Too Large | Either the 'content-length' or the bytes received so far exceed maxContentLength |
构造函数:
//maxContentLength-聚合内容的最大长度(以字节为单位)。 如果聚合内容的长度超过此值,则将调用handleOversizedMessage(ChannelHandlerContext,HttpMessage)。
public HttpObjectAggregator(int maxContentLength)
QueryStringDecoder
public class QueryStringDecoder
extends java.lang.Object
将HTTP查询字符串拆分为路径字符串和键值参数对。 该解码器仅供一次使用。 为每个URI创建一个新实例:
QueryStringDecoder decoder = new QueryStringDecoder("/hello?recipient=world&x=1;y=2");
assert decoder.path().equals("/hello");
assert decoder.parameters().get("recipient").get(0).equals("world");
assert decoder.parameters().get("x").get(0).equals("1");
assert decoder.parameters().get("y").get(0).equals("2");
该解码器还可以解码HTTP POST请求的内容,其内容类型为application / x-www-form-urlencoded:
QueryStringDecoder decoder = new QueryStringDecoder("recipient=world&x=1;y=2", false);
例子
客户端发送http请求,服务端响应
public class HttpServer {
public void bind(int port) {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(61024));
pipeline.addLast(new HttpServerHandler());
}
});
ChannelFuture future = sb.bind(port).sync();
System.out.println("Server...............................");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new HttpServer().bind(9921);
}
}
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
private static final Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
// 获取请求头
HttpHeaders httpHeaders = msg.headers();
// System.out.println(httpHeaders.toString());
// 查看请求头的详细信息
logger.info(httpHeaders.toString());
logger.info("*****************************");
// 打印uri
logger.info(msg.uri());
// 获取Content-Type信息
String strContentType = httpHeaders.get("Content-Type").trim();
Map<String, Object> mapReturnData = new HashMap<String, Object>();
System.out.println("ContentType:" + strContentType);
// 处理get请求
QueryStringDecoder decoder = new QueryStringDecoder(
msg.getUri());
if (msg.getMethod() == HttpMethod.GET) {
Map<String, List<String>> get_parame = decoder.parameters();
System.out.println("GET方式:" + get_parame.toString());
response_message(ctx);
} else if (msg.getMethod() == HttpMethod.POST) {
if (strContentType.contains("application/json")) {
ByteBuf content = msg.content();
Map<String, List<String>> post_parame = decoder.parameters();
System.out.println("POST方式:" + post_parame.toString());
if (content.capacity() != 0 ) {
byte[] resquestContent = new byte[content.readableBytes()];
content.readBytes(resquestContent);
String strContent = new String(resquestContent, "UTF-8");
JSONObject jsonParamRoot = JSONObject.parseObject(strContent);
for (String key : jsonParamRoot.keySet()) {
mapReturnData.put(key, jsonParamRoot.get(key));
}
}
}
// logger.info("POST: " + mapReturnData.toString());
}
}
// 服务端回应数据
private void response_message(ChannelHandlerContext ctx) {
// 构造返回数据
JSONObject jsonRootObj = new JSONObject();
JSONObject jsonUserInfo = new JSONObject();
jsonUserInfo.put("id", 1);
jsonUserInfo.put("name", "张三");
jsonUserInfo.put("password", "123");
jsonRootObj.put("userInfo", jsonUserInfo);
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
response.headers().set(CONTENT_TYPE, "application/json; charset=UTF-8");
StringBuilder bufRespose = new StringBuilder();
bufRespose.append(jsonRootObj.toJSONString());
ByteBuf buffer = Unpooled.copiedBuffer(bufRespose, CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
PostMan测试: