基于Netty的简单多人聊天程序(服务端)

Posted 点滴积累相伴成长

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Netty的简单多人聊天程序(服务端)相关的知识,希望对你有一定的参考价值。

本篇文章,我们一起来实现一个基于Netty的简单多人聊天示例程序,目的是帮助大家了解Netty在Socket通信方面的典型应用场景。


先来简单介绍一下本示例服务端的基本功能点和执行流程:

1.服务端负责记录客户端的上线、下线信息;

2.服务端负责接收多个客户端的消息内容,保存与客户端的链接信息,并将消息内容广播到所有客户端;

4.每个客户端可以接收到其他客户端的加入、离开的信息;


了解了程序的大体功能点之后,接下来就需要考虑具体的实现了。还记得上一篇文章中我们编写的HttpServerHandler类吗?它实现了ChannelHandler接口,这个接口是Netty中极为重要的组件,我们的业务逻辑往往要写在ChannelHandler接口的实现类中,本示例中我们的服务端声明一个ChatServerHandler类,用来实现之前提到的基本功能点。


首先,我们定义ChatServerHandler类,此类同样需要继承SimpleChannelInboundHandler<String>(注意范型String表示服务端接收到的消息类型),由于服务端在接收到某一客户端发送过来的消息后,需要将消息向其他在线的客户端广播,因此ChatServerHandler类中需要包含一个实例变量用于存放所有的链接信息,这个变量需要用到Netty提供的组件ChannelGroup,ChannelGroup是一个接口,创建其实例的代码如下:


//服务端用于存放与多个客户端之间的socket链接

private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);


还记得上篇文章中我们提到的“事件驱动”吗,当客户端链接到服务端时,会产生handlerAdded事件,我们在ChatServerHandler类中添加handlerAdded的事件回调方法:


//客户端与服务端建立好链接时会触发此方法

@Override

public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

   Channel channel = ctx.channel();

   //channelGroup的writeAndFlush方法会调用其所包含的所有的channel的writeAndFlush方法

   channelGroup.writeAndFlush("【服务器】- " + channel.remoteAddress() + " 加入\n");

   channelGroup.add(channel); //将客户端链接存放到channelGroup中

   System.out.println("已链接的客户端:" + channelGroup.size());

}


这段代码的作用是服务端将新接入的链接存放到channelGroup中,向所有客户端广播新链接到的客户端信息并在控制台打印链接数。


当客户端与服务端的链接处于活动状态时,会触发channelActive事件,我们在ChatServerHandler添加channelActive事件的回调方法:


//客户端与服务端建立好链接,链接处于活动状态时触发此方法

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

   Channel channel = ctx.channel();

   System.out.println(channel.remoteAddress() + " 上线");

}



当服务端接收到客户端发来的聊天消息后,会触发channelRead0方法的执行,我们在这个回调方法中处理接收到的消息:


//参数msg表示客户端的消息内容,此类型由SimpleChannelInboundHandler<String>的范型决定

@Override

protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

   Channel channel = ctx.channel();

   Iterator<Channel> iterator = channelGroup.iterator();

   while (iterator.hasNext()) {

       Channel ch = iterator.next();

       if(channel != ch) {

           ch.writeAndFlush(channel.remoteAddress() + " 发送的消息:" + msg + "\n");

       } else {

           ch.writeAndFlush("【自己】" + msg + "\n");

       }

   }

}


此方法为本示例的重点方法,请注意,当有多个客户端链接到服务端时,handlerAdded方法将会被调用多次,每次调用都会分别将链接存放在channelGroup中,通过迭代channelGroup可以获取到所有客户端的链接。例如:有一个客户端向服务端发送了“你好”这样一个消息,服务端会读到此消息,并通过迭代channelGroup获取到其他客户端的链接,并通过channel的writeAndFlush方法将“你好”这条消息消息分别写到不同的客户端,从而实现群聊的效果。



@Override

public void channelInactive(ChannelHandlerContext ctx) throws Exception {

   Channel channel = ctx.channel();

   System.out.println(channel.remoteAddress() + " 下线");

}


当客户端与服务端的链接断开后,会触发handlerRemoved事件,我们在ChatServerHandler类中添加handlerRemoved事件的回调方法:


//链接彻底断开时触发此方法

@Override

public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

   Channel channel = ctx.channel();

   channelGroup.writeAndFlush("【服务器】- " + channel.remoteAddress() + " 离开\n");

   //channelGroup自动移除断掉的channel

   System.out.println(channelGroup.size());

}


另外还有一个事件回调方法需要注意,那就是异常捕获方法,当ChatServerHandler类中的方法产生异常时,exceptionCaught事件将被触发:


@Override

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

   cause.printStackTrace();

   ctx.close();

}


通常情况下这个方法需要通过ctx.close()方法关闭网络连接。至此,我们ChatServerHandler类就编写完成了(写法可以参考上一篇文章中的HttpServerHandler类的实现方式)。


接下来我们定义ChannelHandler的初始化容器ChannelInitializer类的子类ChatServerInitializer。如果记不清ChannelInitializer的作用,请阅读上一篇文章。


public class ChatServerInitializer extends ChannelInitializer<SocketChannel> {

   @Override

   protected void initChannel(SocketChannel ch) throws Exception {

       ChannelPipeline pipeline = ch.pipeline();

       pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));

       pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));

       pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));

       pipeline.addLast(new ChatServerHandler());

   }

}


与我们上一篇定义的HttpServerInitializer不同之处仅在于我们使用了不同的编解码器,DelimiterBasedFrameDecoder解码器会按照指定的分隔符如"\r"或"\n"来判断是否接收了一条完整消息,StringDecoder和StringEncoder理解起来相对容易些,本示例中通过指定utf-8字符集来对消息内容进行编解码(channelRead0方法的第二个参数类型是String,是实际消息内容,这些内容经过了StringDecoder解码器处理,当消息从服务端广播到客户端时,会通过StringEncoder编码器处理),最后将我们定义的ChatServerHandler类也加入到ChannelPipeline中。


最后,我们来编写服务端的启动类,此类几乎与上一篇的启动类HttpServer一样,唯一不同之处在于childHandler方法的参数换成了ChatServerInitializer,具体代码如下:


public class ChatServer {

   public static void main(String[] args) throws Exception {

       EventLoopGroup bossGroup = new NioEventLoopGroup();

       EventLoopGroup workerGroup = new NioEventLoopGroup();

       try {

           ServerBootstrap serverBootstrap = new ServerBootstrap();

           serverBootstrap.group(bossGroup, workerGroup).channel(NioserverSocketChannel.class).

                   childHandler(new ChatServerInitializer());

           ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();

           channelFuture.channel().closeFuture().sync();

       } finally {

           bossGroup.shutdownGracefully();

           workerGroup.shutdownGracefully();

       }

   }

}


至此,服务端的代码全部编写完毕,总结起来您可能会发现,服务端的启动类ChatServer和ChannelHandler的初始化器ChatServerInitializer同上一篇文章中的对应组件几乎没有变化,变化最大的是我们自定义的处理器类,本示例我们在处理器类ChatServerHandler中引入了若干个回调方法,用于响应不同的网络事件,将这些事件进行有机结合,进而实现了聊天的业务逻辑,在下一篇文章中我们将介绍如何编写聊天程序客户端的功能。也许此刻您依然对Channel,ChannelHandler,ChannelInitializer这些组件的逻辑关系存在迷惑,先不要着急,慢慢来,我们下篇文章见。


以上是关于基于Netty的简单多人聊天程序(服务端)的主要内容,如果未能解决你的问题,请参考以下文章

netty搭建web聊天室

netty玩转irving聊天室(android整合netty客户端+springboot整合netty服务端),附源码

Python 多人聊天工具 ( 多线程 )

Java案例:基于TCP的简单聊天程序

多线程+socket实现多人聊天室

nio 代码实现简易多人聊天