关于spring:手写一个类SpringBoot的HTTP框架几十行代码基于Netty搭建一个-HTTP-Server

25次阅读

共计 13607 个字符,预计需要花费 35 分钟才能阅读完成。

本文曾经收录进 : https://github.com/Snailclimb/netty-practical-tutorial (Netty 从入门到实战:手写 HTTP Server+RPC 框架)。
相干我的项目:https://github.com/Snailclimb/jsoncat(仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架)

目前正在写的一个叫做 jsoncat 的轻量级 HTTP 框架内置的 HTTP 服务器是我本人基于 Netty 写的,所有的外围代码加起来不过就几十行。这得益于 Netty 提供的各种开箱即用的组件,为咱们节俭了太多事件。

这篇文章我会手把手带着小伙伴们实现一个繁难的 HTTP Server。

如果文章有任何须要改善和欠缺的中央,欢送在评论区指出,共同进步!

开始之前为了防止有小伙伴不理解 Netty,还是先来简略介绍它!

什么是 Netty?

简略用 3 点来概括一下 Netty 吧!

  1. Netty 是一个基于 NIO 的 client-server(客户端服务器)框架,应用它能够疾速简略地开发网络应用程序。
  2. Netty 极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程, 并且性能以及安全性等很多方面都要更好。
  3. Netty 反对多种协定 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协定。本文所要写的 HTTP Server 就得益于 Netty 对 HTTP 协定(超文本传输协定)的反对。

Netty 利用场景有哪些?

凭借本人的理解,简略说一下吧!实践上来说,NIO 能够做的事件,应用 Netty 都能够做并且更好。

不过,咱们还是首先要明确的是 Netty 次要用来做 网络通信

  1. 实现框架的网络通信模块:Netty 简直满足任何场景的网络通信需要,因而,框架的网络通信模块能够基于 Netty 来做。拿 RPC 框架来说!咱们在分布式系统中,不同服务节点之间常常须要互相调用,这个时候就须要 RPC 框架了。不同服务指导的通信是如何做的呢?那就能够应用 Netty 来做了!比方我调用另外一个节点的办法的话,至多是要让对方晓得我调用的是哪个类中的哪个办法以及相干参数吧!
  2. 实现一个本人的 HTTP 服务器:通过 Netty,咱们能够很不便地应用大量代码实现一个简略的 HTTP 服务器。Netty 自带了编解码器和音讯聚合器,为咱们开发节俭了很多事!
  3. 实现一个即时通讯零碎:应用 Netty 咱们能够实现一个能够聊天相似微信的即时通讯零碎,这方面的开源我的项目还蛮多的,能够自行去 Github 找一找。
  4. 实现音讯推送零碎:市面上有很多音讯推送零碎都是基于 Netty 来做的。
  5. ……

那些开源我的项目用到了 Netty?

咱们平时常常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC、Spring Cloud Gateway 等等都用到了 Netty。

能够说大量的开源我的项目都用到了 Netty,所以把握 Netty 有助于你更好的应用这些开源我的项目并且让你有能力对其进行二次开发。

实际上还有很多很多优良的我的项目用到了 Netty,Netty 官网也做了统计,统计后果在这里:https://netty.io/wiki/related…。

实现 HTTP Server 必知的前置常识

既然,咱们要实现 HTTP Server 那必然先要回顾一下 HTTP 协定相干的基础知识。

HTTP 协定

超文本传输协定(HTTP,HyperText Transfer Protocol)次要是为 Web 浏览器与 Web 服务器之间的通信而设计的。

当咱们应用浏览器浏览网页的时候,咱们网页就是通过 HTTP 申请进行加载的,整个过程如下图所示。

<p style=”text-align:right;font-size:12px”>https://www.seobility.net/en/…</p>

HTTP 协定是基于 TCP 协定的,因而,发送 HTTP 申请之前首先要建设 TCP 连贯也就是要经验 3 次握手。目前应用的 HTTP 协定大部分都是 1.1。在 1.1 的协定外面,默认是开启了 Keep-Alive 的,这样的话建设的连贯就能够在屡次申请中被复用了。

理解了 HTTP 协定之后,咱们再来看一下 HTTP 报文的内容,这部分内容很重要!(参考图片来自:https://iamgopikrishna.wordpress.com/2014/06/13/4/)

HTTP 申请报文:

HTTP 响应报文:

咱们的 HTTP 服务器会在后盾解析 HTTP 申请报文内容,而后依据报文内容进行解决之后返回 HTTP 响应报文给客户端。

Netty 编解码器

如果咱们要通过 Netty 解决 HTTP 申请,须要先进行编解码。所谓编解码说白了就是在 Netty 传输数据所用的 ByteBuf 和 Netty 中针对 HTTP 申请和响应所提供的对象比方 HttpRequestHttpContent之间相互转换。

Netty 自带了 4 个罕用的编解码器:

  1. HttpRequestEncoder(HTTP 申请编码器):将 HttpRequestHttpContent 编码为 ByteBuf
  2. HttpRequestDecoder(HTTP 申请解码器):将 ByteBuf 解码为 HttpRequestHttpContent
  3. HttpResponsetEncoder(HTTP 响应编码器):将 HttpResponseHttpContent 编码为 ByteBuf
  4. HttpResponseDecoder(HTTP 响应解码器):将 ByteBuf 解码为 HttpResponstHttpContent

网络通信最终都是通过字节流进行传输的。ByteBuf 是 Netty 提供的一个字节容器,其外部是一个字节数组。 当咱们通过 Netty 传输数据的时候,就是通过 ByteBuf 进行的。

HTTP Server 端用于接管 HTTP Request,而后发送 HTTP Response。因而咱们只须要 HttpRequestDecoderHttpResponseEncoder 即可。

我手绘了一张图,这样看着应该更容易了解了。

Netty 对 HTTP 音讯的形象

为了可能示意 HTTP 中的各种音讯,Netty 设计了形象了一套残缺的 HTTP 音讯结构图,外围继承关系如下图所示。

  1. HttpObject : 整个 HTTP 音讯体系结构的最上层接口。HttpObject 接口下又有 HttpMessageHttpContent 两大外围接口。
  2. HttpMessage: 定义 HTTP 音讯,为 HttpRequestHttpResponse提供通用属性
  3. HttpRequest : HttpRequest对应 HTTP request。通过 HttpRequest 咱们能够拜访查问参数(Query Parameters)和 Cookie。和 Servlet API 不同的是,查问参数是通过 QueryStringEncoderQueryStringDecoder来结构和解析查问查问参数。
  4. HttpResponseHttpResponse 对应 HTTP response。和 HttpMessage 相比,HttpResponse 减少了 status(相应状态码)属性及其对应的办法。
  5. HttpContent : 分块传输编码Chunked transfer encoding)是超文本传输协定(HTTP)中的一种数据传输机制(HTTP/1.1 才有),容许 HTTP 由应用服务器发送给客户端利用(通常是网页浏览器)的数据能够分成多“块”(数据量比拟大的状况)。咱们能够把 HttpContent 看作是这一块一块的数据。
  6. LastHttpContent : 标识 HTTP 申请完结,同时蕴含 HttpHeaders 对象。
  7. FullHttpRequestFullHttpResponseHttpMessageHttpContent 聚合后失去的对象。

HTTP 音讯聚合器

HttpObjectAggregator 是 Netty 提供的 HTTP 音讯聚合器,通过它能够把 HttpMessageHttpContent 聚合成一个 FullHttpRequest 或者 FullHttpResponse(取决于是解决申请还是响应),不便咱们应用。

另外,音讯体比拟大的话,可能还会分成好几个音讯体来解决,HttpObjectAggregator 能够将这些音讯聚合成一个残缺的,不便咱们解决。

应用办法:将 HttpObjectAggregator 增加到 ChannelPipeline 中,如果是用于解决 HTTP Request 就将其放在 HttpResponseEncoder 之后,反之,如果用于解决 HTTP Response 就将其放在 HttpResponseDecoder 之后。

因为,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 所必须的第三方依赖的坐标增加到 pom.xml中。

<!--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. 创立了两个 NioEventLoopGroup 对象实例:bossGroupworkerGroup

  • bossGroup : 用于解决客户端的 TCP 连贯申请。
  • workerGroup:负责每一条连贯的具体读写数据的解决逻辑,真正负责 I/O 读写操作,交由对应的 Handler 解决。

举个例子:咱们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在里面接完活之后,扔给 workerGroup 去解决。个别状况下咱们会指定 bossGroup 的 线程数为 1(并发连贯量不大的时候),workGroup 的线程数量为 CPU 外围数 *2。另外,依据源码来看,应用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 外围数 *2

2. 创立一个服务端启动疏导 / 辅助类:ServerBootstrap,这个类将疏导咱们进行服务端的启动工作。

3. 通过 .group() 办法给疏导类 ServerBootstrap 配置两大线程组,确定了线程模型。

4. 通过 channel() 办法给疏导类 ServerBootstrap指定了 IO 模型为NIO

  • NioServerSocketChannel:指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的 ServerSocket 对应
  • NioSocketChannel : 指定客户端的 IO 模型为 NIO,与 BIO 编程模型中的 Socket 对应

5. 通过 .childHandler()给疏导类创立一个ChannelInitializer,而后指定了服务端音讯的业务解决逻辑也就是自定义的ChannelHandler 对象

6. 调用 ServerBootstrap 类的 bind()办法绑定端口

//bind()是异步的,然而,你能够通过 sync()办法将其变为同步。ChannelFuture f = b.bind(port).sync();

自定义服务端 ChannelHandler 解决 HTTP 申请

咱们继承SimpleChannelInboundHandler , 并重写上面 3 个办法:

  1. channelRead():服务端接管并解决客户端发送的 HTTP 申请调用的办法。
  2. exceptionCaught():解决客户端发送的 HTTP 申请产生异样的时候被调用。
  3. channelReadComplete() : 服务端生产完客户端发送的 HTTP 申请之后调用的办法。

另外,客户端 HTTP 申请参数类型为 FullHttpRequest。咱们能够把 FullHttpRequest对象看作是 HTTP 申请报文的 Java 对象的表现形式。

@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 对象。通过 FullHttpResponse 对象,咱们能够设置 HTTP 响应报文的 HTTP 协定版本、响应的具体内容 等内容。

咱们能够把 FullHttpResponse 对象看作是 HTTP 响应报文的 Java 对象的表现形式。

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));

咱们通过 FullHttpResponseheaders() 办法获取到 HttpHeaders, 这里的 HttpHeaders 对应于 HTTP 响应报文的头部。通过 HttpHeaders对象,咱们就能够对 HTTP 响应报文的头部的内容比方 Content-Typ 进行设置。

response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());

本案例中,为了拆穿咱们设置的 Content-Type 为 text/html,也就是返回 html 格局的数据给客户端。

常见的 Content-Type

Content-Type 解释
text/html html 格局
text/plain 纯文本格式
text/css css 格局
text/javascript js 格局
application/json json 格局(前后端拆散我的项目罕用)
image/gif gif 图片格式
image/jpeg jpg 图片格式
image/png png 图片格式

申请的具体解决逻辑实现

因为有这里有 POST 申请和 GET 申请。因而咱们须要首先定义一个解决 HTTP Request 的接口。

public interface RequestHandler {Object handle(FullHttpRequest fullHttpRequest);
}

HTTP Method 不只是有 GET 和 POST,其余常见的还有 PUT、DELETE、PATCH。只是本案例中实现的 HTTP Server 只思考了 GET 和 POST。

  • GET:申请从服务器获取特定资源。举个例子:GET /classes(获取所有班级)
  • POST:在服务器上创立一个新的资源。举个例子:POST /classes(创立班级)
  • PUT:更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /classes/12(更新编号为 12 的班级)
  • DELETE:从服务器删除特定的资源。举个例子:DELETE /classes/12(删除编号为 12 的班级)
  • PATCH:更新服务器上的资源(客户端提供更改的属性,能够看做作是局部更新),应用的比拟少,这里就不举例子了。

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 的 @RequestParam 注解了。

倡议想要学习的小伙伴,能够本人独立实现一下。不晓得如何实现的话,你能够参考我开源的轻量级 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 为 application/json 的数据,如果 POST 申请传过来的不是 application/json 类型的数据,咱们就间接抛出异样。

实际上,咱们取得了客户端传来的 json 格局的数据之后,再联合反射和注解相干的常识,咱们很容易实现相似于 Spring Boot 的 @RequestBody 注解了。

倡议想要学习的小伙伴,能够本人独立实现一下。不晓得如何实现的话,你能够参考我开源的轻量级 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 办法的时候,间接实现 RequestHandler 接口,而后将实现类增加到 RequestHandlerFactory 即可。

启动类

public class HttpServerApplication {public static void main(String[] args) {HttpServer httpServer = new HttpServer();
        httpServer.start();}
}

成果

运行 HttpServerApplicationmain() 办法,控制台打印出:

[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 申请

参考

  1. Netty 学习笔记 -http objects

我的开源我的项目举荐

  1. JavaGuide:「Java 学习 + 面试指南」一份涵盖大部分 Java 程序员所须要把握的外围常识。筹备 Java 面试,首选 JavaGuide!
  2. guide-rpc-framework:A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架 - 附具体实现过程和相干教程)
  3. jsoncat:仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架
  4. programmer-advancement:程序员应该有的一些好习惯 + 面试必知事项!
  5. springboot-guide:Not only Spring Boot but also important knowledge of Spring(不只是 SpringBoot 还有 Spring 重要知识点)
  6. awesome-java:Collection of awesome Java project on Github(Github 上十分棒的 Java 开源我的项目汇合).

我是 Guide 哥,一 Java 后端开发,会一点前端,自在的少年。咱们下期再见!微信搜“JavaGuide”回复“面试突击”支付我整顿的 4 本原创 PDF

正文完
 0