Netty 4.1 入门学习笔记
参考博客:bugstack虫洞栈
本笔记基于 Netty 4.1 版本,通过 12 个渐进式项目,从零开始学习 Netty 网络编程框架。
代码来源:https://github.com/fuzhengwei/itstack-demo-netty
📚 学习路线图
Netty入门学习路线
│
├─ 阶段一:服务端基础(项目 1-01 ~ 1-06)
│ ├─ 1-01:搭建最基础的Netty服务端
│ ├─ 1-02:手动解码接收数据
│ ├─ 1-03:使用内置解码器
│ ├─ 1-04:手动编码发送数据
│ ├─ 1-05:使用内置编码器
│ └─ 1-06:实现群发消息功能
│
├─ 阶段二:客户端实现(项目 1-07 ~ 1-08)
│ ├─ 1-07:实现基础Netty客户端
│ └─ 1-08:实现完整的收发功能
│
└─ 阶段三:高级特性(项目 1-09 ~ 1-12)
├─ 1-09:自定义编解码器
├─ 1-10:入站/出站处理器
├─ 1-11:UDP通信实现
└─ 1-12:HTTP服务器实现
📖 目录
阶段一:服务端基础
在这个阶段,我们将从零开始搭建Netty服务端,逐步学习数据的接收和发送,最终实现一个功能完整的服务端。
项目 1-01:搭建最基础的Netty服务端
📌 项目目标
搭建一个最基础的Netty服务端,理解Netty的启动流程和核心组件。此时的服务端只能监听连接,不能接收数据、不能向客户端发送数据、没有业务处理逻辑。
🔑 核心知识点
-
EventLoopGroup:事件循环组,负责处理I/O操作
parentGroup(Boss线程组):负责接收客户端连接childGroup(Worker线程组):负责处理已建立连接的I/O操作
-
ServerBootstrap:服务端启动辅助类,用于配置服务端参数
-
Channel:网络通信的载体,代表一个连接
-
ChannelInitializer:Channel初始化器,用于配置Pipeline
-
ChannelHandler:业务处理器,处理I/O事件
💻 核心代码解析
NettyServer.java - 服务端启动类
public class NettyServer {
public static void main(String[] args) {
new NettyServer().bing(7397);
}
private void bing(int port) {
// ============ 第一步:创建线程组 ============
// parentGroup(Boss线程组):负责接收客户端的连接请求
// 默认线程数 = CPU核心数 * 2
// 作用:类似餐厅的迎宾员,专门负责迎接新客人(接收新连接)
EventLoopGroup parentGroup = new NioEventLoopGroup();
// childGroup(Worker线程组):负责处理已建立连接的IO读写操作
// 默认线程数 = CPU核心数 * 2
// 作用:类似餐厅的服务员,负责为客人提供服务(处理数据读写)
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
// ============ 第二步:创建服务端启动助手 ============
ServerBootstrap b = new ServerBootstrap();
// ============ 第三步:配置服务端参数 ============
b.group(parentGroup, childGroup) // 配置主从Reactor线程模型
.channel(NioServerSocketChannel.class) // 设置NIO模式的ServerSocketChannel(非阻塞)
.option(ChannelOption.SO_BACKLOG, 128) // 设置服务端参数:TCP连接队列大小为128
.childHandler(new MyChannelInitializer()); // 设置子通道的处理器(每个客户端连接都会创建一个)
// ============ 第四步:绑定端口并启动服务 ============
// bind(port):异步绑定端口
// sync():同步等待绑定完成,如果不调用sync(),bind()会立即返回
ChannelFuture f = b.bind(port).sync();
System.out.println("服务端启动成功,端口:" + port);
// ============ 第五步:等待服务端监听端口关闭 ============
// closeFuture():获取Channel关闭的Future对象
// sync():阻塞等待服务器关闭(如果不调用,程序会立即执行到finally导致服务关闭)
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// ============ 第六步:优雅关闭线程组,释放资源 ============
// shutdownGracefully():优雅关闭,会等待任务执行完成
childGroup.shutdownGracefully();
parentGroup.shutdownGracefully();
}
}
}
MyChannelInitializer.java - Channel初始化器
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) {
// 此时Pipeline中没有添加任何Handler
// 所以服务端只能接收连接,不能处理数据
}
}
🎯 运行效果
- 启动服务端后,可以使用Telnet或Socket客户端连接
- 服务端能够接收连接,但不会有任何响应
- 这是最基础的Netty服务端,为后续功能打下基础
📊 知识演进
项目1-01:只能监听连接
↓
项目1-02:增加数据接收功能
项目 1-02:手动解码接收数据
📌 项目目标
在1-01项目的基础上,添加数据接收功能,学习如何处理客户端发送的数据。通过手动处理ByteBuf,理解数据解码的底层原理。
🔑 核心知识点
-
ByteBuf:Netty的字节缓冲区,类似于Java NIO的ByteBuffer,但更强大
readableBytes():获取可读字节数readBytes(byte[]):读取字节到数组
-
channelRead():当Channel有数据可读时触发
-
手动解码:将ByteBuf转换为业务需要的数据类型
💻 核心代码解析
MyServerHandler.java - 业务处理器
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 第一步:将msg转换为ByteBuf(Netty的字节缓冲区)
// ByteBuf类似于Java NIO的ByteBuffer,但功能更强大
ByteBuf buf = (ByteBuf) msg;
// 第二步:创建字节数组,大小为可读字节数
// readableBytes():返回当前可读的字节数
byte[] msgByte = new byte[buf.readableBytes()];
// 第三步:从ByteBuf中读取数据到字节数组
// readBytes():读取数据并移动读指针
buf.readBytes(msgByte);
// 第四步:将字节数组转换为字符串并打印
System.out.print(new Date() + " 接收到消息:");
System.out.println(new String(msgByte, Charset.forName("UTF-8")));
}
MyChannelInitializer.java - 添加Handler
@Override
protected void initChannel(SocketChannel channel) {
// 将自定义的Handler添加到Pipeline中
channel.pipeline().addLast(new MyServerHandler());
}
🎯 运行效果
- 客户端发送消息后,服务端能够接收并打印
- 服务端控制台输出:
服务端接收到消息:Hello Netty
📊 与项目1-01的对比
| 功能 | 项目1-01 | 项目1-02 |
|---|---|---|
| 接收连接 | ✅ | ✅ |
| 接收数据 | ❌ | ✅ |
| 发送数据 | ❌ | ❌ |
| 解码方式 | - | 手动解码 |
💡 存在的问题
- 手动处理ByteBuf比较繁琐
- 需要自己处理字符编码
- 没有处理TCP粘包/拆包问题
项目 1-03:使用内置解码器
📌 项目目标
在1-02项目基础上引入解码器,使用Netty内置的解码器,让代码更简洁优雅。直接接收String对象而不是ByteBuf,学习Netty常用解码器,解决TCP粘包/拆包问题。
🔑 核心知识点
-
LineBasedFrameDecoder:基于换行符的解码器
- 自动处理TCP粘包/拆包问题
- 以
\n或\r\n为分隔符
-
StringDecoder:字符串解码器
- 将ByteBuf自动转换为String
- 支持指定字符编码
-
Pipeline处理顺序:解码器 → 业务Handler
💻 核心代码解析
MyChannelInitializer.java - 添加解码器
@Override
protected void initChannel(SocketChannel channel) {
/* ============ 配置解码器(核心变化)============ */
// 第一步:添加帧解码器 - LineBasedFrameDecoder(基于换行符的帧解码器)
// 作用:解决TCP粘包/拆包问题,按换行符(\n或\r\n)分割数据帧
// 参数1024:单行最大长度,超过会抛异常(防止恶意攻击)
// 工作原理:接收 "Hello\nWorld\n" → 输出 "Hello" 和 "World" 两个独立的帧
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 其他可选的帧解码器(根据实际需求选择):
// 1. 基于指定分隔符:DelimiterBasedFrameDecoder
// channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, false, Delimiters.lineDelimiter()));
// 2. 基于固定长度:FixedLengthFrameDecoder(每4个字节一个帧)
// channel.pipeline().addLast(new FixedLengthFrameDecoder(4));
// 3. 基于长度字段:LengthFieldBasedFrameDecoder(常用于自定义协议)
// 第二步:添加字符串解码器 - StringDecoder
// 作用:将ByteBuf自动转换为String对象
// 参数:字符编码(GBK或UTF-8),需要与客户端编码一致
// 工作原理:ByteBuf → String(自动完成,不需要手动转换)
channel.pipeline().addLast(new StringDecoder(Charset.forName("GBK")));
// 第三步:添加业务处理器
// 此时channelRead()方法接收到的msg已经是String类型
channel.pipeline().addLast(new MyServerHandler());
}
MyServerHandler.java - 简化的业务处理器
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// msg已经是String类型,可以直接使用
// 解码器链:ByteBuf → LineBasedFrameDecoder → StringDecoder → String
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+ " 接收到消息:" + msg);
}
🎯 运行效果
- 代码更简洁,不需要手动处理ByteBuf
- 自动解决TCP粘包/拆包问题
- 直接接收String对象
📊 与项目1-02的对比
| 功能 | 项目1-02 | 项目1-03 |
|---|---|---|
| 解码方式 | 手动解码 | 内置解码器 |
| 代码复杂度 | 高 | 低 |
| 粘包/拆包 | 未处理 | 自动处理 |
| 接收类型 | ByteBuf | String |
💡 常用解码器
LineBasedFrameDecoder:基于换行符DelimiterBasedFrameDecoder:基于自定义分隔符FixedLengthFrameDecoder:固定长度LengthFieldBasedFrameDecoder:基于长度字段
项目 1-04:手动编码发送数据
📌 项目目标
在1-03项目基础上增加了发送消息功能,学习如何向客户端发送数据。此时没有引入编码器,需要手动转换类型。
🔑 核心知识点
-
ChannelHandlerContext.writeAndFlush():发送数据并刷新缓冲区
write():写入缓冲区flush():刷新缓冲区,真正发送数据writeAndFlush():写入并刷新
-
Unpooled.copiedBuffer():创建ByteBuf
- 将字符串转换为ByteBuf
-
手动编码:将业务数据转换为ByteBuf
💻 核心代码解析
MyServerHandler.java - 发送数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 1. 接收客户端消息
String message = (String) msg;
System.out.println("服务端接收到消息:" + message);
// 2. 手动编码:将String转换为ByteBuf
String response = "服务端收到:" + message + "\r\n";
ByteBuf buf = Unpooled.copiedBuffer(response.getBytes(Charset.forName("GBK")));
// 3. 发送数据
ctx.writeAndFlush(buf);
}
🎯 运行效果
- 服务端接收到消息后,会回复客户端
- 客户端能够收到服务端的响应
📊 与项目1-03的对比
| 功能 | 项目1-03 | 项目1-04 |
|---|---|---|
| 接收数据 | ✅ | ✅ |
| 发送数据 | ❌ | ✅ |
| 编码方式 | - | 手动编码 |
💡 存在的问题
- 手动编码比较繁琐
- 需要自己创建ByteBuf
- 代码不够优雅
项目 1-05:使用内置编码器
📌 项目目标
在1-04项目基础上引入编码器,实现收发数据都使用String,代码达到最简洁优雅的状态。
🔑 核心知识点
-
StringEncoder:字符串编码器
- 将String自动转换为ByteBuf
- 支持指定字符编码
-
编解码器配置顺序:
- 解码器:LineBasedFrameDecoder → StringDecoder
- 编码器:StringEncoder
-
Pipeline双向处理:
- 入站(Inbound):解码器处理接收的数据
- 出站(Outbound):编码器处理发送的数据
💻 核心代码解析
MyChannelInitializer.java - 添加编码器
@Override
protected void initChannel(SocketChannel channel) {
/* ============ 配置编解码器(完整版)============ */
// 第一步:添加帧解码器 - LineBasedFrameDecoder
// 作用:解决TCP粘包/拆包问题,按换行符分割数据帧
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 第二步:添加字符串解码器 - StringDecoder(入站)
// 作用:将接收到的ByteBuf自动转换为String
// 处理方向:ByteBuf → String(客户端发来的数据)
channel.pipeline().addLast(new StringDecoder(Charset.forName("GBK")));
// ⭐ 第三步:添加字符串编码器 - StringEncoder(出站,核心新增)
// 作用:将要发送的String自动转换为ByteBuf
// 处理方向:String → ByteBuf(发送给客户端的数据)
// 优势:不需要手动创建ByteBuf,代码更简洁
channel.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
// 第四步:添加业务处理器
// 此时Handler中可以直接使用String收发数据,无需关心编解码细节
channel.pipeline().addLast(new MyServerHandler());
/* ============ Pipeline完整处理链 ============
*
* 【入站方向 - 接收数据】
* 客户端数据 → LineBasedFrameDecoder(分帧)
* → StringDecoder(ByteBuf转String)
* → MyServerHandler(业务处理)
*
* 【出站方向 - 发送数据】
* MyServerHandler(业务处理)
* → StringEncoder(String转ByteBuf)
* → 网络发送
*/
}
MyServerHandler.java - 简化的发送代码
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 1. 接收消息(已经是String类型)
String message = (String) msg;
System.out.println("服务端接收到消息:" + message);
// 2. 直接发送String,编码器会自动转换为ByteBuf
String response = "服务端收到:" + message + "\r\n";
ctx.writeAndFlush(response);
}
🎯 运行效果
- 代码最简洁优雅
- 收发数据都使用String类型
- 不需要手动处理ByteBuf
📊 与项目1-04的对比
| 功能 | 项目1-04 | 项目1-05 |
|---|---|---|
| 编码方式 | 手动编码 | 内置编码器 |
| 代码复杂度 | 高 | 低 |
| 发送类型 | ByteBuf | String |
💡 最佳实践
- 优先使用Netty内置的编解码器
- 保持代码简洁优雅
- 关注业务逻辑,而不是底层细节
项目 1-06:实现群发消息功能
📌 项目目标
在1-05项目的基础上增加群发消息功能,引入ChannelGroup核心概念,实现群发消息功能。
🔑 核心知识点
-
ChannelGroup:Channel组,用于管理多个Channel
- 可以同时向多个Channel发送消息
- 自动管理Channel的生命周期
-
DefaultChannelGroup:ChannelGroup的默认实现
-
群发消息:向所有连接的客户端发送消息
💻 核心代码解析
ChannelHandler.java - 管理Channel组
public class ChannelHandler extends ChannelInboundHandlerAdapter {
// 创建Channel组,用于管理所有连接的客户端
private static ChannelGroup channelGroup = new DefaultChannelGroup(
GlobalEventExecutor.INSTANCE
);
}
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 连接激活时调用
*
* ⭐ 核心变化:将新连接加入到ChannelGroup
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// ⭐ 第一步:将新连接的Channel加入到channelGroup(加入群聊)
// 之后这个Channel就可以接收群发消息了
ChannelHandler.channelGroup.add(ctx.channel());
// 第二步:打印连接信息
SocketChannel channel = (SocketChannel) ctx.channel();
System.out.println("链接报告开始");
System.out.println("链接报告信息:有一客户端链接到本服务端");
System.out.println("链接报告IP:" + channel.localAddress().getHostString());
System.out.println("链接报告Port:" + channel.localAddress().getPort());
System.out.println("链接报告完毕");
System.out.println("当前在线人数:" + ChannelHandler.channelGroup.size());
// 第三步:通知客户端连接成功
String str = "通知客户端链接建立成功" + " " + new Date() + " "
+ channel.localAddress().getHostString() + "\r\n";
ctx.writeAndFlush(str);
}
/**
* 连接断开时调用
*
* ⭐ 核心变化:从ChannelGroup中移除断开的连接
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端断开链接" + ctx.channel().localAddress().toString());
// ⭐ 从channelGroup中移除(退出群聊)
// 之后这个Channel就不会再收到群发消息了
ChannelHandler.channelGroup.remove(ctx.channel());
System.out.println("当前在线人数:" + ChannelHandler.channelGroup.size());
}
/**
* 接收数据时调用
*
* ⭐⭐⭐ 核心变化:群发消息给所有客户端
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接收消息
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+ " 接收到消息:" + msg);
// ⭐⭐⭐ 核心功能:群发消息
//
// 项目五(点对点):
// ctx.writeAndFlush(str); // 只发给当前发送消息的客户端
//
// 项目六(群发):
// ChannelHandler.channelGroup.writeAndFlush(str); // 发给所有客户端
//
// 效果:
// 客户端A发送消息 → 服务端接收 → 群发给A、B、C所有客户端
String str = "服务端收到:" + new Date() + " " + msg + "\r\n";
ChannelHandler.channelGroup.writeAndFlush(str);
}
/**
* 异常处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常信息:\r\n" + cause.getMessage());
}
}
🎯 运行效果
- 启动多个客户端连接服务端
- 任意客户端发送消息,所有客户端都能收到
- 实现了简单的聊天室功能
📊 与项目1-05的对比
| 功能 | 项目1-05 | 项目1-06 |
|---|---|---|
| 发送方式 | 单播(一对一) | 群发(一对多) |
| Channel管理 | 无 | ChannelGroup |
| 应用场景 | 简单通信 | 聊天室、推送 |
💡 应用场景
- 聊天室:群发聊天消息
- 消息推送:向所有在线用户推送通知
- 游戏服务器:广播游戏状态
883



被折叠的 条评论
为什么被折叠?



