概述
几天前偶然看到手搓RPC(远程过程调用)框架的帖子,于是想着重新了解Netty-网络编程框架的想法,写下了关于Netty实现的socket协议实时聊天室demo
。
Socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
简单来说,socket就是服务器-客户端点对点通讯的TCP/IP协议的抽象实现。
Netty
Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能、可扩展协议的服务器和客户端。
它主要简化了网络应用程序的开发,例如简化了TCP
和UDP
的socket
服务的开发,接下来需要着手的demo就是由Netty框架开发。
正文
需要实现的功能是:仿造在线聊天室实现即时通讯,即需要一个服务端和多个客户端的模型。
依赖版本:Netty 4.x
服务端
服务端需要做的:客户端连接/离开时、客户端发送信息时广播对应内容。
从处理器着手,我们需要收发的信息为字符串类型,所以ChatServerHandler
继承 SimpleChannelInboundHandler
泛型为String
类型,它继承于ChannelInboundHandlerAdapter
,提供了信息进入处理器大部分方法,后面只需要按场景重写对应方法即可。
ChatServerHandler
@Slf4j
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 重写channelRead0方法,在读取到客户端发送的信息时,向聊天室广播信息
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
Channel say = ctx.channel();
for (Channel member : channels) {
if (say != member) {
member.writeAndFlush(String.format("[%s say]: %s.\n", say.remoteAddress(), s));
} else {
member.writeAndFlush(String.format("[You say]: %s.\n", s));
}
}
}
// 服务端异常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("服务器异常:{}", cause.getMessage(), cause);
}
// 客户端链接时触发
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel in = ctx.channel();
channels.writeAndFlush(String.format("[Chat Room]: %s,join in chat.\n", in.remoteAddress()));
channels.add(in);
}
// 客户端断开时触发
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel left = ctx.channel();
channels.writeAndFlush(String.format("[Chat Room]: %s,has left.\n", left.remoteAddress()));
channels.remove(left);
}
// 客户端活动时触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("{}上线\n", ctx.channel().remoteAddress());
}
// 客户端不活动时触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("{}离开\n", ctx.channel().remoteAddress());
}
}
ChatServerChannelInitializer
继承于ChannelInitializer<SocketChannel>
,用来给SocketChannel pipeline
中添加多个信息处理类:解码、编码等。
public class ChatServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
ChannelPipeline pipeline = sc.pipeline();
// 信息分割,处理netty信息粘包,Delimiters.lineDelimiter使用\n进行帧分割
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
// 解码
pipeline.addLast("decoder", new StringDecoder());
// 编码
pipeline.addLast("encoder", new StringEncoder());
// 自定义的服务端处理器
pipeline.addLast("handler", new ChatServerHandler());
}
}
ChatServer
public class ChatServer {
private Integer port;
private NioEventLoopGroup boss;
private NioEventLoopGroup worker;
public ChatServer(Integer port) {
this.port = port;
}
// 启动方法
public void run() throws Exception{
// 多线程循环处理器
boss = new NioEventLoopGroup();
worker = new NioEventLoopGroup();
// 服务端启动辅助类,定义配置
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(boss,worker) // boss收到信息后,会转发到worker进行处理
.channel(NioServerSocketChannel.class) // 服务端通道
.option(ChannelOption.SO_BACKLOG,128) // boss线程组选项,这里是服务器的队列长度
.childOption(ChannelOption.SO_KEEPALIVE,true) // worker线程组选项
.childHandler(new ChatServerChannelInitializer()); // 添加自定义的ChannelInitializer
ChannelFuture future = bootstrap.bind(port).sync(); // 绑定端口
//future.channel().closeFuture().sync(); // 接收服务端关闭
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
客户端
客户端要做的只是收发信息,收到信息时打印,或在控制台中发送信息。
ChatClientHandler
信息处理器只需打印内容
@Slf4j
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
// 只需要打印读到的内容
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
log.info(s);
log.info("\n");
}
}
ChatClient
@Data
public class ChatClient {
private String host;
private Integer port;
private NioEventLoopGroup boss;
private Channel channel;
public ChatClient(String host, Integer port) {
this.host = host;
this.port = port;
}
public void run() throws Exception{
boss = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(boss) // 客户端只需一个线程组
.channel(NioSocketChannel.class) // 绑定NioSocketChannel而不是 NioServerSocketChannel
// 使用匿名内部类新建ChannelInitializer
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
ChannelPipeline pipeline = sc.pipeline();
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler",new ChatClientHandler());
}
})
.option(ChannelOption.SO_KEEPALIVE,true);
// 连接
ChannelFuture future = bootstrap.connect(host, port).sync();
this.channel = future.channel(); // 获取连接的channel
// 控制台输出信息
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true){
channel.writeAndFlush(in.readLine() + "\r\n");
}
} finally {
boss.shutdownGracefully(); // 关闭客户端
}
}
}
运行
分别编写主方法运行服务端、2个客户端,测试收发信息。
服务端输出
客户端01发送信息
客户端00接收信息
Demo已经上传代码仓库:netty-learning,后续有机会编写基于Netty的其他demo(远程调用RPC
)会在此仓库同步更新,感兴趣可以看看。