NIO框架Netty之编码初体验
引言
Netty是一个基于Java NIO的高性能网络框架,以其高效的线程模型和丰富的功能广泛应用于各种网络应用开发。在本篇文章中,我们将通过一个完整的聊天室案例,深入体验Netty的强大功能。这个案例包括服务端和客户端的实现,通过WebSocket协议实现登录验证、心跳检测、信息发送、转发、接收等功能。我们将结合常用的信息报文格式设计,并通过详细的源码注释一步一步完成这个案例。
环境准备
在开始编写代码之前,请确保已经安装以下环境:
- JDK 8或以上版本
- Maven 3.3或以上版本
项目结构
我们将创建一个Maven项目,项目结构如下:
netty-chatroom/
├── pom.xml
├── src/
├── main/
├── java/
└── com/
└── example/
└── chat/
├── server/
├── client/
└── common/
├── Message.java
├── MessageType.java
└── ...
├── resources/
└── test/
POM文件配置
首先,我们需要在pom.xml
中添加Netty依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>netty-chatroom</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.68.Final</version>
</dependency>
</dependencies>
</project>
信息报文格式设计
我们使用JSON格式来设计信息报文。每个报文包含以下字段:
type
:消息类型(登录、心跳、消息等)sender
:发送者receiver
:接收者content
:消息内容timestamp
:时间戳
示例报文:
{
"type": "message",
"sender": "user1",
"receiver": "user2",
"content": "Hello, user2!",
"timestamp": 1620000000000
}
服务端实现
创建服务端主类
在com.example.chat.server
包中创建ChatServer
类:
package com.example.chat.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
public class ChatServer {
private final int port;
public ChatServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new IdleStateHandler(60, 0, 0)); // 心跳检测
pipeline.addLast(new ChatServerHandler());
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new ChatServer(8080).start();
}
}
创建服务端处理器
在com.example.chat.server
包中创建ChatServerHandler
类:
package com.example.chat.server;
import com.example.chat.common.Message;
import com.example.chat.common.MessageType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.HashMap;
import java.util.Map;
public class ChatServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final ObjectMapper mapper = new ObjectMapper();
private static final Map<String, ChannelHandlerContext> userMap = new HashMap<>();
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String text = msg.text();
Message message = mapper.readValue(text, Message.class);
handleMessage(ctx, message);
}
private void handleMessage(ChannelHandlerContext ctx, Message message) {
switch (message.getType()) {
case LOGIN:
handleLogin(ctx, message);
break;
case HEARTBEAT:
handleHeartbeat(ctx, message);
break;
case MESSAGE:
handleMessageForward(message);
break;
default:
break;
}
}
private void handleLogin(ChannelHandlerContext ctx, Message message) {
userMap.put(message.getSender(), ctx);
Message response = new Message();
response.setType(MessageType.LOGIN);
response.setContent("Login successful");
ctx.writeAndFlush(new TextWebSocketFrame(encodeMessage(response)));
}
private void handleHeartbeat(ChannelHandlerContext ctx, Message message) {
Message response = new Message();
response.setType(MessageType.HEARTBEAT);
response.setContent("Heartbeat received");
ctx.writeAndFlush(new TextWebSocketFrame(encodeMessage(response)));
}
private void handleMessageForward(Message message) {
ChannelHandlerContext receiverCtx = userMap.get(message.getReceiver());
if (receiverCtx != null) {
receiverCtx.writeAndFlush(new TextWebSocketFrame(encodeMessage(message)));
}
}
private String encodeMessage(Message message) {
try {
return mapper.writeValueAsString(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleEvent = (IdleStateEvent) evt;
if (idleEvent.state() == IdleStateHandler.READER_IDLE_STATE_EVENT.state()) {
ctx.close(); // 关闭超时连接
}
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
创建消息类和枚举
在com.example.chat.common
包中创建Message
类和MessageType
枚举:
package com.example.chat.common;
public class Message {
private MessageType type;
private String sender;
private String receiver;
private String content;
private long timestamp;
// Getters and setters
public MessageType getType() { return type; }
public void setType(MessageType type) { this.type = type; }
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
public String getReceiver() { return receiver; }
public void setReceiver(String receiver) { this.receiver = receiver; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}
public enum MessageType {
LOGIN, HEARTBEAT, MESSAGE
}
客户端实现
创建客户端主类
在com.example.chat.client
包中创建ChatClient
类:
package com.example.chat.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.websocketx.Web
SocketClientProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.net.URI;
public class ChatClient {
private final String host;
private final int port;
private final String username;
public ChatClient(String host, int port, String username) {
this.host = host;
this.port = port;
this.username = username;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new WebSocketClientProtocolHandler(new URI("ws://" + host + ":" + port + "/ws")));
pipeline.addLast(new IdleStateHandler(0, 30, 0)); // 心跳检测
pipeline.addLast(new ChatClientHandler(username));
}
});
b.connect(host, port).sync().channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new ChatClient("localhost", 8080, "user1").start();
}
}
创建客户端处理器
在com.example.chat.client
包中创建ChatClientHandler
类:
package com.example.chat.client;
import com.example.chat.common.Message;
import com.example.chat.common.MessageType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleStateEvent;
public class ChatClientHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final ObjectMapper mapper = new ObjectMapper();
private final String username;
public ChatClientHandler(String username) {
this.username = username;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送登录消息
Message loginMessage = new Message();
loginMessage.setType(MessageType.LOGIN);
loginMessage.setSender(username);
ctx.writeAndFlush(new TextWebSocketFrame(encodeMessage(loginMessage)));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String text = msg.text();
Message message = mapper.readValue(text, Message.class);
System.out.println("Received: " + message.getContent());
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// 发送心跳消息
Message heartbeatMessage = new Message();
heartbeatMessage.setType(MessageType.HEARTBEAT);
heartbeatMessage.setSender(username);
ctx.writeAndFlush(new TextWebSocketFrame(encodeMessage(heartbeatMessage)));
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
private String encodeMessage(Message message) {
try {
return mapper.writeValueAsString(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
客户端和服务端交互
启动服务端:
java -cp target/netty-chatroom-1.0-SNAPSHOT.jar com.example.chat.server.ChatServer
启动客户端:
java -cp target/netty-chatroom-1.0-SNAPSHOT.jar com.example.chat.client.ChatClient
总结
通过本篇文章,我们详细介绍了如何使用Netty实现一个简单的WebSocket聊天室案例。该案例包括了登录验证、心跳检测、信息发送、转发和接收等功能。通过详细的源码注释和逐步讲解,相信你对Netty的使用有了更深入的了解。Netty的强大之处在于其灵活性和高性能,掌握Netty将极大地提升你的网络编程能力。希望本文对你有所帮助,欢迎继续探索Netty的更多高级功能和应用场景。