说明
这是一个使用 Netty 来实现 IM 双向通信的 demo 项目。通信双方的客户端 GUI 界面均是使用 JavaFX 来实现的。
本 demo 项目已完成的工作有:
-
通信双方可互为发送方、接收方。
-
在文本框中,可以点击
发送
按钮来发送消息,也可以使用Enter
,而在文本中另起一行需要使用组合键Ctrl + Enter
来完成。 -
通信过程是由其它线程在后台完成,不会阻塞 UI 线程。
-
通信双方的通信是使用 Netty 来实现的,已解决 Netty 传输过程中的半包、粘包问题。
-
实现对 Java 对象的透明传输。
通信时可以传输 Java 对象,而不限制为简单的文本数据。在发送端、接收端可以借助传输载体,通过对 Java 对象的序列化和反序列化来实现对 Java 对象的透明传输。
-
本项目使用的传输载体有:
- JSON
运行效果
核心代码
- 客户端核心代码
package org.wangpai.demo.im.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.LengthFieldPrepender;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import java.util.List;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.wangpai.demo.im.protocol.Message;
import org.wangpai.demo.im.protocol.Protocol;
import org.wangpai.demo.im.util.json.JsonUtil;
/**
* @since 2021-12-1
*/
@Accessors(chain = true)
public class Client {
@Setter
private String ip;
@Setter
private int port;
private Channel channel;
private EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
public Client start() {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerLoopGroup);
bootstrap.channel(NioSocketChannel.class);
// 设置接收端的 IP 和端口号,但实际上,自己作为发送端也会为自己自动生成一个端口号
bootstrap.remoteAddress(ip, port);
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 最外层编码器。为了帮助接收端解决粘包、半包问题
ch.pipeline().addLast(new LengthFieldPrepender(Protocol.HEAD_LENGTH));
// 将 String 数据转化为二进制数据
ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
// 将 Java 对象转化为 String 数据(JSON 数据)
ch.pipeline().addLast(new MessageToMessageEncoder<Message>() {
@Override
protected void encode(ChannelHandlerContext ctx, Message message, List<Object> out)
throws JsonProcessingException {
out.add(JsonUtil.pojo2Json(message));
}
});
}
});
ChannelFuture future = bootstrap.connect();
future.addListener((ChannelFuture futureListener) -> {
if (futureListener.isSuccess()) {
System.out.println("客户端连接成功"); // FIXME:日志
} else {
System.out.println("客户端连接失败"); // FIXME:日志
}
});
try {
future.sync();
} catch (Exception exception) {
exception.printStackTrace(); // FIXME:日志
}
this.channel = future.channel();
return this;
}
public void send(Message message) {
channel.writeAndFlush(message);
}
public void send(String msg) {
var message = new Message();
message.setMsg(msg);
this.send(message);
}
public void destroy() {
this.workerLoopGroup.shutdownGracefully();
}
private Client() {
super();
}
public static Client getInstance() {
return new Client();
}
}
- 服务器端核心代码
package org.wangpai.demo.im.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.util.CharsetUtil;
import java.util.List;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.wangpai.demo.im.protocol.Message;
import org.wangpai.demo.im.protocol.Protocol;
import org.wangpai.demo.im.util.json.JsonUtil;
import org.wangpai.demo.im.view.MainFace;
/**
* @since 2021-12-1
*/
@Accessors(chain = true)
public class Server {
@Setter
private int port;
@Setter
private MainFace mainFace;
private EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
private EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
public Server start() {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(this.bossLoopGroup, this.workerLoopGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.localAddress(port);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 最外层解码器。可解决粘包、半包问题
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0,
Protocol.HEAD_LENGTH, 0, Protocol.HEAD_LENGTH));
// 将二进制数据解码成 String 数据
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
// 将 String 数据(JSON 数据)解码成 Java 对象
ch.pipeline().addLast(new MessageToMessageDecoder<String>() {
@Override
protected void decode(ChannelHandlerContext ctx, String msg, List<Object> out)
throws JsonProcessingException {
out.add(JsonUtil.json2Pojo(msg, Message.class));
}
});
// 进行对转化后的最终的数据的处理
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object obj) {
mainFace.receive(((Message) obj).getMsg());
}
});
}
});
try {
ChannelFuture channelFuture = bootstrap.bind().sync();
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception exception) {
exception.printStackTrace(); // FIXME:日志
} finally {
this.workerLoopGroup.shutdownGracefully();
this.bossLoopGroup.shutdownGracefully();
}
return this;
}
public void destroy() {
this.workerLoopGroup.shutdownGracefully();
this.bossLoopGroup.shutdownGracefully();
}
private Server() {
super();
}
public static Server getInstance() {
return new Server();
}
}
完整代码
已上传至 GitHub 中,可免费下载:https://github.com/wangpaiblog/20211213-im_demo-netty_javafx
参考知识
-
JavaFX 中使用多线程与保证 UI 线程安全:https://blog.csdn.net/wangpaiblog/article/details/120755930
-
如何在 JavaFX 的 TextArea 实现回车发送信息而不换行,但组合键 Ctrl + Enter 换行:https://blog.csdn.net/wangpaiblog/article/details/121506912