目录
- 开发实战
- 1. 使用echo服务器模拟http
- 2. netty http核心类
- 3. 服务端
- 4. 客户端
- 总结和源码
- 参考
开发实战
1. 使用echo服务器模拟http
通过上一篇文章中的echo服务器程序来模拟一次HTTP请求。
接收消息的代码如下:
public class ServerStringHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("服务端接收到消息:" + msg);
ctx.writeAndFlush(msg);
}
}
我们通过postman直接访问echo服务器:
请求成功,echo服务器接收到了本次HTTP请求,控制台打印内容如下:
服务端接收到消息:GET / HTTP/1.1
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Postman-Token: b340a7ba-bf85-48a7-97af-0bae5e94750e
Host: localhost:8001
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
上面的原理很容易理解,postman通过tcp建立与服务器localhost:8001的连接,然后自己组装了HTTP request消息,然后发送给echo服务器,echo服务器拿到完整的内容后将其打印在控制台,随后返回一条文本数据。
也正是echo服务器返回了一条文本数据,并未组装HTTP response消息,导致postman并未识别出服务器返回的内容。
这里简单提一下HTTP协议:
超文本传输协议(HyperText Transfer Protocol,HTTP)协议属于七(四)层协议中的应用层协议。HTTP协议其实是客户端和服务端之间请求和应答的标准,它规定了每次请求或返回的标准格式。基于HTTP对消息传输的顺序性和稳定性要求的前提下,HTTP协议一般使用TCP协议进行网络传输,路由寻址依旧是IP协议。
HTTP协议的消息格式
(HTTP Messages):
- Start line CRLF:request|response的起始栏
- n * (header CRLF):消息头,以key: value 形式组装,末尾跟上回车换行,最终构成的消息头
- CRLF:空行用于区分消息头和消息体。
- Body:消息体
了解完HTTP协议之后,我们通过如下格式构建HTTP Response消息:
- Start line格式:HTTP-Version SP Status-Code SP Reason-Phrase CRLF
- Response Header格式:KEY: VALUE CRLF
- CRLF
- Response Body格式:data
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("服务端接收到消息:" + msg);
String message = "HTTP/1.1 200 OK\n" +
"Content-Length: 35\n" +
"Date: " + new Date() + "\n" +
"Connection: keep-alive\n" +
"Content-Type: text/plain\n" +
"\n" +
"Reply, This is reply from server-.^";
System.out.println(message);
ctx.writeAndFlush(message);
}
重启后再次请求,postman成功识别出了我们拼接的结果:
通过上述的模拟实验,相信你已经大致理解了HTTP运作的流程。所以,我们要实现HTTP客户端,只需要自行拼凑出HTTP request内容;要实现HTTP服务端,只需要接收和解析request,并根据结果返回response即可。
听起来很简单,但是如果我们要自己来实现HTTP通信,处理各种请求头、cookie、消息体以及压缩算法等等,那么这份工作量过于巨大,所幸netty提供了完整的HTTP协议请求和接收的封装处理。通过使用netty-codec-http
包中的内容,我们就可以轻松的进行HTTP解析和处理工作。
<!-- netty-all包含以下两个依赖 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<!-- 处理HTTP的请求、返回的消息发送和接收 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
</dependency>
<!-- 处理HTTP/2框架下的消息发送和接收 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http2</artifactId>
</dependency>
2. netty http核心类
为了更好的理解netty处理HTTP收发的机制,我们有必要先了解
netty-codec-http
包中的HTTP核心类。
HTTP消息相关类:
- HttpObject:HTTP对象,是HTTP消息的顶层接口。
- HttpMessage:HTTP消息的接口定义,提供HttpRequest和HttpResponse的共用属性,如协议版本
HttpVersion
和请求头HttpHeaders
,默认实现类DefaultHttpMessage
。 - HttpContent:HTTP消息体,用于存储body内容,默认实现类
DefaultHttpContent
。在进行大文件传输或消息头参数有Transfer-Encoding:chunked
时使用,消息体将会进行分块传输编码(Chunked transfer encoding)技术,如果有需要可以对消息体会划分多个HttpContent块(0-N个块),最后总是以LastHttpContent
作为分块传输的结束标识,它的块大小为0,实现类参考DefaultLastHttpContent
。 - HttpRequest:HTTP请求,提供访问和设置请求URI、method和cookie的编码解码等信息,默认实现类
DefaultHttpRequest
。 - HttpResponse:HTTP响应,提供设置返回状态码、版本协议等内容,默认实现类
DefaultHttpResponse
。 - FullHttpMessage:HttpMessage和HttpContent的组合,在抽象定义上,它就代表了整个HTTP消息。
- FullHttpRequest:FullHttpMessage和HttpRequest的组合,代表一个完整的HTTP请求,参考
DefaultFullHttpRequest
。 - FullHttpResponse:FullHttpMessage和HttpResponse的组合,代表一个完整的HTTP响应,参考
DefaultFullHttpResponse
。
FullHttpRequest和FullHttpResponse消息的封装情况如下所示:
netty处理器相关类:
- HttpObjectDecoder:入站处理器,将字节流解析为HttpMessage、HttpContent(如果有的话)。
HttpRequestDecoder
和HttpResponseDecoder
是其子类。作用是将字节流解析为HttpRequest / HttpResponse、HttpContent。 - HttpObjectEncoder:出站处理器,将HttpMessage和HttpContent(如果有的话)转为字节流。
HttpRequestEncoder
和HttpResponseEncoder
是其子类。作用是将HttpRequest / HttpResponse和HttpContent转为字节流。 - HttpClientCodec:客户端HTTP消息处理器,是HttpRequestEncoder与HttpResponseDecoder的组合。
- HttpServerCodec:服务器HTTP消息处理器,是HttpRequestDecoder与HttpResponseEncoder的组合。
3. 服务端
有了上面的理论和实践,要实现一个可用的HTTP已经是非常简单的操作了。这里我们只需根据request请求来生成response即可。
我们新建一个处理器ServerHttpMessageHandler
,它的作用是接收request、创建response设置状态码和消息体:“Hello World”。
代码如下:
public class HttpServerRunner {
private int port;
public HttpServerRunner(Integer port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
HttpServerRunner runner = new HttpServerRunner(8002);
runner.start();
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new LoggingHandler(LogLevel.DEBUG),
new HttpServerCodec(),
new ServerHttpMessageHandler()
);
}
});
// 绑定监听服务端口,并开始接收进来的连接
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
ServerHttpMessageHandler中我们是用HttpRequest来接收HttpMessage,如果要接收消息体HttpContent的内容,需要再建一个if分支语句。这是因为netty在读取消息的时候,它并不会把消息直接转为FullHttpRequest,而是将其划为两个部分:HttpMessage和HttpContent,所以channelRead0将会读取两次以上(HttpMessage读取一次、HttpContent读取0次或多次(分块时)、LastHttpContent读取一次)。
public class ServerHttpMessageHandler extends SimpleChannelInboundHandler<HttpObject> {
private static final byte[] CONTENT = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest) msg;
boolean keepAlive = HttpUtil.isKeepAlive(request);
// 返回http信息
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, Unpooled.wrappedBuffer(CONTENT));
// 设置请求头
response.headers()
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
.setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
// 是否长连接
if (keepAlive) {
if (!request.protocolVersion().isKeepAliveDefault()) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
} else {
// 本次传输完毕后断开连接
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
ChannelFuture f = ctx.writeAndFlush(response);
if (!keepAlive) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
通过postman测试一下:
HTTP服务器流程验证成功!
不过这个服务器只要是个HTTP请求我们就会返回响应,因为我们并未对method、uri、header和body等做处理。
接下来就是构建客户端HTTP请求了。
4. 客户端
客户端这里有两个处理器:
- ClientMessageToHttpHandler:将客户端发送的字符串封装为HTTP请求,并发送给服务端。
- ClientHttpReadHandler:接收和解析服务器的响应数据。
public class HttpClientRunner {
private String host;
private Integer port;
public HttpClientRunner(String host, Integer port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception {
HttpClientRunner client = new HttpClientRunner("127.0.0.1", 8002);
client.start();
}
public void start() throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new HttpClientCodec(), new ClientMessageToHttpHandler(), new ClientHttpReadHandler());
}
});
// 创建一个连接
ChannelFuture f = b.connect(host, port).sync();
// 创建连接后手动发送一个请求
f.channel().writeAndFlush("Hello!");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
ClientMessageToHttpHandler:
public class ClientMessageToHttpHandler extends MessageToMessageEncoder<String> {
private static final byte[] CONTENT = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
@Override
protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception {
FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/get", Unpooled.wrappedBuffer(CONTENT));
// 消息发送完毕后关闭连接
httpRequest.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
ctx.writeAndFlush(httpRequest);
}
}
ClientHttpReadHandler,因为TCP的消息顺序性,我们可以保证每次读取HttpContent前,HttpResponse是已经接收完毕的。
public class ClientHttpReadHandler extends SimpleChannelInboundHandler<HttpObject> {
private HttpResponse request;
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ChannelFuture sync = ctx.close().sync();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpResponse) {
request = (HttpResponse) msg;
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
String length = request.headers().get(HttpHeaderNames.CONTENT_LENGTH);
ByteBuf byteBuf = content.content();
CharSequence charSequence = byteBuf.getCharSequence(0, Integer.parseInt(length), StandardCharsets.UTF_8);
System.out.println(charSequence);
}
}
}
总结和源码
本文简单介绍了HTTP协议相关知识,然后在netty代码中实现HTTP消息的接收发送。服务端客户端的功能较为简单,很多服务器功能并未实现,如地址、参数、请求方法的解析,请求头、cookie等验证,消息体接收、分块消息处理,DNS解析,HTTPS消息的处理,文件流上传以及接收,HTTP消息压缩解压处理,跨域问题等等。所以本篇文章只是netty-HTTP的入门学习文章。后续有时间或者要求会再深入学习一下netty中关于HTTP的更多知识。
源码地址:netty-demo
参考
HTTP Messages
Transfer-Encoding
分块传输编码