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

news2024/12/22 22:15:39

目录

    • 开发实战
      • 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消息:

  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
分块传输编码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/779539.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Acwing.003 完全背包问题(DP动态规划)

题目 有 N 种物品和一个容量是 V 的背包&#xff0c;每种物品都有无限件可用。 第 i 种物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整…

【计算机网络】简易UDP网络小程序

文章目录 1. socket函数&#xff1a;创建套接字2. 服务端2.1 服务端创建套接字2.2 服务端绑定2.3 字符串IP和整数IP2.4 运行服务器 3. 客户端3.1 客户端创建套接字3.2 启动客户端 4. 本地测试5. INADDR_ANY 1. socket函数&#xff1a;创建套接字 我们把服务封装成一个类&#…

计算机网络最基础知识介绍

OSI和TCP/IP是很基础但又非常重要的知识,很多知识点都是以它们为基础去串联的,作为底层,掌握得越透彻,理解上层时会越顺畅。今天这篇网络基础科普,就是根据OSI层级去逐一展开的。 01 计算机网络基础 01 计算机网络的分类 按照网络的作用范围:广域网(WAN)、城域网(MA…

【Kafka源码走读】Admin接口的客户端与服务端的连接流程

注&#xff1a;本文对应的kafka的源码的版本是trunk分支。写这篇文章的主要目的是当作自己阅读源码之后的笔记&#xff0c;写的有点凌乱&#xff0c;还望大佬们海涵&#xff0c;多谢&#xff01; 最近在写一个Web版的kafka客户端工具&#xff0c;然后查看Kafka官网&#xff0c;…

Python基础教程:sklearn机器学习入门

1. sklearn基础介绍 sklearn&#xff08;全名为scikit-learn&#xff09;是一个建立在NumPy、SciPy和matplotlib等科学计算库的基础上&#xff0c;用于机器学习的Python开源库。它提供了丰富的工具和函数&#xff0c;用于处理各种机器学习任务&#xff0c;包括分类、回归、聚类…

线性表的顺序存储和链式存储—Python数据结构(二)

线性表 定义&#xff1a; 线性表的定义是描述其逻辑结构&#xff0c;而通常会在线性表上进行的查找、插入、删除等操作。 线性表作为一种基本的数据结构类型&#xff0c;在计算机存储器中映象(表示)一般有两种形式&#xff0c;一种是顺序映象&#xff0c;一种是链式映象。 线…

接口漏洞-WebService-wsdl+SOAP-Swagger+HTTP-WebPack

什么是接口&#xff1f; 接口就是位于复杂系统之上并且能简化你的任务&#xff0c;它就像一个中间人让你不需要了解详细的所有细节。像谷歌搜索系统&#xff0c;它提供了搜索接口&#xff0c;简化了你的搜索任务。再像用户登录页面&#xff0c;我们只需要调用我们的登录接口&am…

Jupyter 安装和使用

安装Jupyter 使用pip工具进行安装&#xff0c;在命令提示窗口输入命令如下&#xff1a; pip install jupyter notebook 使用Jupyter 在命令提示窗口输入如下命令&#xff0c;启动浏览器页面&#xff1a; jupyter notebook 修改jupyter的工作路径/存储路径 由于默认工作路…

去括号问题(C++处理)

继http://t.csdn.cn/kIcUT后的文章 题目描述 当老师不容易&#xff0c;尤其是当小学的老师更难:现在的小朋友做作业喜欢滥用括号。 虽然不影响计算结果&#xff0c;但不够美观&#xff0c;容易出错&#xff0c;而且可读性差。但又不能一棒子打死&#xff0c;也许他们就是将来的…

【Linux从入门到精通】进程的控制(进程退出+进程等待)

本篇文章主要讲述的是进程的退出和进程等待。希望本篇文章的内容会对你有所帮助。 文章目录 一、fork创建子进程 1、1 在创建子进程中操作系统的作用 1、2 写时拷贝 二、进程终止 2、1 常见的进程退出 2、2 进程的退出码 2、2、1 运行结果正确实例 2、2、2 运行结果不正确实例…

防御第二天-防火墙演示实验

1.上课思维导图 2.防火墙演示实验 防火墙FW1&#xff1a;原用户名&#xff1a;admin 原密码&#xff1a;Admin123 配地址&#xff1a;<USG6000V1>sy [USG6000V1]int g0/0/0 [USG6000V1-GigabitEthernet0/0/0]ip add 192.168.18.2 24 打开所有权限[USG6000V1-Gig…

C++(一):基本数据类型

基本数据类型 基本内置类型定义变量type field value;type field(value);type field{value};type field {value}; 数学常量及函数静态类型转换 static_cast格式化字符串std::stringstreamstd::string引入三方库 fmt/core.h 字符运算auto 关键字枚举类型数据类型定义别名判断是…

牛客网刷题之链表(一)

链表 NB1 删除链表峰值NB2 牛群排列去重NB3 调整牛群顺序NB4 牛群的重新分组NB5 牛群的重新排列NB6 合并两群能量值&#xff08;合并有序单链表&#xff09;NB7 牛群的能量值&#xff08;单链表相加&#xff09; 以下题全部出自牛客网。 题目题目考察的知识点链表&#xff1a; …

Element Plus 日期选择器

计算开始日期到结束日期的总天数 结构 <el-form-item label"计划开始时间" required prop"StartTime"><el-date-pickertype"date"v-model"ruleForm.StartTime":disabled-date"StartTime"placeholder"计划开始…

pytorch工具——使用pytorch构建一个分类器

目录 分类器任务和数据介绍训练分类器的步骤在GPU上训练模型 分类器任务和数据介绍 训练分类器的步骤 #1 import torch import torchvision import torchvision.transforms as transformstransformtransforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,0.5,0.…

SpringCloud学习路线(8)—— Docker

一、Docker的开始 &#xff08;一&#xff09;项目部署问题&#xff1a; 依赖关系复杂&#xff0c;容易出现兼容性问题开发、测试、生产环境有差异 &#xff08;二&#xff09;Docker如何解决问题&#xff1f; 1、依赖兼容问题 &#xff08;1&#xff09;将应用的Libs&…

垃圾回收之三色标记法(Tri-color Marking)

关于垃圾回收算法&#xff0c;基本就是那么几种&#xff1a;标记-清除、标记-复制、标记-整理。在此基础上可以增加分代&#xff08;新生代/老年代&#xff09;&#xff0c;每代采取不同的回收算法&#xff0c;以提高整体的分配和回收效率。 无论使用哪种算法&#xff0c;标记…

(已解决)RuntimeError: Java gateway process exited before sending its port number

今天用Pycharm远程使用pysaprk解释器时&#xff0c;跑代码出现了这个错误&#xff1a; RuntimeError: Java gateway process exited before sending its port number 找了好多博客都没解决问题&#xff0c;有说重装spark的&#xff0c;有说本地配Java_home的&#xff0c;后面我…

[C语言刷题]杨氏矩阵、返回型参数

本文包含知识点 杨氏矩阵极其解法函数return多个值的四种方法 题目&#xff1a; 杨氏矩阵 有一个数字矩阵&#xff0c;矩阵的每行从左到右是递增的&#xff0c;矩阵从上到下是递增的&#xff0c;请编写程序在这样的矩阵中查找某个数字是否存在。 要求&#xff1a;时间复杂度小于…

js 在浏览器窗口关闭后还可以不中断网络请求

有个需求&#xff0c;我们需要在用户发送数据过程中&#xff0c;如果用户关闭了网页(包括整个浏览器关闭)&#xff0c;不要中断数据传递 目前XMLHttpRequest对象是不支持的 http服务器 为了测试效果我们用nodejs写了个http服务器代码 文件名为httpServer.js如下&#xff0c;…