Netty搭建Http服务器
- HTTP服务器在我们日常开发中,常见的实现方式就是实现一个Java Web项目,基于Nginx+Tomcat的方式就可以提供HTTP服务。但是很多场景是非Web容器的场景,这个时候再使用Tomcat就大材小用了。这个时候就可以使用基于Netty的HTTP协议。而且基于Netty开发的HTTP服务器有如下优势:
- Netty的线程模型和异步非阻塞特性能够支持高并发
- 相比于Tomcat HTTP,Netty HTTP更加轻量、小巧、可靠,占用资源更少

HTTP 协议抽象

HTTP 协议抽象-请求

HTTP 协议抽象-响应

HTTP 协议抽象-Content-Length,chunked

HTTP 协议抽象-响应压缩

Netty HTTP 协议抽象的实现-请求和响应

HttpRequestDecoder & HttpObjectAggregator & HttpResponseEncoder

代码实现
服务端
HttpServerHandler
package com.bxg.netty.handler.server;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson.JSONObject;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.DiskFileUpload;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyHttpServerInBoundHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final HttpDataFactory HTTP_DATA_FACTORY = new DefaultHttpDataFactory(DefaultHttpDataFactory.MAXSIZE);
static {
DiskFileUpload.baseDirectory = "D:\netty\upload";
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws Exception {
HttpMethod type = httpRequest.method();
if (type.equals(HttpMethod.GET)) {
parseGet(httpRequest);
}
else if (type.equals(HttpMethod.POST)) {
parsePost(httpRequest);
}
else {
log.error("不支持的请求方式,{}", type);
}
StringBuilder sb = new StringBuilder();
sb.append("<html>");
sb.append("<head>");
sb.append("</head>");
sb.append("<body>");
sb.append("<h3>Success</h3>");
sb.append("</body>");
sb.append("</html>");
writeResponse(ctx, httpRequest, HttpResponseStatus.OK, sb.toString());
}
private void writeResponse(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, HttpResponseStatus status, String msg) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
response.content().writeBytes(msg.getBytes(StandardCharsets.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
HttpUtil.setContentLength(response, response.content().readableBytes());
boolean keepAlive = HttpUtil.isKeepAlive(fullHttpRequest);
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, "keep-alive");
}
ctx.writeAndFlush(response);
}
private void parseGet(FullHttpRequest httpRequest) {
parseKvStr(httpRequest.uri(), true);
}
private void parseKvStr(String uri, boolean hasPath) {
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri, StandardCharsets.UTF_8, hasPath);
Map<String, List<String>> parameters = queryStringDecoder.parameters();
parameters.entrySet().stream().forEach(item -> {
log.info("item:{}={}", item.getKey(), item.getValue());
});
}
private void parsePost(FullHttpRequest httpRequest) {
String contentType = getContentType(httpRequest);
switch (contentType) {
case "application/json":
parseJson(httpRequest);
break;
case "application/x-www-form-urlencoded":
parseFormData(httpRequest);
break;
case "multipart/form-data":
parseMultipart(httpRequest);
break;
default:
log.error("不支持的数据类型:{}", contentType);
break;
}
}
private void parseMultipart(FullHttpRequest httpRequest) {
HttpPostRequestDecoder httpPostRequestDecoder = new HttpPostRequestDecoder(HTTP_DATA_FACTORY, httpRequest);
if (httpPostRequestDecoder.isMultipart()) {
List<InterfaceHttpData> bodyHttpDatas = httpPostRequestDecoder.getBodyHttpDatas();
for (InterfaceHttpData dataItem : bodyHttpDatas) {
InterfaceHttpData.HttpDataType httpDataType = dataItem.getHttpDataType();
if (httpDataType.equals(InterfaceHttpData.HttpDataType.Attribute)){
Attribute attribute = (Attribute) dataItem;
try {
log.info("表单项名称:{},表单项值:{}",attribute.getName(),attribute.getValue());
} catch (IOException e) {
log.error("获取表单项数据错误,msg={}",e.getMessage());
}
} else if (httpDataType.equals(InterfaceHttpData.HttpDataType.FileUpload)){
FileUpload fileUpload = (FileUpload) dataItem;
String filename = fileUpload.getFilename();
String name = fileUpload.getName();
log.info("文件名称:{},表单项名称:{}",filename,name);
if (fileUpload.isCompleted()) {
try {
String path = DiskFileUpload.baseDirectory + File.separator + filename;
fileUpload.renameTo(new File(path));
} catch (IOException e) {
log.error("文件转存失败,msg={}",e.getMessage());
}
}
}
}
}
}
private void parseFormData(FullHttpRequest httpRequest) {
parseKvStr(httpRequest.uri(), true);
parseKvStr(httpRequest.content().toString(StandardCharsets.UTF_8), false);
}
private void parseJson(FullHttpRequest httpRequest) {
String jsonStr = httpRequest.content().toString(StandardCharsets.UTF_8);
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
jsonObject.entrySet().stream().forEach(item -> {
log.info("item:{}={}", item.getKey(), item.getValue());
});
}
private String getContentType(FullHttpRequest httpRequest) {
HttpHeaders headers = httpRequest.headers();
String contentType = headers.get(HttpHeaderNames.CONTENT_TYPE);
return contentType.split(";")[0];
}
}
HttpServer
package com.bxg.netty;
import com.bxg.netty.handler.server.MyHttpServerInBoundHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class HttpServer {
public static void main(String[] args) {
startServer(8888);
}
private static void startServer(int port) {
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss,worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpResponseEncoder());
pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpObjectAggregator(1024 * 1024 * 8));
pipeline.addLast(new MyHttpServerInBoundHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
http页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试 netty http server</title>
</head>
<body>
<form action="http://localhost:8888/test" method="post" enctype="multipart/form-data">
<label for="username">用户名:</label><input id="username" type="text" name="username"><br/>
<label for="password">密码:</label><input id="password" type="password" name="password"><br/>
<label for="email">邮箱:</label><input id="email" type="email" name="email"><br/>
<label for="address">地址:</label><input id="address" type="text" name="address"><br/>
<label for="pic">选择文件:</label><input id="pic" type="file" name="pic"><br/>
<input type="submit" value="提交">
</form>
</body>
</html>