前段时间在项目中有用过Netty做过网络通信,由于项目时间紧,一直没有总结,借着最近有一点空余时间,写个群聊的例子梳理一下如何快速上手一个Netty项目。
客户端代码见Netty实战之登陆机制+心跳机制+群聊(客户端)
maven依赖
xml
<fastjosn.version>2.0.23</fastjosn.version>
<hutool.version>5.8.11</hutool.version>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjosn.version}</version>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.53.Final</version>
</dependency>
代码实现
服务端
java
/**
* @author shark
* @version 1.0.0
* @description 服务启动类
* @date 2023/7/31 10:32
*/
@Slf4j
@Component
public class NettyServerRunner implements ApplicationRunner {
@Value("${netty.port}")
private int port;
//处理客户端连接
private final EventLoopGroup bossGroup = new NioEventLoopGroup();
//轮询注册到selector上io就绪的SocketChnanel(传说中的主从reactor线程模式)
private final EventLoopGroup workerGroup = new NioEventLoopGroup(2);
@Override
public void run(ApplicationArguments args) {
try {
//以下是模版代码,所有netty程序的通用代码
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//分割符解码器
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$".getBytes())));
pipeline.addLast("decoder",new StringDecoder());
pipeline.addLast("encoder",new StringEncoder());
pipeline.addLast(new AuthClientHandler());
pipeline.addLast(new IdleStateHandler(0, 10, 10, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
pipeline.addLast(new GroupChatServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind().sync();
if (channelFuture.isSuccess()) {
log.info("server run success!port:{}", port);
}
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("server error:{}", e.getMessage());
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
log.info("server close!");
}
}
}
这里ChannelPipeline的调用链上的顺序也是有讲究的,netty有入站消息和出站消息,入站的调用顺序是注册的顺序,而出站是反过来的,所以是编解码 -> 认证 -> 心跳 -> 聊天,这里也有个坑,就是handler调用链把消息传递给下一个handler要加fireChannelxxx()的方法,英译释放xxx的通道的意思,很好理解
ChannelHandler类
less
/**
* @author shark
* @version 1.0.0
* @description 认证类
* @date 2023/8/3 11:01
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class AuthClientHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
JSONObject jsonObject = JSON.parseObject(msg);
//是认证消息就走下去
if (MessageType.LOGIN.getCode() == jsonObject.getIntValue("code")) {
LoginMessage message = JSON.parseObject(msg, LoginMessage.class);
if (authClient(ctx.channel(), message.getUserName())) {
//消息工厂类构造消息
AbstractMessage loginRes = MessageWrapper.wrapMessage(MessageType.LOGIN_RESPONSE,
MessageType.LOGIN_RESPONSE.getCode(), AuthType.SUCCESS.getCode(), AuthType.SUCCESS.getMsg());
//认证成功发送响应消息
ctx.channel().writeAndFlush(Unpooled.copiedBuffer(loginRes.toByte()));
//认证成功后,不在这个channel上继续认证
ctx.pipeline().remove(this);
//调用下一个handler,不经过这里永远卡在这个handler
ctx.fireChannelRead(msg);
} else {
//认证失败则关闭,可在这里加认证次数限制逻辑,一个IP在一段时间内限制几次
AbstractMessage loginRes = MessageWrapper.wrapMessage(MessageType.LOGIN_RESPONSE,
MessageType.LOGIN_RESPONSE.getCode(), AuthType.FAILURE.getCode(), AuthType.FAILURE.getMsg());
ctx.channel().writeAndFlush(Unpooled.copiedBuffer(loginRes.toByte()));
ctx.close();
log.warn("[{}]认证失败", message.getUserName());
}
}
}
//认证逻辑
private boolean authClient(Channel channel, String userName) {
if ("admin".equals(userName) || "shark".equals(userName)) {
//群聊ChannelGroup添加组员
ChannelManage.CHANNEL_GROUP.add(channel);
//存到Map中,此处不做解释,后面会用到
ChannelManage.CHANNEL_MAP.put(channel.remoteAddress().toString(), channel);
AbstractMessage chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT_RESPONSE, MessageType.CHAT_RESPONSE.getCode(),
ChatMessageType.SERVER.getType(), "[客户端]" + channel.remoteAddress() + "上线了");
ChannelManage.CHANNEL_GROUP.writeAndFlush(Unpooled.copiedBuffer(chatMessage.toByte()));
log.info("[客户端]" + channel.remoteAddress() + "上线了");
return true;
}
return false;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
if (ctx.channel().isActive()) {
ctx.close();
}
}
less
/**
* @author shark
* @version 1.0.0
* @description 心跳
* @date 2023/7/31 14:24
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class HeartbeatHandler extends ChannelDuplexHandler {
// 自定义心跳内容
private static final String HEARTBEAT_MESSAGE = "Heartbeat";
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
//多少秒没有读请求
if (e.state() == IdleState.READER_IDLE) {
log.warn("Disconnecting due to no inbound traffic.");
ctx.close();
//多少秒没有写请求
} else if (e.state() == IdleState.WRITER_IDLE) {
AbstractMessage heartbeatMessage = MessageWrapper.wrapMessage(MessageType.HEARTBEAT,
MessageType.HEARTBEAT.getCode(),HEARTBEAT_MESSAGE);
ctx.writeAndFlush(Unpooled.copiedBuffer(heartbeatMessage.toByte()));
} else if (IdleState.ALL_IDLE == e.state()) {
log.warn("No action");
}
}
}
}
less
/**
* @author shark
* @version 1.0.0
* @description 聊天类
* @date 2023/7/31 15:00
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
JSONObject jsonObject = JSON.parseObject(msg);
//聊天请求
if (MessageType.CHAT.getCode() == jsonObject.getIntValue("code")) {
ChatMessage chatMessage = JSON.parseObject(msg,ChatMessage.class);
log.info("{}:{}", ctx.channel().remoteAddress(), chatMessage.getContent());
//加入聊天组的群发
broadcastMessage(ctx.channel(), chatMessage.getContent());
}
}
private void broadcastMessage(Channel sender, String message) {
ChannelManage.CHANNEL_GROUP.forEach(channel -> {
AbstractMessage chatMessage;
//是否发送者发的消息
if (channel != sender) {
chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT_RESPONSE, MessageType.CHAT_RESPONSE.getCode(),
ChatMessageType.OTHER.getType(),message);
} else {
chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT_RESPONSE, MessageType.CHAT_RESPONSE.getCode(),
ChatMessageType.MYSELF.getType(),message);
}
channel.writeAndFlush(Unpooled.copiedBuffer(chatMessage.toByte()));
});
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//客户端建立连接时加入群聊组
ChannelManage.CHANNEL_GROUP.remove(ctx.channel());
ChannelManage.CHANNEL_MAP.remove(ctx.channel().remoteAddress().toString());
AbstractMessage chatMessage = MessageWrapper.wrapMessage(MessageType.CHAT, MessageType.CHAT.getCode(),
ChatMessageType.SERVER.getType(),"[客户端]" + ctx.channel().remoteAddress() + "下线了");
ChannelManage.CHANNEL_GROUP.writeAndFlush(Unpooled.copiedBuffer(chatMessage.toByte()));
log.info("{}------下线", ctx.channel().remoteAddress());
}
}
java
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/7/31 18:12
*/
public class ChannelManage {
public static final ConcurrentHashMap<String, Channel> CHANNEL_MAP = new ConcurrentHashMap<>();
public static final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
枚举类
kotlin
/**
* @author shark
* @version 1.0.0
* @description 认证枚举
* @date 2023/8/3 13:41
*/
@Getter
public enum AuthType {
/* 登陆认证成功 */
SUCCESS("1","认证成功"),
/* 登陆认证失败 */
FAILURE("0","认证失败");
private final String code;
private final String msg;
AuthType(String code, String msg) {
this.code = code;
this.msg = msg;
}
}
typescript
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 17:22
*/
@Getter
public enum ChatMessageType {
/* 服务端 */
SERVER("SERVER"),
/* 客户端 */
MYSELF("MYSELF"),
/* 其他客户端 */
OTHER("OTHER");
public final String type;
ChatMessageType(String type) {
this.type = type;
}
}
scss
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 13:02
*/
@Getter
public enum MessageType {
/* 登陆 */
LOGIN(1),
/* 登陆响应 */
LOGIN_RESPONSE(2),
/* 心跳 */
HEARTBEAT(3),
/* 聊天 */
CHAT(4),
/* 聊天响应 */
CHAT_RESPONSE(5);
private final int code;
MessageType(int code) {
this.code = code;
}
}
消息封装类
java
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 12:43
*/
@Data
@NoArgsConstructor
public abstract class AbstractMessage implements Serializable {
//消息类型
private int code;
private long timestamp;
public AbstractMessage(int code) {
this.code = code;
this.timestamp = Instant.now().toEpochMilli();
}
public String toJson() {
return JSON.toJSONString(this);
}
public byte[] toByte() {
return toJson().concat("$").getBytes();
}
}
less
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 13:09
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ChatMessage extends AbstractMessage {
private String content;
public ChatMessage(int code, String[] args) {
super(code);
this.content = args[0];
}
}
less
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 21:45
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ChatResMessage extends AbstractMessage {
//聊天消息类型(服务端,客户端,其他客户端)
private String chatType;
private String content;
public ChatResMessage(int code, String[] args) {
super(code);
this.chatType = args[0];
this.content = args[1];
}
}
scala
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 13:09
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class HeartMessage extends AbstractMessage {
private String heartbeat;
public HeartMessage(int code, String[] args) {
super(code);
this.heartbeat = args[0];
}
}
less
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class LoginMessage extends AbstractMessage {
private String userName;
private String password;
public LoginMessage(int code, String[] args) {
super(code);
this.userName = args[1];
this.password = args[2];
}
}
less
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 13:40
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class LoginResMessage extends AbstractMessage {
private String auth;
private String msg;
public LoginResMessage(int code, String[] args) {
super(code);
this.auth = args[0];
this.msg = args[1];
}
}
消息工厂类
typescript
/**
* @author shark
* @version 1.0.0
* @description
* @date 2023/8/3 15:22
*/
public class MessageWrapper {
private static final Map<MessageType, MessageConstructor> MESSAGE_CONSTRUCTORS = new HashMap<>();
static {
MESSAGE_CONSTRUCTORS.put(MessageType.LOGIN, LoginMessage::new);
MESSAGE_CONSTRUCTORS.put(MessageType.CHAT, ChatMessage::new);
MESSAGE_CONSTRUCTORS.put(MessageType.CHAT_RESPONSE, ChatResMessage::new);
MESSAGE_CONSTRUCTORS.put(MessageType.HEARTBEAT, HeartMessage::new);
MESSAGE_CONSTRUCTORS.put(MessageType.LOGIN_RESPONSE, LoginResMessage::new);
}
@FunctionalInterface
private interface MessageConstructor {
AbstractMessage create(int code, String... args);
}
public static AbstractMessage wrapMessage(MessageType type, int code, String... args) {
MessageConstructor constructor = MESSAGE_CONSTRUCTORS.get(type);
if (constructor == null) {
throw new IllegalArgumentException("Unsupported message type: " + type);
}
return constructor.create(code, args);
}
使用工厂类创建消息体更加的优雅,但是这里有一个弊端,是消息的构造方法,根据索引下标来获取消息,这里虽然是根据枚举类构造了不同的消息类型,但是当类型的种类越来越膨胀的时候,这里会越来越复杂,而且在序列化消息的时候,需要一个默认的构造方法,所以需要@NoArgsConstructor
,否则带有数组的构造方法序列化后会有一个bug,当获取不到下标的回报错,虽然可以判空处理,但就失去了美感
Netty的主从reactor多线程模型
服务端代码已经完全贴在上面了,这个简单例子主要有消息的编解码,心跳机制的处理,认证服务的拦截以及工厂方法创建消息包,遇到的问题也写在注释在上面。
之前又聊到netty的线程模型,以下就八股一下netty的主从reactor多线程模型吧
接下来我要复制一下网上的结论
- Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件;
- Acceptor 处理建立连接事件后,MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理;
- SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理各种连接事件;
- 当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应;
- Handler 通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
- Worker 线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给 Handler 进行处理;
- Handler 收到响应结果后通过 Send 将响应结果返回给 Client。 这个面试的时候背背就行类,想理解还是结合源码是想想这个过程
上面的过场走完了,我主要说明一下netty的多路复用器selector,网上介绍netty说它是事件驱动的nio,nio就是选择器轮询每个注册上的io请求,监听到io请求,就开启一个线程去处理请求。我之前准备面试的时候也是这么背的,然后看了b站上图灵的免费课,发现这么说对也不对,这个可能和netty的版本有关系,之前epoll还没出来,使用的select和poll,采用主动轮询的方式监听事件是否就绪。
下面就单reactor模型来解释,netty是如何高效的监听io请求,使时间复杂度到达O(1)
-
- 首先客户端与服务端三次握手后连接成功,会产生一个ScoketChannel注册到selector
-
- 在linux环境中selector通过调用系统函数epoll()监听文件描述符fd,
文件描述符是计算机操作系统中用于唯一标识和访问文件或输入/输出资源的整数值。可以理解为进程打开文件的唯一索引,多个fd可能指向同一文件
,epoll()会给fd注册事件,通过监听这些事件感应fd是否就绪(可读可写或者连接就绪),当fd准备就绪时,返回就绪的事件到一个队列里
- 在linux环境中selector通过调用系统函数epoll()监听文件描述符fd,
-
- selector一直轮询就绪队列上的就绪时间然后调用对应的handler去处理
这样更高效的完成需要处理的事件,但是还是避免不了短时间产生大量就绪io请求的压力,比如在线人数达到几十万或者接近百万级别的话,需要做集群负载策略,借助redis绑定连接关系,通过网关路由到对应的服务端,有兴趣的可以去看看b站上号称千万级别im架构的视频。