从零到精通:NIO、Netty与WebSocket全解(超详细保姆级教程)
本文用最通俗的语言,最完整的案例,带你从零掌握高并发网络编程核心。每一行代码都有详细注释,每一个概念都用生活化比喻解释。
引言:为什么传统IO会让我们头疼?
想象一下你开了一家只有3个柜台的银行(服务器),采用传统的一对一服务模式(BIO):
// 这就是传统BIO(Blocking IO)的工作方式
public class BioBank {
public static void main(String[] args) throws IOException {
// 银行开门(服务器启动)
ServerSocket bank = new ServerSocket(8888);
System.out.println("银行开门营业,3个柜台...");
while (true) {
// 顾客来了(客户端连接)
Socket customer = bank.accept(); // 这里会阻塞!没有顾客就干等着
System.out.println("新顾客来了,分配一个柜台...");
// 每个顾客分配一个专门的柜员(线程)
new Thread(() -> {
try {
// 柜员开始服务(处理业务)
serveCustomer(customer);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
static void serveCustomer(Socket customer) throws IOException {
// 模拟业务办理(读取请求,处理,返回响应)
BufferedReader in = new BufferedReader(new InputStreamReader(customer.getInputStream()));
PrintWriter out = new PrintWriter(customer.getOutputStream(), true);
String request;
while ((request = in.readLine()) != null) { // 这里也会阻塞!
System.out.println("办理业务: " + request);
// 模拟耗时业务
Thread.sleep(1000); // 假设每笔业务需要1秒
out.println("业务办理完成: " + request);
}
customer.close();
System.out.println("顾客离开");
}
}
问题来了:
-
第100个顾客来了,但只有3个柜台怎么办?→ 排队,体验差
-
柜员给一个顾客办业务,这个顾客慢慢填表,柜员只能干等→ 资源浪费
-
顾客太多,创建太多线程→ 内存耗尽,系统崩溃
这就是现实:一个Tomcat默认200个线程,2000人同时访问就卡死。
一、NIO:银行的叫号系统
1.1 什么是NIO?
NIO(Non-blocking IO,非阻塞IO)就像银行的叫号系统:
-
一个大堂经理(一个线程)管理所有顾客
-
顾客取号后去旁边等待(非阻塞)
-
哪个顾客准备好了(数据就绪),经理就叫哪个
// 叫号系统的核心组件
public class NioCoreComponents {
public static void main(String[] args) {
System.out.println("NIO三大核心组件:");
System.out.println("1. Selector(选择器)- 大堂经理");
System.out.println("2. Channel(通道)- 办理窗口");
System.out.println("3. Buffer(缓冲区)- 业务申请表");
}
}
1.2 Buffer(缓冲区)- 数据的"申请表"
为什么需要Buffer?
传统IO是流(水流),数据来了就得马上处理。NIO是块(砖头),数据可以暂存起来。
public class BufferDetailDemo {
public static void main(String[] args) {
System.out.println("========= Buffer深度解析 =========");
// 创建一个10字节的缓冲区(就像10个格子的申请表)
ByteBuffer buffer = ByteBuffer.allocate(10);
printBuffer("初始状态", buffer); // 位置=0, 限制=10, 容量=10
System.out.println("\n1. 写入数据(顾客填表):");
buffer.put("Hello".getBytes()); // 写入5个字节
printBuffer("写入5字节后", buffer); // 位置=5, 限制=10, 容量=10
System.out.println("\n2. 切换为读模式(柜员查看申请表):");
buffer.flip(); // 翻转!这是关键操作
printBuffer("flip()之后", buffer); // 位置=0, 限制=5, 容量=10
System.out.println("\n3. 读取数据(柜员读取申请表内容):");
byte[] data = new byte[buffer.limit()];
buffer.get(data); // 读取数据
System.out.println("读取的内容: " + new String(data));
printBuffer("读取后", buffer); // 位置=5, 限制=5, 容量=10
System.out.println("\n4. 清空缓冲区(业务办完,表格回收):");
buffer.clear(); // 注意:clear不会删除数据,只是重置指针
printBuffer("clear()之后", buffer); // 位置=0, 限制=10, 容量=10
System.out.println("\n5. compact压缩缓冲区(部分处理,保留未处理数据):");
buffer.put("HelloWorld".getBytes()); // 写入10字节
buffer.flip();
buffer.get(new byte[5]); // 读取前5字节
printBuffer("读取5字节后", buffer); // 位置=5, 限制=10
buffer.compact(); // 把未读的"World"移到开头
printBuffer("compact()之后", buffer); // 位置=5, 限制=10
buffer.put("!!!".getBytes()); // 可以继续写入
printBuffer("再写入3字节", buffer); // 位置=8, 限制=10
}
static void printBuffer(String stage, ByteBuffer buffer) {
System.out.printf("%s: position=%d, limit=%d, capacity=%d%n",
stage, buffer.position(), buffer.limit(), buffer.capacity());
}
}
Buffer四个关键属性:
-
capacity(容量):表格总格子数,创建后不变 -
limit(限制):当前允许操作的最大位置 -
position(位置):下一个要操作的位置 -
mark(标记):临时标记位置,可通过reset()返回
Buffer状态流转图:
+------[初始]------+
| position=0 |
| limit=capacity |
+-----------------+
|
| put()
v
+------[写模式]-----+
| position=写入的数据量 |
| limit=capacity |
+-----------------+
|
| flip()
v
+------[读模式]-----+
| position=0 |
| limit=写入的数据量 |
+-----------------+
|
| get()
v
+------[读取后]-----+
| position=读取的数据量 |
| limit=写入的数据量 |
+-----------------+
|
clear()/compact()
v
+------[重置]------+
| position=0 |
| limit=capacity |
+-----------------+
1.3 Channel(通道)- 数据的"办理窗口"
Channel vs Stream:
-
Stream是单行道(InputStream只能读,OutputStream只能写)
-
Channel是双行道(可读可写),支持异步
public class ChannelDetailDemo {
public static void main(String[] args) throws Exception {
System.out.println("========= Channel类型详解 =========");
System.out.println("\n1. FileChannel - 文件通道:");
fileChannelDemo();
System.out.println("\n2. SocketChannel - 网络套接字通道:");
socketChannelDemo();
System.out.println("\n3. ServerSocketChannel - 服务器监听通道:");
serverSocketChannelDemo();
System.out.println("\n4. DatagramChannel - UDP通道:");
datagramChannelDemo();
}
static void fileChannelDemo() throws IOException {
// 创建测试文件
String content = "Hello, FileChannel!";
Files.write(Paths.get("test.txt"), content.getBytes());
// 使用FileChannel读取文件
try (FileInputStream fis = new FileInputStream("test.txt");
FileChannel channel = fis.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer); // 从通道读取到缓冲区
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("文件内容: " + new String(data));
// 零拷贝演示
try (FileOutputStream fos = new FileOutputStream("copy.txt");
FileChannel destChannel = fos.getChannel()) {
buffer.flip(); // 重置position
destChannel.write(buffer); // 直接从缓冲区写入
}
}
Files.deleteIfExists(Paths.get("test.txt"));
Files.deleteIfExists(Paths.get("copy.txt"));
}
static void socketChannelDemo() throws IOException {
System.out.println("创建SocketChannel的两种方式:");
System.out.println("方式1: SocketChannel.open()");
SocketChannel clientChannel = SocketChannel.open();
System.out.println("\n配置非阻塞模式(关键!):");
clientChannel.configureBlocking(false); // 设为非阻塞
System.out.println("尝试连接到服务器(非阻塞立即返回):");
boolean connected = clientChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
System.out.println("连接结果: " + connected); // 非阻塞模式可能返回false
System.out.println("\n完成连接(在非阻塞模式下需要循环检查):");
while (!clientChannel.finishConnect()) {
System.out.println("连接进行中...可以做其他事");
Thread.sleep(100);
}
System.out.println("\n读写操作都是非阻塞的:");
ByteBuffer buffer = ByteBuffer.wrap("Hello".getBytes());
int bytesWritten = clientChannel.write(buffer); // 非阻塞,立即返回
System.out.println("写入字节数: " + bytesWritten);
clientChannel.close();
}
static void serverSocketChannelDemo() throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("ServerSocketChannel监听中...");
System.out.println("非阻塞模式下,accept()立即返回:");
SocketChannel client = serverChannel.accept(); // 非阻塞,没有连接返回null
System.out.println("accept结果: " + (client == null ? "null" : "有连接"));
serverChannel.close();
}
static void datagramChannelDemo() throws IOException {
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(9999));
System.out.println("UDP通道已绑定到端口9999");
channel.close();
}
}
通道的重要特性:
-
非阻塞模式:
configureBlocking(false)是关键 -
分散/聚集:一次操作多个Buffer
-
文件锁定:FileChannel支持文件锁
-
内存映射:FileChannel.map()实现内存映射文件
1.4 Selector(选择器)- 多路复用器
Selector工作原理(最核心最难理解的部分):
public class SelectorDetailDemo {
public static void main(String[] args) throws IOException {
System.out.println("========= Selector工作原理 =========");
// 创建Selector(大堂经理)
Selector selector = Selector.open();
// 创建ServerSocketChannel(银行门口)
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 必须非阻塞
serverChannel.bind(new InetSocketAddress(8888));
// 注册到Selector,关注ACCEPT事件(新顾客事件)
// register()返回的SelectionKey就是顾客的"号牌"
SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册ServerSocketChannel,关注ACCEPT事件");
// 为这个Key附加一些额外信息(比如顾客的VIP级别)
serverKey.attach("VIP通道");
// 开始处理事件
processEvents(selector);
}
static void processEvents(Selector selector) throws IOException {
System.out.println("\n======= 事件处理循环开始 =======");
while (true) {
System.out.println("\n等待事件发生(select()会阻塞)...");
// select()方法会阻塞,直到有事件发生
// 可以设置超时:selector.select(1000) - 最多等1秒
int readyChannels = selector.select(2000); // 2秒超时
if (readyChannels == 0) {
System.out.println("2秒内没有事件,继续等待...");
continue;
}
System.out.println("有 " + readyChannels + " 个Channel就绪");
// 获取就绪的SelectionKey集合(拿到需要服务的顾客号牌)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
System.out.println("\n处理Key: " + key);
System.out.println("附加信息: " + key.attachment());
// 判断事件类型
if (key.isAcceptable()) {
System.out.println("事件类型: 新连接");
handleAccept(key, selector);
}
if (key.isConnectable()) {
System.out.println("事件类型: 连接完成");
handleConnect(key);
}
if (key.isReadable()) {
System.out.println("事件类型: 可读");
handleRead(key);
}
if (key.isWritable()) {
System.out.println("事件类型: 可写");
handleWrite(key);
}
// 非常重要:处理完必须移除,否则下次还会处理
keyIterator.remove();
System.out.println("Key已从selectedKeys移除");
}
}
}
static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept(); // 不会阻塞
clientChannel.configureBlocking(false);
System.out.println("接受新连接: " + clientChannel.getRemoteAddress());
// 为新连接注册读事件
// 注意:这里只注册了OP_READ,没有注册OP_WRITE
// 因为写事件通常只在需要写时才注册
clientChannel.register(selector, SelectionKey.OP_READ);
// 发送欢迎消息
ByteBuffer welcome = ByteBuffer.wrap("Welcome!\n".getBytes());
clientChannel.write(welcome);
}
static void handleConnect(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
System.out.println("连接建立完成");
// 连接建立后,可以注册读事件
key.interestOps(SelectionKey.OP_READ);
}
}
static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到消息: " + message.trim());
// 回显消息
String response = "Echo: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
channel.write(responseBuffer);
// 如果缓冲区还有空间,可以继续监听读事件
// 如果缓冲区满了,可能需要暂停读事件监听
} else if (bytesRead == -1) {
System.out.println("客户端关闭连接");
channel.close();
key.cancel(); // 取消这个Key
}
}
static void handleWrite(SelectionKey key) throws IOException {
System.out.println("处理写事件...");
// 写完后,通常要取消写事件监听,避免CPU空转
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
}
Selector关键概念:
-
SelectionKey:事件令牌,包含:
-
Channel:关联的通道
-
Selector:关联的选择器
-
InterestOps:感兴趣的事件集合
-
ReadyOps:就绪的事件集合
-
Attachment:附加对象
-
-
事件类型:
-
OP_ACCEPT:新连接,ServerSocketChannel专用
-
OP_CONNECT:连接建立,SocketChannel专用
-
OP_READ:数据可读
-
OP_WRITE:数据可写
-
-
重要方法:
-
select():阻塞等待事件 -
select(timeout):带超时的等待 -
selectNow():立即返回,不阻塞 -
wakeup():唤醒阻塞的select()
-
1.5 完整的NIO服务器示例
public class CompleteNioServer {
private Selector selector;
private ServerSocketChannel serverChannel;
private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private final ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
public void start(int port) throws IOException {
// 1. 创建Selector
selector = Selector.open();
// 2. 创建ServerSocketChannel
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(port));
// 3. 注册ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务器启动在端口 " + port);
// 4. 事件循环
eventLoop();
}
private void eventLoop() throws IOException {
while (true) {
// 等待事件,最多1秒
int readyCount = selector.select(1000);
if (readyCount == 0) {
// 没有事件,可以做一些其他工作
doOtherWork();
continue;
}
// 处理事件
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 必须移除!
try {
if (!key.isValid()) {
continue; // Key已失效
}
if (key.isAcceptable()) {
acceptClient(key);
} else if (key.isReadable()) {
readData(key);
} else if (key.isWritable()) {
writeData(key);
}
} catch (Exception e) {
// 异常处理:关闭通道,取消Key
System.err.println("处理事件异常: " + e.getMessage());
if (key != null) {
key.cancel();
try {
key.channel().close();
} catch (IOException ex) {
// 忽略关闭异常
}
}
}
}
}
}
private void acceptClient(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
System.out.println("接受新连接: " + client.getRemoteAddress());
// 注册读事件,并附加一个缓冲区给这个连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.register(selector, SelectionKey.OP_READ, buffer);
// 发送欢迎消息
String welcome = "Welcome to NIO Server!\n";
ByteBuffer welcomeBuf = ByteBuffer.wrap(welcome.getBytes());
client.write(welcomeBuf);
}
private void readData(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear(); // 清空缓冲区准备读取
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端断开: " + client.getRemoteAddress());
client.close();
key.cancel();
return;
}
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data).trim();
System.out.println("收到: " + message);
// 处理消息
String response = processMessage(message);
// 切换到写模式
key.interestOps(SelectionKey.OP_WRITE);
key.attach(ByteBuffer.wrap(response.getBytes()));
}
}
private void writeData(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
while (buffer.hasRemaining()) {
int written = client.write(buffer);
if (written == 0) {
// TCP缓冲区满,等待下次写事件
break;
}
}
if (!buffer.hasRemaining()) {
// 数据发送完毕,切换回读模式
key.interestOps(SelectionKey.OP_READ);
key.attach(ByteBuffer.allocate(1024)); // 重新分配读缓冲区
}
}
private String processMessage(String message) {
// 简单的业务处理
if ("time".equalsIgnoreCase(message)) {
return "当前时间: " + new Date();
} else if ("quit".equalsIgnoreCase(message)) {
return "BYE";
} else {
return "ECHO: " + message;
}
}
private void doOtherWork() {
// 这里可以执行一些后台任务
// 比如:清理资源、记录日志、监控状态等
// System.out.println("执行其他任务...");
}
public void stop() throws IOException {
if (selector != null) {
selector.close();
}
if (serverChannel != null) {
serverChannel.close();
}
}
public static void main(String[] args) throws IOException {
CompleteNioServer server = new CompleteNioServer();
try {
server.start(8888);
} finally {
server.stop();
}
}
}
二、Netty:NIO的"豪华装修版"
2.1 为什么需要Netty?
原生NIO的问题:
-
API太复杂:Selector、Channel、Buffer各种组合,容易出错
-
需要处理太多细节:拆包粘包、编码解码、异常处理
-
性能优化困难:内存管理、线程模型需要自己实现
-
Bug多:空轮询Bug、epoll Bug等
Netty的优势:
-
简单:封装了NIO的复杂性
-
高性能:优化的线程模型,零拷贝
-
稳定:经过大规模生产验证(阿里、Facebook、Twitter都在用)
-
功能全:支持HTTP、WebSocket、ProtoBuf等多种协议
2.2 Netty核心组件详解
public class NettyComponentsDetail {
public static void main(String[] args) {
System.out.println("========= Netty核心组件 =========");
System.out.println("\n1. Channel - 通信的管道");
System.out.println(" 类似SocketChannel,但功能更强");
System.out.println(" 类型:NioSocketChannel, NioServerSocketChannel");
System.out.println("\n2. EventLoop - 事件循环(核心中的核心)");
System.out.println(" 一个EventLoop = 一个线程 + 一个Selector");
System.out.println(" 负责处理:IO事件、定时任务、普通任务");
System.out.println("\n3. ChannelPipeline - 处理器管道");
System.out.println(" 一系列ChannelHandler的链式处理");
System.out.println(" 数据像流水一样经过每个Handler");
System.out.println("\n4. ChannelHandler - 业务处理器");
System.out.println(" 处理具体的业务逻辑");
System.out.println(" 分两种:Inbound(入站)、Outbound(出站)");
System.out.println("\n5. ByteBuf - 增强的缓冲区");
System.out.println(" 比ByteBuffer更好用,支持引用计数、池化");
}
}
2.3 Netty线程模型(最核心!)
public class NettyThreadModel {
public static void main(String[] args) {
System.out.println("========= Netty线程模型详解 =========");
System.out.println("\n传统模型的问题:");
System.out.println(" 一个连接一个线程 → 线程太多");
System.out.println(" 线程池模型 → 上下文切换开销大");
System.out.println("\nNetty的解决方案:Reactor模式");
System.out.println(" Boss Group:1个线程,负责接受连接");
System.out.println(" Worker Group:N个线程,负责处理IO");
System.out.println(" 一个Worker线程管理多个连接");
System.out.println("\n为什么这样设计?");
System.out.println(" 1. 连接建立是低频操作,1个线程足够");
System.out.println(" 2. IO操作是高频操作,需要多个线程");
System.out.println(" 3. 一个连接固定在一个线程,避免并发问题");
System.out.println("\n线程数设置建议:");
System.out.println(" Boss线程数 = 1(通常足够)");
System.out.println(" Worker线程数 = CPU核心数 * 2");
}
}
2.4 完整Netty服务器(带详细注释)
/**
* 完整的Netty服务器示例
* 包含:心跳检测、编解码、业务处理、异常处理
*/
public class CompleteNettyServer {
public static void main(String[] args) throws InterruptedException {
System.out.println("开始启动Netty服务器...");
// ========== 第一步:创建线程组 ==========
// BossGroup:负责接受客户端连接
// 参数1:线程数,通常1个就够了(除非有多个网卡或多个端口)
EventLoopGroup bossGroup = new NioEventLoopGroup(1, new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "BOSS-" + counter.incrementAndGet());
}
});
// WorkerGroup:负责处理IO操作
// 参数0:使用默认线程数(CPU核心数*2)
EventLoopGroup workerGroup = new NioEventLoopGroup(0, new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "WORKER-" + counter.incrementAndGet());
thread.setDaemon(true); // 设置为守护线程
return thread;
}
});
try {
// ========== 第二步:创建服务器引导类 ==========
ServerBootstrap bootstrap = new ServerBootstrap();
// ========== 第三步:配置参数 ==========
bootstrap.group(bossGroup, workerGroup) // 设置线程组
.channel(NioServerSocketChannel.class) // 使用NIO传输
// BossGroup配置
.option(ChannelOption.SO_BACKLOG, 128) // 连接队列大小
.option(ChannelOption.SO_REUSEADDR, true) // 端口重用
// WorkerGroup配置
.childOption(ChannelOption.TCP_NODELAY, true) // 禁用Nagle算法
.childOption(ChannelOption.SO_KEEPALIVE, true) // 保持连接
.childOption(ChannelOption.SO_RCVBUF, 32 * 1024) // 接收缓冲区
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024) // 发送缓冲区
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) // 使用内存池
// ========== 第四步:设置处理器 ==========
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 获取管道(数据处理的流水线)
ChannelPipeline pipeline = ch.pipeline();
System.out.println("初始化客户端通道: " + ch.remoteAddress());
// ===== 添加处理器(顺序很重要!) =====
// 1. 空闲检测(5秒未读触发)
pipeline.addLast("idleStateHandler",
new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
// 2. 心跳处理器(处理空闲事件)
pipeline.addLast("heartbeatHandler", new HeartbeatHandler());
// 3. 解决TCP粘包/拆包问题
// 按行分割(\n或\r\n)
pipeline.addLast("lineBasedFrameDecoder",
new LineBasedFrameDecoder(1024));
// 4. 字符串解码器(ByteBuf → String)
pipeline.addLast("stringDecoder",
new StringDecoder(CharsetUtil.UTF_8));
// 5. 字符串编码器(String → ByteBuf)
pipeline.addLast("stringEncoder",
new StringEncoder(CharsetUtil.UTF_8));
// 6. 业务处理器
pipeline.addLast("businessHandler", new BusinessHandler());
// 7. 异常处理器(放在最后,捕获所有异常)
pipeline.addLast("exceptionHandler", new ExceptionHandler());
}
});
// ========== 第五步:绑定端口 ==========
System.out.println("绑定端口8888...");
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("服务器启动成功!");
// ========== 第六步:注册关闭钩子 ==========
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\n收到关闭信号,优雅关闭服务器...");
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
System.out.println("服务器已关闭");
}));
// ========== 第七步:等待服务器关闭 ==========
future.channel().closeFuture().sync();
} finally {
// ========== 第八步:优雅关闭 ==========
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* 心跳处理器
*/
static class HeartbeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
System.out.println("5秒未收到心跳,发送心跳包");
ctx.writeAndFlush("HEARTBEAT_REQUEST\n");
}
} else {
ctx.fireUserEventTriggered(evt);
}
}
}
/**
* 业务处理器
*/
static class BusinessHandler extends SimpleChannelInboundHandler<String> {
// 连接建立时调用
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("客户端连接: " + ctx.channel().remoteAddress());
ctx.writeAndFlush("欢迎连接Netty服务器!\n输入'help'查看帮助\n");
}
// 收到消息时调用
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到消息[" + ctx.channel().remoteAddress() + "]: " + msg);
// 处理消息
String response = processCommand(msg.trim());
// 发送响应
ctx.writeAndFlush(response + "\n");
}
// 连接断开时调用
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("客户端断开: " + ctx.channel().remoteAddress());
}
// 处理命令
private String processCommand(String command) {
switch (command.toLowerCase()) {
case "help":
return "可用命令:\n" +
" time - 查看服务器时间\n" +
" echo <message> - 回声测试\n" +
" stats - 查看服务器状态\n" +
" quit - 断开连接";
case "time":
return "服务器时间: " + new SimpleDateFormat("HH:mm:ss").format(new Date());
case "stats":
return "服务器状态: 运行正常\n连接数: " +
ChannelManager.getConnectionCount();
case "quit":
return "BYE";
default:
if (command.startsWith("echo ")) {
return command.substring(5);
}
return "未知命令,输入'help'查看帮助";
}
}
}
/**
* 异常处理器
*/
static class ExceptionHandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException) {
System.out.println("客户端异常断开: " + cause.getMessage());
} else {
System.err.println("处理异常: " + cause.getMessage());
cause.printStackTrace();
}
ctx.close();
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) {
// 处理写缓冲区满的情况
if (!ctx.channel().isWritable()) {
System.out.println("写缓冲区满,暂停接收数据");
// 可以暂停读取,避免OOM
ctx.channel().config().setAutoRead(false);
} else {
System.out.println("写缓冲区可用,恢复接收数据");
ctx.channel().config().setAutoRead(true);
}
}
}
/**
* 连接管理器
*/
static class ChannelManager {
private static final ConcurrentMap<String, Channel> channels =
new ConcurrentHashMap<>();
public static void addChannel(Channel channel) {
channels.put(channel.id().asShortText(), channel);
}
public static void removeChannel(Channel channel) {
channels.remove(channel.id().asShortText());
}
public static int getConnectionCount() {
return channels.size();
}
}
}
2.5 Netty客户端(带重连机制)
public class CompleteNettyClient {
private EventLoopGroup group;
private Bootstrap bootstrap;
private Channel channel;
private final String host;
private final int port;
private volatile boolean reconnect = true;
private int reconnectAttempts = 0;
private static final int MAX_RECONNECT_ATTEMPTS = 5;
public CompleteNettyClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws InterruptedException {
System.out.println("启动Netty客户端...");
group = new NioEventLoopGroup(1, new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "CLIENT-" + counter.incrementAndGet());
}
});
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 添加处理器(顺序要和服务端对应)
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new ClientHandler());
}
});
// 连接服务器
connect();
// 启动控制台输入
startConsoleInput();
}
private void connect() {
if (!reconnect) {
return;
}
System.out.println("连接服务器 " + host + ":" + port + "...");
ChannelFuture future = bootstrap.connect(host, port);
future.addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) {
System.out.println("连接服务器成功!");
channel = f.channel();
reconnectAttempts = 0; // 重置重连次数
} else {
System.err.println("连接服务器失败: " + f.cause().getMessage());
if (reconnect && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
System.out.println("第" + reconnectAttempts + "次重连,3秒后重试...");
// 定时重连
f.channel().eventLoop().schedule(this::connect, 3, TimeUnit.SECONDS);
} else {
System.err.println("重连次数超过限制,停止重连");
stop();
}
}
});
}
private void startConsoleInput() {
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
try {
if (!scanner.hasNextLine()) {
Thread.sleep(100);
continue;
}
String line = scanner.nextLine().trim();
if ("quit".equalsIgnoreCase(line)) {
System.out.println("退出客户端");
stop();
break;
}
if (channel != null && channel.isActive()) {
channel.writeAndFlush(line + "\n");
} else {
System.err.println("未连接到服务器,无法发送消息");
}
} catch (Exception e) {
e.printStackTrace();
}
}
scanner.close();
}, "Console-Input").start();
}
public void sendMessage(String message) {
if (channel != null && channel.isActive()) {
channel.writeAndFlush(message + "\n");
}
}
public void stop() {
reconnect = false;
if (channel != null) {
channel.close();
}
if (group != null) {
group.shutdownGracefully();
}
System.out.println("客户端已停止");
}
static class ClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("服务器回复: " + msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("已连接到服务器");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("与服务器断开连接");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.println("客户端异常: " + cause.getMessage());
ctx.close();
}
}
public static void main(String[] args) throws InterruptedException {
CompleteNettyClient client = new CompleteNettyClient("127.0.0.1", 8888);
// 注册关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(client::stop));
client.start();
}
}
三、WebSocket:双向通信的终极方案
3.1 HTTP的局限性 vs WebSocket的优势
HTTP轮询的问题(传统方案):
// 客户端每1秒轮询一次
public class HttpPollingClient {
public void pollServer() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
try {
// 1. 创建HTTP连接(每次都要握手)
URL url = new URL("http://server.com/check");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 2. 发送请求(即使没有新消息)
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String response = in.readLine();
// 3. 处理响应
if (!"no new message".equals(response)) {
processMessage(response);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}, 0, 1, TimeUnit.SECONDS); // 每秒请求一次
}
}
问题分析:
-
浪费带宽:90%的请求可能都是"没有新消息"
-
延迟高:最坏情况要等1秒才收到消息
-
服务器压力大:每秒都要处理大量请求
WebSocket的优势:
-
一次握手,持久连接:建立连接后一直保持
-
双向通信:服务器可以主动推送
-
低延迟:消息实时到达
-
节省资源:没有重复的HTTP头
3.2 WebSocket协议详解
握手过程(HTTP升级):
客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
数据帧格式:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
帧类型(opcode):
-
0x0:继续帧(数据分片)
-
0x1:文本帧
-
0x2:二进制帧
-
0x8:关闭帧
-
0x9:Ping帧
-
0xA:Pong帧
3.3 完整的WebSocket聊天室
/**
* 完整的WebSocket聊天室服务器
* 功能:用户管理、房间管理、消息广播、历史记录
*/
public class CompleteWebSocketChatServer {
public static void main(String[] args) throws InterruptedException {
System.out.println("启动WebSocket聊天室服务器...");
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// ===== HTTP处理器 =====
// WebSocket基于HTTP,首先处理HTTP请求
pipeline.addLast("httpCodec", new HttpServerCodec());
// 聚合HTTP请求(WebSocket握手请求是一个完整的HTTP请求)
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
// ===== WebSocket处理器 =====
// 处理WebSocket握手和协议升级
pipeline.addLast("webSocketHandler", new WebSocketServerProtocolHandler(
"/ws", // WebSocket路径
null, // 子协议
true, // 允许扩展
65536, // 最大帧大小
false, // 不验证掩码(客户端必须掩码)
true, // 强制关闭超时
10000L // 握手超时10秒
));
// WebSocket数据压缩
pipeline.addLast("compression", new WebSocketServerCompressionHandler());
// ===== 业务处理器 =====
pipeline.addLast("chatHandler", new WebSocketChatHandler());
// ===== 异常处理器 =====
pipeline.addLast("exceptionHandler", new ExceptionHandler());
}
});
// 绑定端口
ChannelFuture future = bootstrap.bind(8080).sync();
System.out.println("WebSocket服务器启动在 ws://localhost:8080/ws");
System.out.println("HTTP管理界面: http://localhost:8080");
// 等待关闭
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* WebSocket聊天处理器
*/
static class WebSocketChatHandler extends SimpleChannelInboundHandler<Object> {
// 使用ChannelGroup管理所有连接
private static final ChannelGroup allChannels =
new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 用户信息映射
private static final Map<ChannelId, UserInfo> users =
new ConcurrentHashMap<>();
// 聊天室映射
private static final Map<String, ChatRoom> rooms =
new ConcurrentHashMap<>();
static {
// 初始化默认聊天室
rooms.put("general", new ChatRoom("general", "综合聊天室"));
rooms.put("tech", new ChatRoom("tech", "技术交流"));
rooms.put("game", new ChatRoom("game", "游戏天地"));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
Channel incoming = ctx.channel();
allChannels.add(incoming);
System.out.println("新连接: " + incoming.remoteAddress());
// 发送欢迎消息
sendWelcomeMessage(incoming);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
Channel leaving = ctx.channel();
allChannels.remove(leaving);
UserInfo user = users.remove(leaving.id());
if (user != null) {
System.out.println("用户离开: " + user.getUsername());
// 通知其他用户
broadcastSystemMessage(user.getUsername() + " 离开了聊天室");
// 从聊天室移除
if (user.getCurrentRoom() != null) {
ChatRoom room = rooms.get(user.getCurrentRoom());
if (room != null) {
room.removeMember(user.getUsername());
}
}
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof FullHttpRequest) {
// 处理HTTP请求(比如管理界面)
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
// 处理WebSocket帧
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
// 如果是WebSocket握手请求,交给后续处理器
if (isWebSocketUpgrade(req)) {
ctx.fireChannelRead(req.retain());
return;
}
// 否则提供管理界面
if (req.uri().equals("/")) {
sendHtmlPage(ctx, req);
} else if (req.uri().equals("/admin")) {
sendAdminPage(ctx, req);
} else {
send404(ctx, req);
}
}
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
// 关闭帧
if (frame instanceof CloseWebSocketFrame) {
handleCloseFrame(ctx, (CloseWebSocketFrame) frame);
return;
}
// Ping帧(回复Pong)
if (frame instanceof PingWebSocketFrame) {
handlePingFrame(ctx, (PingWebSocketFrame) frame);
return;
}
// Pong帧(心跳回复)
if (frame instanceof PongWebSocketFrame) {
handlePongFrame(ctx, (PongWebSocketFrame) frame);
return;
}
// 只处理文本帧和二进制帧
if (frame instanceof TextWebSocketFrame) {
handleTextFrame(ctx, (TextWebSocketFrame) frame);
} else if (frame instanceof BinaryWebSocketFrame) {
handleBinaryFrame(ctx, (BinaryWebSocketFrame) frame);
} else {
throw new UnsupportedOperationException(
"不支持帧类型: " + frame.getClass().getName());
}
}
private void handleCloseFrame(ChannelHandlerContext ctx, CloseWebSocketFrame frame) {
System.out.println("收到关闭帧,关闭连接");
ctx.close();
}
private void handlePingFrame(ChannelHandlerContext ctx, PingWebSocketFrame frame) {
System.out.println("收到Ping,回复Pong");
ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
}
private void handlePongFrame(ChannelHandlerContext ctx, PongWebSocketFrame frame) {
System.out.println("收到Pong,连接正常");
// 可以更新最后活跃时间
}
private void handleTextFrame(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
String request = frame.text();
System.out.println("收到文本消息: " + request);
try {
// 解析JSON消息
JsonNode json = JsonUtil.parse(request);
String type = json.get("type").asText();
switch (type) {
case "login":
handleLogin(ctx, json);
break;
case "join":
handleJoinRoom(ctx, json);
break;
case "message":
handleChatMessage(ctx, json);
break;
case "leave":
handleLeaveRoom(ctx, json);
break;
case "list_users":
handleListUsers(ctx, json);
break;
case "list_rooms":
handleListRooms(ctx);
break;
default:
sendError(ctx, "未知消息类型: " + type);
}
} catch (Exception e) {
sendError(ctx, "消息格式错误: " + e.getMessage());
}
}
private void handleBinaryFrame(ChannelHandlerContext ctx, BinaryWebSocketFrame frame) {
// 处理二进制数据(比如图片、文件)
System.out.println("收到二进制数据,大小: " + frame.content().readableBytes());
// 可以保存文件或转发给其他用户
// 这里简单回复确认
ctx.writeAndFlush(new TextWebSocketFrame("收到文件"));
}
private void handleLogin(ChannelHandlerContext ctx, JsonNode json) {
String username = json.get("username").asText();
Channel channel = ctx.channel();
// 检查用户名是否已存在
if (isUsernameTaken(username)) {
sendError(ctx, "用户名已存在");
return;
}
// 创建用户信息
UserInfo user = new UserInfo(
channel.id().toString(),
username,
channel,
new Date()
);
users.put(channel.id(), user);
// 发送登录成功响应
JsonNode response = JsonUtil.createObject()
.put("type", "login_success")
.put("username", username)
.put("message", "登录成功");
channel.writeAndFlush(new TextWebSocketFrame(response.toString()));
// 广播用户加入
broadcastSystemMessage(username + " 加入了聊天室");
System.out.println("用户登录: " + username);
}
private void handleJoinRoom(ChannelHandlerContext ctx, JsonNode json) {
String roomId = json.get("room").asText();
Channel channel = ctx.channel();
UserInfo user = users.get(channel.id());
if (user == null) {
sendError(ctx, "请先登录");
return;
}
// 检查聊天室是否存在
ChatRoom room = rooms.get(roomId);
if (room == null) {
sendError(ctx, "聊天室不存在");
return;
}
// 离开之前的房间
if (user.getCurrentRoom() != null) {
ChatRoom oldRoom = rooms.get(user.getCurrentRoom());
if (oldRoom != null) {
oldRoom.removeMember(user.getUsername());
// 通知原房间用户
broadcastToRoom(oldRoom.getId(),
new TextWebSocketFrame(user.getUsername() + " 离开了房间"));
}
}
// 加入新房间
user.setCurrentRoom(roomId);
room.addMember(user.getUsername());
// 发送加入成功响应
JsonNode response = JsonUtil.createObject()
.put("type", "join_success")
.put("room", roomId)
.put("roomName", room.getName())
.put("message", "已加入 " + room.getName());
channel.writeAndFlush(new TextWebSocketFrame(response.toString()));
// 发送房间历史消息
sendRoomHistory(channel, room);
// 通知房间其他用户
broadcastToRoom(roomId,
new TextWebSocketFrame(user.getUsername() + " 加入了房间"));
System.out.println(user.getUsername() + " 加入房间: " + roomId);
}
private void handleChatMessage(ChannelHandlerContext ctx, JsonNode json) {
String message = json.get("content").asText();
Channel channel = ctx.channel();
UserInfo user = users.get(channel.id());
if (user == null) {
sendError(ctx, "请先登录");
return;
}
if (user.getCurrentRoom() == null) {
sendError(ctx, "请先加入聊天室");
return;
}
// 创建聊天消息
ChatMessage chatMessage = new ChatMessage(
UUID.randomUUID().toString(),
user.getCurrentRoom(),
user.getUserId(),
user.getUsername(),
message,
new Date(),
MessageType.TEXT
);
// 保存到聊天室历史
ChatRoom room = rooms.get(user.getCurrentRoom());
if (room != null) {
room.addMessage(chatMessage);
// 广播给房间所有用户
JsonNode broadcastMsg = JsonUtil.createObject()
.put("type", "chat_message")
.put("sender", user.getUsername())
.put("content", message)
.put("timestamp", chatMessage.getTimestamp().getTime());
broadcastToRoom(room.getId(),
new TextWebSocketFrame(broadcastMsg.toString()));
}
System.out.println(user.getUsername() + " 在 " +
user.getCurrentRoom() + " 说: " + message);
}
private void broadcastSystemMessage(String message) {
JsonNode msg = JsonUtil.createObject()
.put("type", "system_message")
.put("content", message)
.put("timestamp", System.currentTimeMillis());
TextWebSocketFrame frame = new TextWebSocketFrame(msg.toString());
allChannels.writeAndFlush(frame);
}
private void broadcastToRoom(String roomId, TextWebSocketFrame frame) {
for (Map.Entry<ChannelId, UserInfo> entry : users.entrySet()) {
UserInfo user = entry.getValue();
if (roomId.equals(user.getCurrentRoom())) {
user.getChannel().writeAndFlush(frame.retain());
}
}
frame.release();
}
private void sendWelcomeMessage(Channel channel) {
JsonNode welcome = JsonUtil.createObject()
.put("type", "welcome")
.put("message", "欢迎来到WebSocket聊天室")
.put("timestamp", System.currentTimeMillis());
channel.writeAndFlush(new TextWebSocketFrame(welcome.toString()));
}
private void sendError(ChannelHandlerContext ctx, String error) {
JsonNode errorMsg = JsonUtil.createObject()
.put("type", "error")
.put("message", error);
ctx.writeAndFlush(new TextWebSocketFrame(errorMsg.toString()));
}
private void sendRoomHistory(Channel channel, ChatRoom room) {
List<ChatMessage> history = room.getHistory();
if (history.isEmpty()) {
return;
}
JsonNode historyMsg = JsonUtil.createObject()
.put("type", "room_history")
.put("room", room.getId());
JsonNode messages = JsonUtil.createArray();
for (ChatMessage msg : history) {
JsonNode msgNode = JsonUtil.createObject()
.put("sender", msg.getSender())
.put("content", msg.getContent())
.put("timestamp", msg.getTimestamp().getTime());
messages.add(msgNode);
}
historyMsg.set("messages", messages);
channel.writeAndFlush(new TextWebSocketFrame(historyMsg.toString()));
}
private boolean isUsernameTaken(String username) {
return users.values().stream()
.anyMatch(user -> username.equals(user.getUsername()));
}
private boolean isWebSocketUpgrade(FullHttpRequest req) {
return req.headers().contains(HttpHeaderNames.UPGRADE) &&
req.headers().contains(HttpHeaderNames.CONNECTION) &&
"websocket".equalsIgnoreCase(req.headers().get(HttpHeaderNames.UPGRADE));
}
private void sendHtmlPage(ChannelHandlerContext ctx, FullHttpRequest req) {
String html = "<html><body><h1>WebSocket聊天室</h1>" +
"<p>请使用WebSocket连接 ws://" +
req.headers().get(HttpHeaderNames.HOST) + "/ws</p>" +
"</body></html>";
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ctx.writeAndFlush(response);
}
private void sendAdminPage(ChannelHandlerContext ctx, FullHttpRequest req) {
JsonNode stats = JsonUtil.createObject()
.put("online_users", users.size())
.put("total_rooms", rooms.size())
.put("server_time", new Date().toString());
String html = "<html><body><h1>管理后台</h1>" +
"<pre>" + stats.toString() + "</pre>" +
"</body></html>";
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ctx.writeAndFlush(response);
}
private void send404(ChannelHandlerContext ctx, FullHttpRequest req) {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.NOT_FOUND);
ctx.writeAndFlush(response);
}
}
// ========== 辅助类 ==========
static class UserInfo {
private final String userId;
private final String username;
private final Channel channel;
private String currentRoom;
private final Date joinTime;
// 构造方法、getter、setter省略
}
static class ChatRoom {
private final String id;
private final String name;
private final Set<String> members = ConcurrentHashMap.newKeySet();
private final List<ChatMessage> history = new ArrayList<>();
// 构造方法、getter、setter省略
}
static class ChatMessage {
private final String id;
private final String roomId;
private final String senderId;
private final String sender;
private final String content;
private final Date timestamp;
private final MessageType type;
// 构造方法、getter省略
}
enum MessageType {
TEXT, IMAGE, FILE, SYSTEM
}
static class JsonUtil {
private static final ObjectMapper mapper = new ObjectMapper();
static JsonNode parse(String json) throws IOException {
return mapper.readTree(json);
}
static ObjectNode createObject() {
return mapper.createObjectNode();
}
static ArrayNode createArray() {
return mapper.createArrayNode();
}
}
}
3.4 HTML客户端页面
<!DOCTYPE html>
<html>
<head>
<title>WebSocket聊天室</title>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
header {
background: #4a5568;
color: white;
padding: 20px;
text-align: center;
}
.main {
display: flex;
min-height: 600px;
}
.sidebar {
width: 300px;
background: #f7fafc;
border-right: 1px solid #e2e8f0;
padding: 20px;
}
.content {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
}
.login-form {
background: #edf2f7;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.room-list {
margin-top: 20px;
}
.room-item {
padding: 10px;
margin: 5px 0;
background: white;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.room-item:hover {
background: #e2e8f0;
}
.room-item.active {
background: #4299e1;
color: white;
}
.user-list {
margin-top: 20px;
}
.user-item {
padding: 8px;
display: flex;
align-items: center;
}
.user-status {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
}
.user-status.online {
background: #48bb78;
}
.message-area {
flex: 1;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
overflow-y: auto;
margin-bottom: 20px;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 8px;
max-width: 80%;
}
.message.system {
background: #e2e8f0;
margin: 10px auto;
text-align: center;
max-width: 60%;
color: #718096;
}
.message.incoming {
background: #ebf8ff;
align-self: flex-start;
border-left: 4px solid #4299e1;
}
.message.outgoing {
background: #f0fff4;
align-self: flex-end;
margin-left: auto;
border-right: 4px solid #48bb78;
}
.message-header {
font-size: 12px;
color: #718096;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
}
.input-area {
display: flex;
gap: 10px;
}
input, button {
padding: 12px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
}
input {
flex: 1;
outline: none;
transition: border 0.3s;
}
input:focus {
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
button {
background: #4299e1;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #3182ce;
}
button:disabled {
background: #cbd5e0;
cursor: not-allowed;
}
.status {
padding: 10px;
text-align: center;
background: #f7fafc;
border-top: 1px solid #e2e8f0;
font-size: 14px;
color: #718096;
}
.status.connected {
color: #48bb78;
}
.status.disconnected {
color: #f56565;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>WebSocket聊天室</h1>
<p>实时聊天,畅所欲言</p>
</header>
<div class="main">
<!-- 侧边栏 -->
<div class="sidebar">
<!-- 登录表单 -->
<div class="login-form" id="loginForm">
<h3>登录聊天室</h3>
<input type="text" id="username" placeholder="请输入用户名" style="width: 100%; margin: 10px 0;">
<button onclick="login()" style="width: 100%;">登录</button>
</div>
<!-- 房间列表 -->
<div class="room-list" id="roomList" style="display: none;">
<h3>聊天室列表</h3>
<div class="room-item" onclick="joinRoom('general')">
🏠 综合聊天室
</div>
<div class="room-item" onclick="joinRoom('tech')">
💻 技术交流
</div>
<div class="room-item" onclick="joinRoom('game')">
🎮 游戏天地
</div>
</div>
<!-- 用户列表 -->
<div class="user-list" id="userList" style="display: none;">
<h3>在线用户 (<span id="userCount">0</span>)</h3>
<div id="users"></div>
</div>
</div>
<!-- 主内容区 -->
<div class="content">
<!-- 消息区域 -->
<div class="message-area" id="messageArea">
<div class="message system">
欢迎来到WebSocket聊天室!
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
<button onclick="sendMessage()" id="sendBtn" disabled>发送</button>
</div>
</div>
</div>
<!-- 状态栏 -->
<div class="status disconnected" id="status">
未连接
</div>
</div>
<script>
// WebSocket连接
let socket = null;
let username = '';
let currentRoom = '';
let users = new Set();
// 连接WebSocket服务器
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = protocol + '//' + window.location.host + '/ws';
socket = new WebSocket(wsUrl);
socket.onopen = function(event) {
console.log('WebSocket连接成功');
updateStatus(true);
// 如果已有用户名,自动重新登录
if (username) {
login();
}
};
socket.onmessage = function(event) {
console.log('收到消息:', event.data);
handleMessage(JSON.parse(event.data));
};
socket.onclose = function(event) {
console.log('WebSocket连接关闭');
updateStatus(false);
// 3秒后重连
setTimeout(connect, 3000);
};
socket.onerror = function(error) {
console.error('WebSocket错误:', error);
};
}
// 处理服务器消息
function handleMessage(data) {
switch (data.type) {
case 'welcome':
addSystemMessage(data.message);
break;
case 'login_success':
handleLoginSuccess(data);
break;
case 'join_success':
handleJoinSuccess(data);
break;
case 'chat_message':
addChatMessage(data.sender, data.content, data.timestamp, false);
break;
case 'system_message':
addSystemMessage(data.content);
break;
case 'room_history':
handleRoomHistory(data);
break;
case 'user_list':
updateUserList(data.users);
break;
case 'error':
alert('错误: ' + data.message);
break;
}
}
// 登录
function login() {
const input = document.getElementById('username');
username = input.value.trim();
if (!username) {
alert('请输入用户名');
return;
}
if (!socket || socket.readyState !== WebSocket.OPEN) {
alert('连接未就绪');
return;
}
const msg = {
type: 'login',
username: username
};
socket.send(JSON.stringify(msg));
}
// 处理登录成功
function handleLoginSuccess(data) {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('roomList').style.display = 'block';
document.getElementById('userList').style.display = 'block';
document.getElementById('messageInput').disabled = false;
document.getElementById('sendBtn').disabled = false;
addSystemMessage('登录成功: ' + data.username);
}
// 加入房间
function joinRoom(roomId) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
alert('请先登录');
return;
}
const msg = {
type: 'join',
room: roomId
};
socket.send(JSON.stringify(msg));
currentRoom = roomId;
// 更新房间选中状态
document.querySelectorAll('.room-item').forEach(item => {
item.classList.remove('active');
if (item.textContent.includes(getRoomName(roomId))) {
item.classList.add('active');
}
});
}
// 处理加入成功
function handleJoinSuccess(data) {
clearMessages();
addSystemMessage(data.message);
// 请求用户列表
const msg = {
type: 'list_users',
room: data.room
};
socket.send(JSON.stringify(msg));
}
// 处理房间历史
function handleRoomHistory(data) {
data.messages.forEach(msg => {
addChatMessage(msg.sender, msg.content, msg.timestamp, true);
});
}
// 发送消息
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message) {
return;
}
if (!currentRoom) {
alert('请先加入聊天室');
return;
}
const msg = {
type: 'message',
content: message
};
socket.send(JSON.stringify(msg));
// 添加到自己的消息列表
addChatMessage(username, message, Date.now(), true);
// 清空输入框
input.value = '';
}
// 添加系统消息
function addSystemMessage(message) {
const messageArea = document.getElementById('messageArea');
const msgDiv = document.createElement('div');
msgDiv.className = 'message system';
msgDiv.textContent = message;
messageArea.appendChild(msgDiv);
scrollToBottom();
}
// 添加聊天消息
function addChatMessage(sender, content, timestamp, isOutgoing) {
const messageArea = document.getElementById('messageArea');
const msgDiv = document.createElement('div');
msgDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`;
const time = new Date(timestamp).toLocaleTimeString();
msgDiv.innerHTML = `
<div class="message-header">
<span>${sender}</span>
<span>${time}</span>
</div>
<div>${content}</div>
`;
messageArea.appendChild(msgDiv);
scrollToBottom();
}
// 清空消息
function clearMessages() {
const messageArea = document.getElementById('messageArea');
messageArea.innerHTML = '';
}
// 更新用户列表
function updateUserList(userList) {
const usersDiv = document.getElementById('users');
usersDiv.innerHTML = '';
userList.forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'user-item';
userDiv.innerHTML = `
<div class="user-status online"></div>
<span>${user}</span>
`;
usersDiv.appendChild(userDiv);
});
document.getElementById('userCount').textContent = userList.length;
}
// 更新连接状态
function updateStatus(connected) {
const statusDiv = document.getElementById('status');
if (connected) {
statusDiv.textContent = '已连接';
statusDiv.className = 'status connected';
} else {
statusDiv.textContent = '连接断开,正在重连...';
statusDiv.className = 'status disconnected';
}
}
// 滚动到底部
function scrollToBottom() {
const messageArea = document.getElementById('messageArea');
messageArea.scrollTop = messageArea.scrollHeight;
}
// 获取房间名称
function getRoomName(roomId) {
const roomNames = {
'general': '综合聊天室',
'tech': '技术交流',
'game': '游戏天地'
};
return roomNames[roomId] || roomId;
}
// 回车发送消息
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
// 页面加载时连接
window.onload = connect;
// 页面关闭时断开连接
window.onbeforeunload = function() {
if (socket) {
socket.close();
}
};
</script>
</body>
</html>
四、技术选型深度分析
4.1 什么情况下用NIO?什么情况下用Netty?
使用原生NIO的场景(极少):
-
对性能有极致要求:需要手动控制每一个细节
-
特殊需求:需要绕过Netty的某些限制
-
学习研究:理解底层原理
-
依赖限制:不能引入第三方库
使用Netty的场景(绝大多数):
-
快速开发:需要快速构建网络应用
-
生产环境:需要稳定性、可维护性
-
复杂协议:需要HTTP、WebSocket、gRPC等支持
-
团队协作:标准化的代码,容易维护
性能对比表格:
| 指标 | 原生NIO | Netty | 说明 |
|---|---|---|---|
| 开发效率 | 低 | 高 | Netty封装了复杂细节 |
| 性能 | 理论上限高 | 优化后接近上限 | Netty做了大量优化 |
| 内存管理 | 手动管理 | 自动池化 | Netty的ByteBuf更好用 |
| 线程模型 | 需要自己实现 | Reactor模式内置 | Netty的线程模型成熟 |
| 协议支持 | 需要自己实现 | 内置多种协议 | HTTP/WebSocket/Protobuf等 |
| 社区生态 | 弱 | 强大 | 大量开源项目使用 |
| 学习成本 | 高 | 中 | Netty需要学习,但比NIO简单 |
结语
通过这篇超详细的教程,你应该已经掌握了:
✅ NIO的核心原理:Selector多路复用、非阻塞IO
✅ Netty的完整使用:线程模型、编解码、性能优化
✅ WebSocket的实现:协议细节、完整聊天室
关键要点回顾:
-
NIO是基础,理解Selector才能理解Netty
-
Netty线程模型是核心,一定要彻底理解
-
ByteBuf管理很重要,生产环境一定要用池化
-
WebSocket不是魔法,本质是TCP长链接
更多推荐

所有评论(0)