Netty详解-02
Netty详解-02
Netty详解-01
先随便聊聊
其实是我遇到的很多疑问的总结:
要netty干嘛?
就一个在线聊天平台,通过springboot + websocket可以快速开发
(在我的其他文章里有Websocket讲解和案例哦)
但他会有很多缺点:
- 高并发扛不住
- 延迟高
- 无法实现复杂需求(清理僵尸用户等)
- 消息同步困难(分布式)
这些是因为默认用到了tomcat,底层虽然也是nio,但tomcat只提供了WebSocket的基础通信能力
因此就要用netty来开发
tomcat和netty都基于nio,不是tomcat基于netty
后端项目中可让tomcat处理传统http请求,netty处理websocket长连接
Websocket是网络应用层协议(和http一层)
不是说netty可以 替代 Websocket实现在线聊天,而是用netty 结合 Websocket可以让效果更好
netty要实现长连接要基于Websocket(不过WebSocket只是Netty支持的其中一种长连接方案,并非只有Websocket才能实现长连接)
长连接的本质是TCP连接不主动关闭
netty可以基于TCP自定义长连接,更底层
请求与连接
注意请求和连接的区别:
连接:本质是TCP连接(如HTTP短连接、WebSocket长连接),是客户端与服务端之间的通信通道(对应 Netty 的Channel)
请求:是连接上传输的业务数据单元(如HTTP的一次GET/POST、WebSocket的一条聊天消息)
要发送请求→ 建立连接→ 传输请求(因请求诞生连接,连接上跑请求)
连接由IO线程管理,分发给工作线程处理
IO线程少(CPU 核心 ×2)
工作线程多(20~100 个,按需调整)
TCP是面向连接、保证可靠传输但实时性稍低的协议,UDP是无连接、不保证可靠传输但实时性极高的协议
IO线程管连接,连接(http短连接(跑一次请求),websocket长连接(跑n次请求))上跑请求
工作线程管业务
IO线程调用工作线程,前者不阻塞,后者容易阻塞
请求传输流程
- 前端通常通过axios发送请求,axios基于(封装)XHR(XmlHttpRequest)
- XHR包装请求数据(请求路径、请求头、请求体)
- 浏览器通过Http协议将请求数据转为Http请求报文(二进制,包含请求行、请求头、请求体)
- 浏览器调用操作系统的网络模块,让TCP给Http报文加上TCP头并传给电脑网卡
- 网络层模块给这个TCP段(TCP头+Http报文)添加IP头
- 网卡将二进制数据转为电信号,并通过网线传输
- 到达路由器,路由器解析目标IP,判断是否要发到外网
- 若需要,路由器则将电信号传给光猫(光纤宽带)
- 光猫将电信号转为光信号并传给外部(小区分光箱、城域网、互联网骨干网(国家级网络高速路))
- 互联网骨干网根据目标IP将光信号不断转发到对应机房
- 机房光模块将光信号转回电信号,再传回网卡
- 网卡将电信号再转为二进制数据交给操作系统
- 操作系统根据TCP头中的目标端口(比如 8080),把二进制数据交给正在监听该端口的应用层服务器(Tomcat/Netty)
- Tomcat/Netty解析二进制数据中的Http报文,提取出请求路径、参数、请求体,分发到对应的后端业务代码(Controller/Servlet)
- 业务代码处理请求(查库、逻辑计算),生成响应数据
- Tomcat/Netty把响应数据包装成Http响应报文(二进制),交给服务器操作系统 + TCP
- 响应报文通过原物理链路(机房→骨干网→光猫→路由器→客户端网卡)转回电信号,再转为二进制数据
- 客户端操作系统 + TCP 把二进制数据交给浏览器,浏览器解析Http响应报文,提取响应体
- 浏览器通过XHR把响应体回传给axios,axios转换为JSON后,交给前端代码处理(比如渲染页面)
前端代码 -> 浏览器 + Http -> 客户端操作系统 + TCP/IP -> 网卡 -> 路由器 -> 光猫 -> 外部网络 -> 机房 -> 网卡 -> 服务端操作系统 -> 服务器(netty)-> 后端代码 -> 服务端操作系统 -> 原物理链路 -> 客户端操作系统 -> 浏览器 -> 前端代码(axios)
- 应用层(HTTP):包装请求数据→HTTP报文(二进制)
- 传输层(TCP):给HTTP报文加TCP头(含目标端口、源端口等)→TCP段
- 网络层(IP):给TCP 段加IP头(含目标IP、源IP等)→IP数据报
- 数据链路层(网卡):给IP数据报加帧头→数据帧,转为电信号 / 光信号传输
Netty总体流程结构:!!!
重点:
-
配置(IO线程池、Channel、Pipeline(Handler))、启动
Bootstrap(一个EventLoopGroup(WorkerGroup))配置客户端
ServerBootstrap(两个EventLoopGroup(BossGroup(接收连接)、WorkerGroup(处理IO)))配置服务器
EventLoop就是IO线程 + Selectot + 任务队列(存放轻量的非IO任务),EventLoopGroup是EventLoop集合
-
一个IO线程对应一个Selector(通常1:1,也可以1:n)
-
一个Selector监控多个Channel
-
每个Channel绑定Pipline
-
每个Pipline对应多个Handler(Pipeline是Handler的双向链表)
-
Buffer存放具体数据,通过Channel发送
-
连接到来时,BossGroup中的IO线程建立连接,并将Channel注册到WorkGroup中的某个IO线程的Selector下
-
当有Channel就绪时(连接建立、可读(来数据)、可写(有对象))(就绪不代表Buffer有数据,不要局限)
-
Selector告知IO线程(WorkGroup)
-
IO线程调用对应Channel的Pipline,依次执行所有Handler
-
IO线程自己执行轻量级操作(解码、编码)
-
IO线程将业务逻辑Handler交给工作线程池(非IO线程)处理
-
处理结果(对象)在服务器一侧编码为二进制字节流,再存入Buffer(例如ByteBuf),通过Channel传给客户端
-
客户端(浏览器Http客户端 或者 netty客户端)一侧重复8~13(netty客户端并非刚需,只有我们不想使用浏览器这种情况时,才自定义客户端,netty客户端和浏览器作用相同)
在开始下面的详解前,一定要先自己了解一下nio、netty核心组件(我的另一篇文章有哦!)
然后好好理清楚上面13条的流程结构,我觉得一定一定对你有所帮助!!!
NIO详解
netty底层就是nio,所以,得学
以前java基础中的io操作中,FileInputStream这种就是BIO(阻塞的)
其他基本概念可查看Netty详解-01
三大组件
Channel
Channel是双向通道,FileInputStream这种是单向的
常见:
- FileChannel
- SocketChannel
- ServcerSocketChannel
Buffer
Buffer负责临时存储输入Channel或者从Channel输出的数据
常见:
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- IntBuffer
- CharBuffer
Selector
服务器设计演化:
-
单线程版本:所有连接共用一个线程(完全不并发)
-
多线程版本:每个连接对应一个线程(无节制创建线程)
-
线程池版本:复用线程
-
selector版本:多路复用优化
selector充当监控的作用,一个selector监控多个channel,只要有channel传来了数据,selector将其交给IO线程处理
-
主从Reactor版本
接下来根据nio中常用组件进行讲解
注意:
nio中buffer、channel都有很多种,但是在netty使用中不用特别关心用哪种(你也可以直接去看Netty详解)
关于Buffer:Netty只用
ByteBuf,且默认选好了实现关于Channel:Netty按场景自动绑定
服务端用
ServerBootstrap配置时,指定NioServerSocketChannel客户端:用
Bootstrap配置时,指定NioSocketChannel后续Netty详解会给出一个netty案例,到时候你可以再来这里回味一下
NIO示例
曾经的FileInputStream(BIO):
public class FileInputStreamDemo {public static void main(String[] args) {String filePath = "test.txt";byte[] buf = new byte[1024]; // 缓冲区大小(1KB,可根据文件大小调整,如 4096)int readLen; // 实际读取的字节数try (FileInputStream fis = new FileInputStream(filePath)) {// 循环读取:每次最多读 1024 字节,存入 bufwhile ((readLen = fis.read(buf)) != -1) {// 字节数组转字符串(参数:数组、起始索引、实际长度,避免读取到缓冲区残留数据)System.out.print(new String(buf, 0, readLen));}} catch (IOException e) {e.printStackTrace();}}
}
基于ByteBuffer + FileChannel(NIO)
public class ByteBuffer_demo {public static void main(String[] args) {try(FileChannel channel = new FileInputStream("data.txt").getChannel()){ByteBuffer buffer = ByteBuffer.allocate(10);int len;while ((len = channel.read(buffer)) != -1){System.out.println("read " + len + " bytes");buffer.flip();while (buffer.hasRemaining()){byte b = buffer.get();System.out.println((char)b);}buffer.clear();// // 直接把缓冲区有效字节转成字符串(无需逐个字节读)
// System.out.print(new String(buffer.array(), 0, len, "UTF-8"));
// buffer.clear(); // 切换回写模式}}catch (IOException e){e.printStackTrace();}}
}
对比这两段代码,你会感觉到基础文件操作中,BIO、NIO操作类似,只是后者多加了几个步骤(可控事件更多)
| 维度 | BIO(FileInputStream) | NIO(FileChannel+ByteBuffer) |
|---|---|---|
| 核心模型 | 「流模型」:数据是 “流动的字节流”,只能 “从头到尾读”,不能回退、不能跳着读 | 「块模型」:数据是 “块级存储”,缓冲区支持回退(position--)、随机访问(position(100))、部分读取 |
| 控制权 | 黑盒封装:底层缓冲区、数据拷贝、读写切换都由框架处理,你只能 “被动接收数据” | 显式掌控:缓冲区的读写模式、读取位置、剩余数据都由你控制,能 “主动操作数据” |
| 额外能力(复杂场景) | 几乎没有:无法处理跨缓冲区数据、无法随机访问、无法避免二次拷贝 | 核心能力:支持随机访问(channel.position(100) 直接读第 100 字节)、跨缓冲区数据解析、直接缓冲区(避免二次拷贝)、非阻塞读取(网络 I/O 场景) |
| 适用场景 | 简单需求:小文件读取、文本打印、简单复制(不需要复杂字节操作) | 复杂需求:大文件处理、自定义协议解析、随机访问文件、网络高并发(和 Netty 等框架配合) |
简单来说,BIO这套有很多是框架定好的,NIO允许我们做更加底层丰富的定义
例如,上面我们只是定义了ByteBuffer的大小,但还可以通过
ByteBuffer的 API 实现位置控制、范围限制、数据压缩、批量读写、直接缓冲区等复杂操作
ByteBuffer概念
FileChannel概念
NIO具体代码后面Netty详解-03再讲吧
现在感觉理清楚概念、组件顺序就好了
Netty详解
就先来一点开胃小菜吧
理不清楚时一定要再去看之前的执行流程!!!
算了我直接粘过来吧:

netty服务端编码:
public class HelloServer {public static void main(String[] args) {// 启动器new ServerBootstrap()// 创建线程组(理论上需要:BossEventLoop、WorkerEventLoop用于分配处理连接和处理IO的线程)// 此处只创建BossEventLoop// NioEventLoopGroup:多个NioEventLoop线程// NioEventLoop:是一个IO线程+Selector+任务队列.group(new NioEventLoopGroup())// 选择服务器的ServerSocketChannel实现.channel(NioServerSocketChannel.class)// 添加处理器.childHandler(// 创建一个通道初始化对象new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {// 通道绑定Pipline(处理器链)ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Override//父类方法public void channelRead(ChannelHandlerContext ctx, Object msg)//因此不能用protected void channelRead(ChannelHandlerContext ctx, Object msg)//子类不能比父类还保守public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println(msg);}});}}).bind(8080);}
}
netty客户端编码:
public class HelloClient {public static void main(String[] args) throws InterruptedException{
// IO线程 -> Selector -> channel -> Pipline// 启动器new Bootstrap()// 线程组EventLoop.group(new NioEventLoopGroup())// 通道Channel.channel(NioSocketChannel.class)// 处理器链Pipline.handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new StringDecoder());}}).connect(new InetSocketAddress("localhost", 8080)).sync().channel().writeAndFlush("hello world");}
}
可以先看看这两段代码
重点理清楚使用时,我们要:
IO线程 -> Selector -> channel -> Pipline
其中IO线程 + Selectot 属于EventLoop
因此:
ch.pipeline().addLast(new StringDecoder());}}).connect(new InetSocketAddress("localhost", 8080)).sync().channel().writeAndFlush("hello world");
}
}
可以先看看这两段代码重点理清楚使用时,我们要:IO线程 -> Selector -> channel -> Pipline其中IO线程 + Selectot 属于EventLoop因此:**EventLoop -> Channel -> Pipline**
