Netty理论与实践(二) 创建http客户端 服务端


开发实战

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消息:

  1. Start line格式:HTTP-Version SP Status-Code SP Reason-Phrase CRLF
  2. Response Header格式:KEY: VALUE CRLF
  3. CRLF
  4. 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(如果有的话)。HttpRequestDecoderHttpResponseDecoder是其子类。作用是将字节流解析为HttpRequest / HttpResponse、HttpContent。
  • HttpObjectEncoder:出站处理器,将HttpMessage和HttpContent(如果有的话)转为字节流。HttpRequestEncoderHttpResponseEncoder是其子类。作用是将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
分块传输编码

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值