目录
1、系统设计
2、代码实现
2.1 服务端代码
2.2 客户端代码
2.3 启动说明
3、关键技术解析
3.1 编解码器使用
3.2 通道管理
3.3 消息协议设计
3.4 用户管理
核心功能:
用户加入/离开聊天室通知
群发聊天消息
在线用户列表管理
用户昵称设置
通信协议设计:
使用简单的文本协议,消息格式:[类型]:[内容]
消息类型:JOIN
(改昵称), MSG
(消息), LIST
(用户列表), SYS(系统消息)
关键技术组件:
LineBasedFrameDecoder:解决TCP粘包/拆包问题
StringEncoder/StringDecoder:字符串编解码
ChannelGroup:管理所有连接的客户端通道
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
public class ChatServer {
private final int port;
private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); // 存储所有连接的客户端
private final Map users = new HashMap<>(); // 在线的客户端连接及用户
public ChatServer(int port) {
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LineBasedFrameDecoder(1024)) // 行分隔解码器
.addLast(new StringDecoder(CharsetUtil.UTF_8)) // 字符串解码器
.addLast(new StringEncoder(CharsetUtil.UTF_8)) // 字符串编码器
.addLast(new ChatServerHandler());// 自定义业务处理器
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 启动服务器(同步)
ChannelFuture future = bootstrap.bind().sync();
System.out.println("聊天服务器已启动,监听端口: " + port);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully().sync();
workerGroup.shutdownGracefully().sync();
}
}
// 自定义服务端处理器
private class ChatServerHandler extends SimpleChannelInboundHandler {
// 新客户端连接
@Override
public void channelActive(ChannelHandlerContext ctx) {
channels.add(ctx.channel());
users.put(ctx.channel(), "用户" + ctx.channel().id().asShortText());
broadcastSystemMessage(users.get(ctx.channel()) + " 加入了聊天室");
sendUserList(); // 输出用户列表
}
// 客户端断开
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String username = users.get(ctx.channel());
channels.remove(ctx.channel());
users.remove(ctx.channel());
broadcastSystemMessage(username + " 离开了聊天室");
sendUserList(); // 输出用户列表
}
//接收消息
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
if (msg.startsWith("JOIN:")) {
// 设置用户名
String newName = msg.substring(5).trim();
String oldName = users.get(ctx.channel());
users.put(ctx.channel(), newName);
broadcastSystemMessage(oldName + " 更名为: " + newName);
sendUserList();
} else if (msg.startsWith("LIST")) {
// 请求用户列表
ctx.channel().writeAndFlush("LIST:" + String.join(",", users.values()));
} else {
// 普通消息
String username = users.get(ctx.channel());
broadcastMessage(username, msg);
}
}
// 异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.println("客户端连接异常: " + cause.getMessage());
cause.printStackTrace();
ctx.close();
}
private void broadcastMessage(String username, String message) {
String formatted = String.format("[%s]: %s", username, message);
channels.writeAndFlush("MSG:" + formatted + "\n", channel ->
channel != ctx.channel()); // 广播给除了发送者之外的所有人
}
private void broadcastSystemMessage(String message) {
channels.writeAndFlush("SYS:" + message + "\n");
}
private void sendUserList() {
channels.writeAndFlush("LIST:" + String.join(",", users.values()) + "\n");
}
}
public static void main(String[] args) throws InterruptedException {
new ChatServer(8080).start();
}
}
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class ChatClient {
private final String host;
private final int port;
private String username;
public ChatClient(String host, int port) {
this.host = host;
this.port = port;
this.username = "匿名用户";
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder(StandardCharsets.UTF_8))
.addLast(new StringEncoder(StandardCharsets.UTF_8))
.addLast(new ChatClientHandler());
}
});
Channel channel = bootstrap.connect(host, port).sync().channel();
System.out.println("已连接到聊天服务器 " + host + ":" + port);
// 读取控制台输入
System.out.println("输入 '/name 新名字' 更改昵称");
System.out.println("输入 '/list' 查看在线用户");
System.out.println("输入 '/exit' 退出");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String input = in.readLine();
if (input == null || "/exit".equalsIgnoreCase(input)) {
channel.close();
break;
} else if (input.startsWith("/name ")) {
String newName = input.substring(6).trim();
channel.writeAndFlush("JOIN:" + newName + "\n");
} else if ("/list".equalsIgnoreCase(input)) {
channel.writeAndFlush("LIST\n");
} else {
channel.writeAndFlush(input + "\n");
}
}
} finally {
group.shutdownGracefully();
}
}
private class ChatClientHandler extends SimpleChannelInboundHandler {
// 接收服务端消息
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
if (msg.startsWith("SYS:")) {
System.out.println("\u001B[33m[系统] " + msg.substring(4) + "\u001B[0m");
} else if (msg.startsWith("MSG:")) {
System.out.println(msg.substring(4));
} else if (msg.startsWith("LIST:")) {
System.out.println("\u001B[34m==== 在线用户 ====");
String[] users = msg.substring(5).split(",");
for (String user : users) {
System.out.println(user);
}
System.out.println("================\u001B[0m");
}
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.println("服务器连接异常: " + cause.getMessage());
cause.printStackTrace();
ctx.close();
}
}
public static void main(String[] args) throws Exception {
new ChatClient("localhost", 8080).start();
}
}
# 启动服务器:
java ChatServer
# 启动多个客户端:
java ChatClient
客户端命令:
/name 新名字
:更改昵称
/list
:查看在线用户
/exit
:退出聊天室
ch.pipeline()
.addLast(new LineBasedFrameDecoder(1024)) // 解决TCP粘包/拆包
.addLast(new StringDecoder(CharsetUtil.UTF_8)) // 字节转字符串
.addLast(new StringEncoder(CharsetUtil.UTF_8)); // 字符串转字节
LineBasedFrameDecoder:基于换行符的消息分割器
StringDecoder:将接收到的ByteBuf转换为字符串
StringEncoder:将字符串转换为ByteBuf发送
// 服务器端管理所有连接的通道
private final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 添加新连接
channels.add(ctx.channel());
// 广播消息
channels.writeAndFlush("广播内容");
消息类型 | 格式 | 说明 |
---|---|---|
JOIN | JOIN:新用户名 |
设置或更改用户名 |
MSG | MSG:[用户名]:内容 |
聊天消息 |
SYS | SYS:系统消息 |
系统通知 |
LIST | LIST:用户1,用户2,... |
在线用户列表 |
// 存储用户信息
private final Map users = new HashMap<>();
// 用户加入
users.put(ctx.channel(), "默认用户名");
// 用户离开
users.remove(ctx.channel());