承接上篇文章Netty实战之认证机制+心跳机制+群聊(服务端) 下篇netty客户端文章在此。
maven依赖
<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>
yml
netty:
port: 9090
# ip: 10.10.10.28
reconnectDelay: 5
代码实现
客户端
/**
* @author shark
*/
@Slf4j
@Component("nettyClientRunner")
public class NettyClientRunner implements ApplicationRunner {
@Value("${netty.port}")
private int port;
@Value("${netty.ip}")
private String ip;
//重连次数
@Value("${netty.reconnectDelay}")
private int reconnectDelay;
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
//创建一个单线程池用于控制台输出
private final ExecutorService inputExecutor = createSingleThreadExecutor();
@Override
public void run(ApplicationArguments args) throws Exception {
connect();
}
private ExecutorService createSingleThreadExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), r -> {
Thread thread = new Thread(r);
thread.setName("UserInputThread");
thread.setDaemon(true);
return thread;
});
}
public void connect() {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(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 LoginServerHandler());
pipeline.addLast(new HeartbeatHandler());
//pipeline.addLast(new ReconnectHandler(NettyClientRunner.this));
pipeline.addLast(new GroupChatClientHandler());
}
});
try {
ChannelFuture future = bootstrap.connect(ip, port);
future.addListener((ChannelFutureListener) channelFuture -> {
if (channelFuture.isSuccess()) {
log.info("Connected to server at {}:{}", ip, port);
}
//去掉断线重连逻辑
// else {
// log.error("Connection to server failed. Scheduling reconnection in {} seconds.", reconnectDelay);
// channelFuture.channel().eventLoop().schedule(this::connect, reconnectDelay, TimeUnit.MINUTES);
// }
});
//控制台输出
inputExecutor.execute(() -> handleUserInput(future.channel()));
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("Client connection interrupted", e);
}
}
private void handleUserInput(Channel channel) {
try (Scanner scanner = new Scanner(System.in)) {
while (!Thread.currentThread().isInterrupted()) {
if (scanner.hasNextLine()) {
String msg = scanner.nextLine();
AbstractMessage chat = MessageWrapper.wrapMessage(MessageType.CHAT, MessageType.CHAT.getCode(), msg);
channel.writeAndFlush(Unpooled.copiedBuffer(chat.toByte()));
}
}
} catch (Exception e) {
log.error("Error reading user input", e);
}
}
//应用断开时关闭资源
@PreDestroy
public void shutdown() {
log.info("Shutting down the Netty client gracefully...");
workerGroup.shutdownGracefully();
inputExecutor.shutdown();
}
}
/**
* @author shark
* @version 1.0.0
* @description 登陆类
* @date 2023/8/3 11:16
*/
@Slf4j
@ChannelHandler.Sharable
public class LoginServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
JSONObject jsonObject = JSON.parseObject(msg);
if (MessageType.LOGIN_RESPONSE.getCode() == jsonObject.getIntValue("code")) {
LoginResMessage message = JSON.parseObject(msg,LoginResMessage.class);
if (AuthType.SUCCESS.getCode().equals(message.getAuth())) {
log.info(message.getMsg());
ctx.pipeline().remove(this);
ctx.fireChannelRead(msg);
}else{
log.error("server auth fail");
ctx.close();
}
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
AbstractMessage loginMessage = MessageWrapper.wrapMessage(MessageType.LOGIN,MessageType.LOGIN.getCode(),
"admin","123456");
ctx.channel().writeAndFlush(Unpooled.copiedBuffer(loginMessage.toByte()));
ctx.fireChannelActive();
}
}
/**
* @author shark
* @version 1.0.0
* @description 心跳
* @date 2023/7/31 14:24
*/
@Slf4j
@ChannelHandler.Sharable
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
private static final String HEARTBEAT_MESSAGE = "Heartbeat";
@Override
public void channelActive(ChannelHandlerContext ctx) {
//与客户端连接时5秒一次心跳,时间间隔与服务端读超时一致
scheduleHeartbeat(ctx);
}
private void scheduleHeartbeat(ChannelHandlerContext ctx) {
ctx.executor().schedule(() -> {
if (ctx.channel().isActive()) {
AbstractMessage heartbeatMessage = MessageWrapper.wrapMessage(MessageType.HEARTBEAT,
MessageType.HEARTBEAT.getCode(),HEARTBEAT_MESSAGE);
ctx.writeAndFlush(Unpooled.copiedBuffer(heartbeatMessage.toByte()));
scheduleHeartbeat(ctx);
}
}, 5, TimeUnit.SECONDS);
}
}
/**
* @author shark
* @version 1.0.0
* @description 断线重连
* @date 2023/8/2 09:48
*/
@Slf4j
@ChannelHandler.Sharable
public class ReconnectHandler extends ChannelDuplexHandler {
private final NettyClientRunner nettyClientRunner;
public ReconnectHandler(NettyClientRunner nettyClientRunner) {
this.nettyClientRunner = nettyClientRunner;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//与服务器断开连接时,重试连接
log.info("server connect failed");
nettyClientRunner.connect();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("{} 错误关闭",ctx.channel().remoteAddress());
super.exceptionCaught(ctx, cause);
if (ctx.channel().isActive()) {
ctx.close();
}
}
}
/**
* @author shark
* @version 1.0.0
* @description 聊天类
* @date 2023/7/31 15:05
*/
@Slf4j
@ChannelHandler.Sharable
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
JSONObject jsonObject = JSON.parseObject(msg);
//聊天类消息
if (MessageType.CHAT_RESPONSE.getCode() == jsonObject.getIntValue("code")) {
ChatResMessage chatMessage = JSON.parseObject(msg,ChatResMessage.class);
if (ChatMessageType.SERVER.getType().equals(chatMessage.getChatType())) {
log.info("服务端消息:{}",chatMessage.getContent());
}else if (ChatMessageType.MYSELF.getType().equals(chatMessage.getChatType())) {
log.info("自己发的消息:{}",chatMessage.getContent());
}else if (ChatMessageType.OTHER.getType().equals(chatMessage.getChatType())) {
log.info("{}:{}",ctx.channel().remoteAddress(),chatMessage.getContent());
}
}
}
}
其中的枚举类,消息类,消息工厂类同Netty实战之认证机制+心跳机制+群聊(服务端)
总结
操作步骤
首先启动服务端,再启动客户端一
然后启动客户端二
服务端知道客户端一和二上线了
此时客户端二收到服务端发来客户端一上线的消息
客户端一发出消息,服务端发来自己发的消息
服务端收到消息,并向聊天室内的组员发消息
客户端二收到服务端发来客户端一发的消息
客户端二发消息
客户端一收到服务端发来客户端二发的消息
在此说明,客户端和客户端是不能总结发消息的,都是需要服务端进行中转的