一、Netty群聊系统开发要求
- 编写一个Netty群聊系统,实现服务器端和客户端之间的数据简单通信(非阻塞)
- 实现多人群聊
- 服务器端:检测用户上线、离线、转发客户端消息
- 客户端:通过channel可以无阻塞发送消息给其他客户,同时可以接收其他客户端发送的消息(服务器转发得到)
二、设计与开发
2-1、服务器端
- 首先要设置监听端口,服务器将通过该端口与客户端交换信息,发送和接收数据;
- 初始化事件循环组EventLoopGroup,这里初始化两个事件循环组bossGroup和workerGroup。其中,bossGroup主要处理新来的客户端连接,workerGroup主要负责连接建立以后的IO事件;
- 初始化Netty的启动类ServerBootStrap,通过链式调用配置参数:【设置事件循环对象:bossGroup用于处理accept事件,workerGroup用于处理已建立连接的IO请求】——【构造serverSocketChannel的工厂类】——【设置serverSocketChannel允许的最大连接队列数】——【设置channel为保持活动状态】——【初始化SocketChannel的业务处理器管道pipeline>>考虑到群聊系统主要进行文字交流,故需要一个编码器,一个解码器和一个自定义转发消息的业务处理器】;
- 给服务器启动类绑定端口;
- 在main()方法中调用启动类对象的run()方法。
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class groupChatServer { //相关属性 private int port; //监听端口 public groupChatServer(int port){ this.port = port; } //编写run()方法,处理客户端请求 public void run() throws InterruptedException { //创建两个eventLoopGroup EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //获取到pipeline ChannelPipeline pipeline = ch.pipeline(); //向pipeline里面加入解码器 pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); //加入自己的业务处理 handler pipeline.addLast(new groupChatServerHandler()); } }); System.out.println("Netty服务器已启动"); ChannelFuture channelFuture = b.bind(port).sync(); //监听关闭事件 channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { new groupChatServer(7000).run(); } }
2-2、服务器自定义业务处理器handler
- 1为了方便管理,需要建立一个channel的集合,每当有新的客户端连接时,新建一个channel放入该集合,因此创建一个任务队列的单线程事件执行器GlobalEventExecutor并使用ChannelGroup来管理连接进来的channel。
- 自定义的handler需要重写某些方法,满足:
在新的客户端建立连接时触发,提示客户加入聊天; 在客户端断开连接时触发,提示客户下线; 在客户端发送消息时触发,接收消息并将消息转发给其他客户端; 在发生异常时触发,关闭通道ChannelHandler关联的channel和ChannelFuture(监听器) import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.util.concurrent.GlobalEventExecutor; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; public class groupChatServerHandler extends SimpleChannelInboundHandler<String> { //定义一个hashmap进行管理 //public static Map<String,Channel> channels = new HashMap<String,Channel>(); //定义一个channel组管理所有的channel /** * GlobalEventExecutor.INSTANCE是一个全局的事件执行器,是一个单例 * */ private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-DD HH:mm:ss"); //handlerAdded:表示连接建立,一旦连接建立,第一个被执行 //将当前channel加入到 channelGroup @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); //将该客户加入聊天的信息推送给其他在线的客户端 //writeAndFlush:该方法会将 channelGroup 中的所有 channel 遍历,并发送消息,无需自己遍历 channelGroup.writeAndFlush(sdf.format(new java.util.Date()) + " [客户端]"+channel.remoteAddress()+"加入聊天\n"); channelGroup.add(channel); //channels.put("id100",channel); } //断开连接时触发,将xx客户离开的消息推送给当前在线的客户端,自动将当前channel从channelGroup中移除 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); channelGroup.writeAndFlush(sdf.format(new java.util.Date()) + " [客户端]"+channel.remoteAddress()+"离开了\n"); System.out.println("channelGroup size = "+channelGroup.size()); } //表示 channel 处于活动状态,提示 xx 上线 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println(ctx.channel().remoteAddress() + "上线了"); } //表示 channel 处于非活动状态,提示 xx 离线 @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println(ctx.channel().remoteAddress() + "离线了"); } @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { //获取到当前的channel Channel channel = ctx.channel(); //遍历 channelGroup,根据不同的情况,回送不同的消息 channelGroup.forEach(ch -> { if (channel != ch){ //表示遍历到的channel不是当前的channel,转发消息 ch.writeAndFlush("[客户]"+channel.remoteAddress() + " 发送消息:"+ msg + "\n"); }else { //回显一下 ch.writeAndFlush("自己发送了消息:"+msg+"\n"); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { //关闭 ctx.close(); } }
2-3、客户端
- 客户端需要初始化主机和端口两个属性;
- 客户端同样需要初始化线程池(EventLoopGroup),但只需要初始化一个进行具体业处理的eventExecutors即可;
- 新建客户端的启动类BootStrap,并设置相关参数——【设置事件循环对象:eventExecutors用于处理已建立连接的IO请求】——【构造SocketChannel的工厂类】——【初始化SocketChannel的业务处理器管道pipeline>>考虑到群聊系统主要进行文字交流,故需要一个编码器,一个解码器和一个自定义的业务处理器】
- 客户端需要输入消息,故创建一个扫描器scanner用来通过键盘输入消息。
import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import java.util.Scanner; public class groupChatClient { //相关属性 private final String host; private final int port; public groupChatClient(String host,int port){ this.host = host; this.port = port; } public void run() throws InterruptedException { EventLoopGroup eventExecutors = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventExecutors) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //得到pipeline ChannelPipeline pipeline = ch.pipeline(); //加入相关的handler pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); //加入自定义的handler pipeline.addLast(new groupChatClientHandler()); } }); ChannelFuture channelFuture = bootstrap.connect(host, port).sync(); //得到channel Channel channel = channelFuture.channel(); System.out.println("----------"+channel.remoteAddress() + "-----------"); //客户端需要输入信息,创建一个扫描器 Scanner scanner = new Scanner(System.in); while (scanner.hasNext()){ String msg = scanner.nextLine(); //通过channel发送到服务器端 channel.writeAndFlush(msg+"\r\n"); } }finally { eventExecutors.shutdownGracefully(); } } public static void main(String[] args) throws InterruptedException { new groupChatClient("127.0.0.1",7000).run(); } }
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class groupChatClientHandler extends SimpleChannelInboundHandler<String> { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println(msg.trim()); } }
三、点对点聊天功能扩展
- 要实现点对点的聊天,必须很快地找到每个客户端连接时创建的channel,因此不能再使用ChannelGroup来进行管理,改用hasmap进行管理,规定每个channel对应一个id,Map<String id,Channel channel>;
- 依然是通过hashmap进行管理,但可以创建一个User对象,在该该对象内部初始化用户ID,登录密码等属性,可以存入数据库,使每个user实体与channel唯一对应,Map<User user,Channel channel>;