14.Netty源码之模拟简单的HTTP服务器


highlight: arduino-light

简单的 HTTP 服务器

HTTP 服务器是我们平时最常用的工具之一。同传统 Web 容器 Tomcat、Jetty 一样,Netty 也可以方便地开发一个 HTTP 服务器。我从一个简单的 HTTP 服务器开始,通过程序示例为你展现 Netty 程序如何配置启动,以及引导器如何与核心组件产生联系。

完整地实现一个高性能、功能完备、健壮性强的 HTTP 服务器非常复杂,本文仅为了方便理解 Netty 网络应用开发的基本过程,所以只实现最基本的请求-响应的流程:

md 搭建 HTTP 服务器,配置相关参数并启动。 ​ 从浏览器或者终端发起 HTTP 请求。 ​ 成功得到服务端的响应结果。

Netty 的模块化设计非常优雅,客户端或者服务端的启动方式基本是固定的。

作为开发者来说,只要照葫芦画瓢即可轻松上手。

大多数场景下,你只需要实现与业务逻辑相关的一系列 ChannelHandler,再加上 Netty 已经预置了 HTTP 相关的编解码器就可以快速完成服务端框架的搭建。

所以,我们只需要两个类就可以完成一个最简单的 HTTP 服务器,它们分别为服务器启动类和业务逻辑处理类,结合完整的代码实现我将对它们分别进行讲解。

服务端启动类

所有 Netty 服务端的启动类都可以采用如下代码结构进行开发。简单梳理一下流程:

首先创建引导器;

然后配置线程模型,通过引导器绑定业务逻辑处理器,并配置一些网络参数;

最后绑定端口,就可以完成服务器的启动了。

java public class HttpServer { ​    public void start(int port) throws Exception { ​        EventLoopGroup bossGroup = new NioEventLoopGroup();        EventLoopGroup workerGroup = new NioEventLoopGroup(); ​        try {            ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)               //指定服务器端的channel类型为NioServerSocketChannel                   .channel(NioServerSocketChannel.class)               //绑定端口号                   .localAddress(new InetSocketAddress(port))               //注冊channelHandler                   .childHandler(new ChannelInitializer<SocketChannel>() {                        @Override                        public void initChannel(SocketChannel ch) {                            ch.pipeline()                            // HTTP 编解码                           .addLast("codec", new HttpServerCodec())                            // HttpContent 压缩                           .addLast("compressor", new HttpContentCompressor())                              // HTTP 消息聚合                           .addLast("aggregator", new HttpObjectAggregator(65536))                              // 自定义业务逻辑处理器                           .addLast("handler", new HttpServerHandler());                                   }                   }).childOption(ChannelOption.SO_KEEPALIVE, true);            ChannelFuture f = b.bind().sync();            System.out.println("Http Server started, Listening on " + port);            f.channel().closeFuture().sync();       } finally {            workerGroup.shutdownGracefully();            bossGroup.shutdownGracefully();       }   }    public static void main(String[] args) throws Exception {        new HttpServer().start(8088);   } } ​

服务端业务逻辑处理类

如下代码所示,HttpServerHandler 是业务自定义的服务端逻辑处理类。它是入站 ChannelInboundHandler 类型的处理器,负责接收解码后的 HTTP 请求数据,并将请求处理结果写回客户端。

java public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {    @Override    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {        String content = String.format("Receive http request, uri: %s, method: %s, content: %s%n", msg.uri(), msg.method(), msg.content().toString(CharsetUtil.UTF_8));        FullHttpResponse response = new DefaultFullHttpResponse(                HttpVersion.HTTP_1_1,                HttpResponseStatus.OK,                Unpooled.wrappedBuffer(content.getBytes()));        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);   } }

通过上面两个类,我们可以完成 HTTP 服务器最基本的请求-响应流程,测试步骤如下:

  1. 启动 HttpServer 的 main 函数。
  2. 终端或浏览器发起 HTTP 请求。

测试结果输出如下:

  1. curl http://localhost:8088/abc
  2. $ Receive http request, uri: /abc, method: GET, content:

当然,你也可以使用 Netty 自行实现 HTTP Client,客户端和服务端的启动类代码十分相似。

引导器实践指南

Netty 服务端的启动过程大致分为三个步骤:

1.配置线程池。 2.Channel 初始化。 3.端口绑定。

1.配置线程池:Reactor

网络框架的设计离不开 I/O 线程模型,线程模型的优劣直接决定了系统的吞吐量、可扩展性、安全性等。目前主流的网络框架几乎都采用了 I/O 多路复用的方案。Reactor 模式作为其中的事件分发器,负责将读写事件分发给对应的读写事件处理者。大名鼎鼎的 Java 并发包作者 Doug Lea,在 Scalable I/O in Java 一文中阐述了服务端开发中 I/O 模型的演进过程。Netty 中三种 Reactor 线程模型也来源于这篇经典文章。下面我们对这三种 Reactor 线程模型做一个详细的分析。Netty 是采用 Reactor 模型进行开发的,可以非常容易切换三种 Reactor 模式:单线程模式多线程模式主从多线程模式

单线程模型

Reactor 单线程模型所有 I/O 操作都由一个线程完成,所以只需要启动一个 EventLoopGroup 即可。

java ServerBootstrap b = new ServerBootstrap(); //注意参数是1 EventLoopGroup group = new NioEventLoopGroup(1); //代表服务器和客户端都是一个group b.group(group)

image.png

上图描述了 Reactor 的单线程模型结构,在 Reactor 单线程模型中,所有 I/O 操作(包括连接建立、数据读写、事件分发等),都是由一个线程完成的。单线程模型逻辑简单,缺陷也十分明显:

  • 一个线程支持处理的连接数非常有限,CPU 很容易打满,性能方面有明显瓶颈;

  • 当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时;

  • 线程在处理 I/O 事件时,Select 无法同时处理连接建立、事件分发等操作;

  • 如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用。

多线程模型

Reactor 单线程模型有非常严重的性能瓶颈,因此 Reactor 多线程模型出现了。在 Netty 中使用 Reactor 多线程模型与单线程模型非常相似,区别是 NioEventLoopGroup 可以不需要任何参数,它默认会启动 2 倍 CPU 核数的线程。当然,你也可以自己手动设置固定的线程数。

java ServerBootstrap b = new ServerBootstrap(); //默认会启动 2 倍 CPU 核数的线程 EventLoopGroup group = new NioEventLoopGroup(); //代表服务器和客户端都是一个group b.group(group)

image.png

由于单线程模型有性能方面的瓶颈,多线程模型作为解决方案就应运而生了。Reactor 多线程模型将业务逻辑交给多个线程进行处理。

除此之外,多线程模型其他的操作与单线程模型是类似的,例如读取数据依然保留了串行化的设计。当客户端有数据发送至服务端时,Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理。

主从多线程模型

在大多数场景下,我们采用的都是主从多线程 Reactor 模型。Boss 是主 Reactor,Worker 是从 Reactor。它们分别使用不同的 NioEventLoopGroup,主 Reactor 负责处理 Accept,然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。

ServerBootstrap b = new ServerBootstrap(); //默认会启动 2 倍 CPU 核数的线程 EventLoopGroup bossGroup = new NioEventLoopGroup(); //默认会启动 2 倍 CPU 核数的线程 EventLoopGroup workerGroup = new NioEventLoopGroup(); //代表服务器和客户端使用各自的group b.group(bossGroup, workerGroup)

image.png

主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程都有独立的 Selector 对象。 MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。

从上述三种 Reactor 线程模型的配置方法可以看出:Netty 线程模型的可定制化程度很高。它只需要简单配置不同的参数,便可启用不同的 Reactor 线程模型,而且无需变更其他的代码,很大程度上降低了用户开发和调试的成本。

Netty 推荐使用主从多线程模型,这样就可以轻松达到成千上万规模的客户端连接。在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。

2.Channel 初始化

设置 Channel 类型

NIO 模型是 Netty 中最成熟且被广泛使用的模型。因此,推荐 Netty 服务端采用 NioServerSocketChannel 作为 Channel 的类型,客户端采用 NioSocketChannel。设置方式如下

java b.channel(NioServerSocketChannel.class);

Netty 提供了多种Channel实现,可以按需切换,例如 OioServerSocketChannel、EpollServerSocketChannel 等。

注册 ChannelHandler

在 Netty 中可以通过 ChannelPipeline 去注册多个 ChannelHandler,每个 ChannelHandler 各司其职,这样就可以实现最大化的代码复用,充分体现了 Netty 设计的优雅之处。

那么如何通过引导器添加多个 ChannelHandler 呢?其实很简单,我们看下 HTTP 服务器代码示例:

java /***     在基类AbstractBootstrap有handler方法,目的是添加一个handler,监听Bootstrap的动作,客户端的Bootstrap中,继承了这一点。 ​ 在服务端的ServerBootstrap中增加了一个方法childHandler,它的目的是添加handler,用来监听已经连接的客户端的Channel的动作和状态。 ​ handler()和childHandler()的主要区别是,handler()是发生在初始化的时候,childHandler()是发生在客户端连接之后。 ​ 也就是说,如果需要在客户端连接前的请求进行handler处理,则需要配置handler(),如果是处理客户端连接之后的handler,则需要配置在childHandler()。 ***/ b.childHandler(new ChannelInitializer<SocketChannel>() {    @Override    public void initChannel(SocketChannel ch) {        ch.pipeline()           //HTTP 编解码处理器               .addLast("codec", new HttpServerCodec())           //HTTPContent 压缩处理器               .addLast("compressor", new HttpContentCompressor())           //HTTP 消息聚合处理器               .addLast("aggregator", new HttpObjectAggregator(65536))           //自定义业务逻辑处理器               .addLast("handler", new HttpServerHandler());   } })

ServerBootstrap 的 childHandler() 方法需要注册一个 ChannelHandler。

ChannelInitializer是实现了 ChannelHandler接口的匿名类

通过实例化 ChannelInitializer 作为 ServerBootstrap 的参数。

Channel 初始化时都会绑定一个 Pipeline,它主要用于服务编排。Pipeline 管理了多个 ChannelHandler。I/O 事件依次在 ChannelHandler 中传播,ChannelHandler 负责业务逻辑处理。上述 HTTP 服务器示例中使用链式的方式加载了多个 ChannelHandler,包含HTTP 编解码处理器、HTTPContent 压缩处理器、HTTP 消息聚合处理器、自定义业务逻辑处理器

服务端收到 HTTP 请求后,会依次经过 HTTP 编解码处理器、HTTPContent 压缩处理器、HTTP 消息聚合处理器、自定义业务逻辑处理器分别处理后,再将最终结果通过 HTTPContent 压缩处理器、HTTP 编解码处理器写回客户端。

设置 ChannelOption参数

Netty 提供了十分便捷的方法,用于设置 Channel 参数。关于 Channel 的参数数量非常多,如果每个参数都需要自己设置,那会非常繁琐。

幸运的是 Netty 提供了默认参数设置,实际场景下默认参数已经满足我们的需求,我们仅需要修改自己关系的参数即可。

java b.option(ChannelOption.SO_KEEPALIVE, true);

ServerBootstrap 设置 Channel 属性有option和childOption两个方法,

option 主要负责设置 Boss 线程组,childOption 对应的是 Worker 线程组。

这里我列举了经常使用的参数含义,你可以结合业务场景,按需设置。

| 参数 | 含义 | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | SOKEEPALIVE | 设置为 true 代表启用了 TCP SOKEEPALIVE 属性,TCP 会主动探测连接状态,即连接保活 | | SOBACKLOG | 已完成三次握手的请求队列最大长度,同一时刻服务端可能会处理多个连接,在高并发海量连接的场景下,该参数应适当调大 | | TCPNODELAY | Netty 默认是 true,表示立即发送数据。如果设置为 false 表示启用 Nagle 算法,该算法会将 TCP 网络数据包累积到一定量才会发送,虽然可以减少报文发送的数量,但是会造成一定的数据延迟。Netty 为了最小化数据传输的延迟,默认禁用了 Nagle 算法 | | SOSNDBUF | TCP 数据发送缓冲区大小 | | SORCVBUF | TCP数据接收缓冲区大小,TCP数据接收缓冲区大小 | | SOLINGER | 设置延迟关闭的时间,等待缓冲区中的数据发送完成 | | CONNECTTIMEOUT_MILLIS | 建立连接的超时时间 |

3.端口绑定

在完成上述 Netty 的配置之后,bind() 方法会真正触发启动,sync() 方法则会阻塞,直至整个启动过程完成,具体使用方式如下:

java ChannelFuture f = b.bind().sync();

到此为止我们就开发了1个简单的http服务,并且可以接受请求。

通过使用Netty模拟简单的HTTP服务器我们知道了服务器端的引导器开发的3个步骤。

1.配置线程池

2.channel初始化

3.端口绑定

客户端的开发流程和服务器端的开发流程类似,后续会在示例中给出完整的代码。

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

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

相关文章

2023年全国程序员薪酬排行天梯榜

文章目录 ⭐️ 2023年全国程序员薪酬排行天梯榜 在过去很长的一段时间内&#xff0c;网上总有一个声音&#xff1a;“大厂裁员”、“程序员内卷严重”、“程序员人员过盛”、“35岁中年危机”、“码农吃的青春饭”、“互联网寒冬” 等等等等。 讲道理&#xff0c;我对这种人为的…

LLM Data Pipelines: 解析大语言模型训练数据集处理的复杂流程

编者按&#xff1a;在训练大语言模型的过程中,构建高质量的训练数据集是非常关键的一步&#xff0c;但关于构建大模型训练所需数据集的通用数据处理流程&#xff08;Data pipelines)的相关资料极为稀少。 本文主要介绍了基于Common Crawl数据集的数据处理流程。首先,文章概述了…

复现YOLOv8改进最新MPDIoU:有效和准确的边界盒回归的损失,打败G/E/CIoU,效果明显!!!

MPDIoU: A Loss for Efficient and Accurate Bounding Box Regression 论文简介MPDIoU核心设计思路论文方法实验部分加入YOLOv5代码论文地址:https://arxiv.org/pdf/2307.07662.pdf 论文简介 边界盒回归(Bounding box regression, BBR)广泛应用于目标检测和实例分割,是目标…

【业务功能篇56】SpringBoot 日志SLF4J Logback

3.5.1 日志框架分类与选择 3.5.1.1 日志框架的分类 日志门面 (日志抽象)日志实现JCL(Jakarta Commons Logging) SLF4J(Simple Logging Facade for Java)Jul(Java Util Logging) , Log4j , Log4j2 , Logback 记录型日志框架 Jul (Java Util Logging)&#xff1a;JDK中的日志…

Python实现指定区域桌面变化监控并报警

在这篇博客中&#xff0c;我们将使用Python编程语言和一些常用的库来实现一个简单的区域监控和变化报警系统。我们将使用Tkinter库创建一个图形界面&#xff0c;允许用户选择监控区域&#xff0c;并使用OpenCV库进行图像处理和相似性比较&#xff0c;以检测区域内的变化&#x…

基于IP地址的证书实现https

基于IP地址实现传递数据的&#xff0c;默认的HTTP很容易被不法分子劫持数据&#xff0c;网络防洪是当下的互联网为确保安全&#xff0c;要用HTTPS协议更为妥当。 使用IP地址申请证书的主要条件&#xff0c;必须在申请认证过程&#xff0c;开放IP地址外网可以访问&#xff0c;包…

全方位支持图文和音视频、100+增强功能,Facebook开源数据增强库AugLy

Facebook 近日开源了数据增强库 AugLy&#xff0c;包含四个子库&#xff0c;每个子库对应不同的模态&#xff0c;每个库遵循相同的接口。支持四种模态&#xff1a;文本、图像、音频和视频。 最近&#xff0c;Facebook 开源了一个新的 Python 库——AugLy&#xff0c;该库旨在帮…

C语言每日一题:4.消失的数字+数字在升序数组中出现的次数+整数转换

消失的数字&#xff1a; 思路1&#xff1a;排序遍历 1.使用qsort排序数组判断当前数值1是否是数组下一个元素的数值。 2.如果是一直循环注意数组越界&#xff0c;如果不是那么当前的数组的数值1就是消失的数。 3.存在0——n的数字是第n个数没有了。循环过程中从头到尾也找不到这…

Zabbix监控之分布式部署

文章目录 Zabbix监控之分布式部署zabbix proxy概述部署zabbix-proxy节点规划基础环境准备安装proxy以及数据库配置数据库添加服务端host解析修改zabbix-proxy配置文件启动代理服务器 zabbix页面(1)在zabbix页面添加代理(2)zabbix-agent连接proxy Zabbix监控之分布式部署 zabbi…

【LeetCode】101.对称二叉树

题目 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; 树中节点数…

几个影响 cpu cache 性能因素及 cache 测试工具介绍

》内核新视界文章汇总《 文章目录 1 cache 性能及影响因素1.1 内存访问和性能比较1.2 cache line 对性能的影响1.3 L1 和 L2 缓存大小1.4 指令集并行性对 cache 性能的影响1.5 缓存关联性对 cache 的影响1.6 错误的 cacheline 共享 (缓存一致性)1.7 硬件设计 2 cpu cache benc…

让企业出海支付流程更加安全,亚马逊云科技推出支付加密服务

在咖啡店买一杯咖啡&#xff0c;在电商平台下单一件商品&#xff0c;其消费流程背后&#xff0c;都要涉及到金融支付的多个环节&#xff0c;而其中对金融数据存储和流通的加密则是保障金融安全的重要环节。 加密是保障消费者支付流程安全的最大挑战。亚马逊云科技首席安全布道…

数仓学习---15、数据仓库工作流调度

1、数据仓库工作流调度 1.1 调度工具部署 工具部署链接 1.2 新数据生成 1.2.1 用户行为日志 1、启动日志采集通道&#xff0c;包括Kafka、Flume等 &#xff08;1&#xff09;启动Zookeeper zk.sh start&#xff08;2&#xff09;启动Kafka kf.sh start&#xff08;3&…

【ROS第一讲】一、创建工作空间

【ROS第一讲】一、创建工作空间 一、工作空间1.src&#xff1a;2.build&#xff1a;3.devel&#xff1a;4.install: 二、创建工作空间1.工作空间的编译2.配置环境变量&#xff1a; 三、创建功能包 一、工作空间 1.src&#xff1a; 放置所有功能包源码的空间 2.build&#xf…

【语音识别】- 声学,词汇和语言模型

一、说明 语音识别是指计算机通过处理人类语言的音频信号&#xff0c;将其转换为可理解的文本形式的技术。也就是说&#xff0c;它可以将人类的口语语音转换为文本&#xff0c;以便计算机能够进一步处理和理解。它是自然语言处理技术的一部分&#xff0c;被广泛应用于语音识别助…

项目播报 | 新基德携手璞华易研PLM,打造企业新一代研发管理平台

近日&#xff0c;“新基德PLM&#xff08;Product Lifecycle Management&#xff0c;产品生命周期管理&#xff09;项目”在新基德&#xff08;深圳&#xff09;电子有限公司&#xff08;以下简称&#xff1a;新基德&#xff09;正式启动。新基德是一家集手机研发、生产、销售、…

基于深度学习的高精度六类海船检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度六类海船检测识别系统可用于日常生活中检测与定位海船目标&#xff08;散装货船&#xff08;bulk cargo carrier&#xff09;、集装箱船&#xff08;container ship&#xff09;、渔船&#xff08;fishing boat&#xff09;、普通货船&…

【Docker】Docker Compose的配置与部署

文章目录 一、Docker Compose1. Docker Compose 的概述2. Docker Compose 三大的概念3. Docker Compose 环境安装二、YAML 文件格式及编写注意事项1. YAML 文件格式2. YAML 格式的注意事项3. YAML 数据结构3.1 基本类型3.2 实例3.3 YAML 特殊类型文本块锚点与引用三、Docker Co…

JAVA面试总结-Redis篇章(四)——双写一致性

JAVA面试总结-Redis篇章&#xff08;四&#xff09;——双写一致性 问&#xff1a;redis 做为缓存&#xff0c;mysql的数据如何与redis进行同步呢&#xff1f;第一种情况&#xff0c;如果你的项目一致性要求高的话 采用以下逻辑我们应该先删除缓存&#xff0c;再修改数据库&…

栈粉碎原理分析

栈粉碎原理分析 源代码如下 #include <stdio.h>void function(int a, int b) {char buffer[12];gets(buffer);//long* ret (long *) ((long)buffer28);//*ret *ret 7;return; }void main() {int x;x 0;function(1,2);x 1;printf("%d\n",x); } 由解注释前…