目前正在写的一个叫做 jsoncat 的轻量级 HTTP 框架内置的 HTTP 服务器是我自己基于 Netty 写的,所有的核心代码加起来不过就几十行。这得益于 Netty 提供的各种开箱即用的组件,为我们节省了太多事情。 这篇文章我会手把手带着小伙伴们实现一个简易的 HTTP Server。 如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步! 开始之前为了避免有小伙伴不了解 Netty ,还是先来简单介绍它! 什么是 Netty?简单用 3 点来概括一下 Netty 吧!
Netty 应用场景有哪些?凭借自己的了解,简单说一下吧!理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。 不过,我们还是首先要明确的是 Netty 主要用来做网络通信 。
那些开源项目用到了 Netty?我们平常经常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 、Spring Cloud Gateway 等等都用到了 Netty。 可以说大量的开源项目都用到了 Netty,所以掌握 Netty 有助于你更好的使用这些开源项目并且让你有能力对其进行二次开发。 实际上还有很多很多优秀的项目用到了 Netty,Netty 官方也做了统计,统计结果在这里:https:///wiki/related-projects.html 。 实现 HTTP Server 必知的前置知识既然,我们要实现 HTTP Server 那必然先要回顾一下 HTTP 协议相关的基础知识。 HTTP 协议超文本传输协议(HTTP,HyperText Transfer Protocol)主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。 当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的,整个过程如下图所示。 https://www./en/wiki/HTTP_headers HTTP 协议是基于 TCP 协议的,因此,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。 了解了 HTTP 协议之后,我们再来看一下 HTTP 报文的内容,这部分内容很重要!(参考图片来自:https://iamgopikrishna./2014/06/13/4/) HTTP 请求报文: HTTP 响应报文: 我们的 HTTP 服务器会在后台解析 HTTP 请求报文内容,然后根据报文内容进行处理之后返回 HTTP 响应报文给客户端。 Netty 编解码器如果我们要通过 Netty 处理 HTTP 请求,需要先进行编解码。所谓编解码说白了就是在 Netty 传输数据所用的 Netty 自带了 4 个常用的编解码器:
网络通信最终都是通过字节流进行传输的。 HTTP Server 端用于接收 HTTP Request,然后发送 HTTP Response。因此我们只需要 我手绘了一张图,这样看着应该更容易理解了。 Netty 对 HTTP 消息的抽象为了能够表示 HTTP 中的各种消息,Netty 设计了抽象了一套完整的 HTTP 消息结构图,核心继承关系如下图所示。
HTTP 消息聚合器
另外,消息体比较大的话,可能还会分成好几个消息体来处理, 使用方法:将 因为,HTTP Server 端用于接收 HTTP Request,对应的使用方式如下。 ChannelPipeline p = ...; p.addLast("decoder", new HttpRequestDecoder()) .addLast("encoder", new HttpResponseEncoder()) .addLast("aggregator", new HttpObjectAggregator(512 * 1024)) .addLast("handler", new HttpServerHandler()); 基于 Netty 实现一个 HTTP Server通过 Netty,我们可以很方便地使用少量代码构建一个可以正确处理 GET 请求和 POST 请求的轻量级 HTTP Server。 源代码地址:https://github.com/Snailclimb/netty-practical-tutorial/tree/master/example/http-server 。 添加所需依赖到 pom.xml第一步,我们需要将实现 HTTP Server 所必需的第三方依赖的坐标添加到 <!--netty--> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.42.Final</version> </dependency> <!-- log --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope> </dependency> <!--commons-codec--> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.14</version> </dependency> 创建服务端@Slf4j public class HttpServer { private static final int PORT = 8080; public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // TCP默认开启了 Nagle 算法,该算法的作用是尽可能的发送大数据快,减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。 .childOption(ChannelOption.TCP_NODELAY, true) // 是否开启 TCP 底层心跳机制 .childOption(ChannelOption.SO_KEEPALIVE, true) //表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数 .option(ChannelOption.SO_BACKLOG, 128) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast("decoder", new HttpRequestDecoder()) .addLast("encoder", new HttpResponseEncoder()) .addLast("aggregator", new HttpObjectAggregator(512 * 1024)) .addLast("handler", new HttpServerHandler()); } }); Channel ch = b.bind(PORT).sync().channel(); log.info("Netty Http Server started on port {}.", PORT); ch.closeFuture().sync(); } catch (InterruptedException e) { log.error("occur exception when start server:", e); } finally { log.error("shutdown bossGroup and workerGroup"); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } 简单解析一下服务端的创建过程具体是怎样的! 1.创建了两个
举个例子:我们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在外面接完活之后,扔给 workerGroup 去处理。一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为 CPU 核心数 *2 。另外,根据源码来看,使用 2.创建一个服务端启动引导/辅助类: 3.通过 4.通过
5.通过 6.调用 //bind()是异步的,但是,你可以通过 sync()方法将其变为同步。 ChannelFuture f = b.bind(port).sync(); 自定义服务端 ChannelHandler 处理 HTTP 请求我们继承
另外,客户端 HTTP 请求参数类型为 @Slf4j public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final String FAVICON_ICO = "/favicon.ico"; private static final AsciiString CONNECTION = AsciiString.cached("Connection"); private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive"); private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type"); private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length"); @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) { log.info("Handle http request:{}", fullHttpRequest); String uri = fullHttpRequest.uri(); if (uri.equals(FAVICON_ICO)) { return; } RequestHandler requestHandler = RequestHandlerFactory.create(fullHttpRequest.method()); Object result; FullHttpResponse response; try { result = requestHandler.handle(fullHttpRequest); String responseHtml = "<html><body>" + result + "</body></html>"; byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8); response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes)); response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8"); response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); } catch (IllegalArgumentException e) { e.printStackTrace(); String responseHtml = "<html><body>" + e.toString() + "</body></html>"; byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8); response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, Unpooled.wrappedBuffer(responseBytes)); response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8"); } boolean keepAlive = HttpUtil.isKeepAlive(fullHttpRequest); if (!keepAlive) { ctx.write(response).addListener(ChannelFutureListener.CLOSE); } else { response.headers().set(CONNECTION, KEEP_ALIVE); ctx.write(response); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } } 我们返回给客户端的消息体是 我们可以把 FullHttpResponse response; String responseHtml = "<html><body>" + result + "</body></html>"; byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8); // 初始化 FullHttpResponse ,并设置 HTTP 协议 、响应状态码、响应的具体内容 response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes)); 我们通过 response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8"); response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); 本案例中,为了掩饰我们设置的 Content-Type 为 常见的 Content-Type
请求的具体处理逻辑实现因为有这里有 POST 请求和 GET 请求。因此我们需要首先定义一个处理 HTTP Request 的接口。 public interface RequestHandler { Object handle(FullHttpRequest fullHttpRequest); } HTTP Method 不只是有 GET 和 POST,其他常见的还有 PUT、DELETE、PATCH。只是本案例中实现的 HTTP Server 只考虑了 GET 和 POST。
GET 请求的处理@Slf4j public class GetRequestHandler implements RequestHandler { @Override public Object handle(FullHttpRequest fullHttpRequest) { String requestUri = fullHttpRequest.uri(); Map<String, String> queryParameterMappings = this.getQueryParams(requestUri); return queryParameterMappings.toString(); } private Map<String, String> getQueryParams(String uri) { QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8)); Map<String, List<String>> parameters = queryDecoder.parameters(); Map<String, String> queryParams = new HashMap<>(); for (Map.Entry<String, List<String>> attr : parameters.entrySet()) { for (String attrVal : attr.getValue()) { queryParams.put(attr.getKey(), attrVal); } } return queryParams; } } 我这里只是简单得把 URI 的查询参数的对应关系直接返回给客户端了。 实际上,获得了 URI 的查询参数的对应关系,再结合反射和注解相关的知识,我们很容易实现类似于 Spring Boot 的 建议想要学习的小伙伴,可以自己独立实现一下。不知道如何实现的话,你可以参考我开源的轻量级 HTTP 框架jsoncat (仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架)。 POST 请求的处理@Slf4j public class PostRequestHandler implements RequestHandler { @Override public Object handle(FullHttpRequest fullHttpRequest) { String requestUri = fullHttpRequest.uri(); log.info("request uri :[{}]", requestUri); String contentType = this.getContentType(fullHttpRequest.headers()); if (contentType.equals("application/json")) { return fullHttpRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8)); } else { throw new IllegalArgumentException("only receive application/json type data"); } } private String getContentType(HttpHeaders headers) { String typeStr = headers.get("Content-Type"); String[] list = typeStr.split(";"); return list[0]; } } 对于 POST 请求的处理,我们这里只接受处理 Content-Type 为 实际上,我们获得了客户端传来的 json 格式的数据之后,再结合反射和注解相关的知识,我们很容易实现类似于 Spring Boot 的 建议想要学习的小伙伴,可以自己独立实现一下。不知道如何实现的话,你可以参考我开源的轻量级 HTTP 框架jsoncat (仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架)。 请求处理工厂类public class RequestHandlerFactory { public static final Map<HttpMethod, RequestHandler> REQUEST_HANDLERS = new HashMap<>(); static { REQUEST_HANDLERS.put(HttpMethod.GET, new GetRequestHandler()); REQUEST_HANDLERS.put(HttpMethod.POST, new PostRequestHandler()); } public static RequestHandler create(HttpMethod httpMethod) { return REQUEST_HANDLERS.get(httpMethod); } } 我这里用到了工厂模式,当我们额外处理新的 HTTP Method 方法的时候,直接实现 启动类public class HttpServerApplication { public static void main(String[] args) { HttpServer httpServer = new HttpServer(); httpServer.start(); } } 效果运行 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a] REGISTERED [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a] BIND: 0.0.0.0/0.0.0.0:8080 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a, L:/0:0:0:0:0:0:0:0:8080] ACTIVE [main] INFO server.HttpServer - Netty Http Server started on port 8080. GET 请求POST 请求参考我的开源项目推荐
|
|