写点什么

【Netty】「项目实战」(一)如何构建多客户端聊天室

作者:sidiot
  • 2023-06-24
    浙江
  • 本文字数:4970 字

    阅读完需:约 16 分钟

前言


本篇博文是《从 0 到 1 学习 Netty》中实战系列的第一篇博文,主要内容是使用 Netty 构建包含登录、私聊、群聊、退出等功能的多客户端聊天室,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;


整体结构


本文将介绍如何使用 Netty 构建一个多客户端聊天室,包括用户登录、消息发送、多人聊天、退出聊天等核心功能,让读者了解 Netty 的基本使用方法,并具备构建简单的聊天室的能力。


项目整体结构如下所示:



  • client 包:存放客户端相关类;

  • message 包:存放各种类型的消息;

  • protocol 包:存放自定义协议;

  • server 包:存放服务器相关类;

  • handler 包:存放消息相关处理类;

  • service 包:存放用户相关服务类;

  • session 包:存放聊天相关会话类;

用户登录功能实现

为了让用户能够进行登录操作,我们需要实现一个登录请求消息的传递机制,流程示意图如下所示:



我们将使用 LoginRequestMessage 来封装用户输入的账号密码信息,并将其发送给服务端进行验证,客户端实现代码如下所示:


@Override  public void channelActive(ChannelHandlerContext ctx) throws Exception {      // 负责接收用户在控制台的输入    new Thread(() -> {          Scanner scanner = new Scanner(System.in);          System.out.println("请输入用户名:");          String username = scanner.next();          System.out.println("请输入密码:");          String password = scanner.next();          // 构造消息对象          LoginRequestMessage msg = new LoginRequestMessage(username, password);          // 发送消息          ctx.writeAndFlush(msg);      }, "system in").start();  }
复制代码


同时,服务端将会使用 SimpleChannelInboundHandler 来关注并处理特定类型的消息,如 LoginRequestMessage,因为这个消息包含了用户的登录信息,所以服务端会对这些信息进行验证,通过后会将用户名与 channel 进行绑定,并返回相应的结果给客户端。请注意,以上代码仅是为了凸显使用 Netty 完成登录的过程,因此示例简化了业务,实际上应该将访问令牌返回给客户端。


服务端实现代码如下所示:


ch.pipeline().addLast(new SimpleChannelInboundHandler<LoginRequestMessage>() {      @Override      protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {          // 获得登录信息          String username = msg.getUsername();          String password = msg.getPassword();          // 校验登录信息          boolean login = UserServiceFactory.getUserService().login(username, password);          LoginResponseMessage message;          if (login) {              message = new LoginResponseMessage(true, "登陆成功");              // channel 与 user 相互绑定              SessionFactory.getSession().bind(ctx.channel(), username);          } else {              message = new LoginResponseMessage(false, "用户名或密码不正确");          }          ctx.writeAndFlush(message);      }  }
复制代码


运行结果:



客户端向服务端发送登录请求后,需要等待服务端返回登录结果才能进行接下来的操作。


这时可以创建一个 CountDownLatch 对象,并将其初始值设置为 1。在发送登录请求的线程中,调用 await() 方法使该线程进入等待状态,而在服务端返回登录结果后,调用 countDown() 方法对计数器进行减一操作,此时该线程就会被唤醒并继续执行接下来的代码。


同时,可以使用 AtomicBoolean 类型的变量来记录用户登录状态,因为 AtomicBoolean 类型的变量可以确保读写操作的原子性,避免出现线程安全问题。


添加客户端代码如下:


CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);  AtomicBoolean LOGIN = new AtomicBoolean(false);
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { new Thread(() -> { ... ctx.writeAndFlush(msg); System.out.println("正在登录中..."); // 阻塞直到登陆成功后 CountDownLatch 被设置为 0 try { WAIT_FOR_LOGIN.await(); System.out.println("后续操作中..."); } catch (InterruptedException e) { e.printStackTrace(); } // 执行后续操作 if (!LOGIN.get()) { // 登陆失败,关闭 channel 并返回 ctx.channel().close(); return; } ... }, "system in").start(); }
@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("验证信息中..."); if (msg instanceof LoginResponseMessage) { // 如果是登录响应信息 LoginResponseMessage message = (LoginResponseMessage) msg; boolean isSuccess = message.isSuccess(); // 登录成功,设置登陆标记 if (isSuccess) { LOGIN.set(true); } // 登陆后,唤醒登陆线程 WAIT_FOR_LOGIN.countDown(); }}
复制代码


运行结果:



总而言之,使用 CountDownLatch 进行计数,并结合 AtomicBoolean 记录登录状态,可以有效地保证程序的并发安全性和正确性。

消息发送功能实现

在实现用户登录功能之后,下一步需要着手完成聊天功能的开发,其中一个核心功能就是消息发送功能。消息发送功能旨在让聊天参与者双方都在线时可以实现实时通信,流程示意图如下所示:



为了实现这一过程,我们可以使用 ChatRequestMessage 对象来封装消息,ChatRequestMessage 是一个自定义的 Java 类型,它包含了发送方 from、接收方 to 和消息正文 content 等信息。


而且我们还需要通过执行指令 send name content 来实际发送消息,客户端实现代码如下所示:


String command = scanner.nextLine();  // 获得指令及其参数,并发送对应类型消息  String[] commands = command.split("\\$");  switch (commands[0]){      case "send":          ctx.writeAndFlush(new ChatRequestMessage(username, commands[1], commands[2]));          break;                ...
复制代码


同时,服务器需要对此进行相应的处理,使用 SimpleChannelInboundHandler 来关注并处理特定类型的消息 ChatRequestMessage,当服务器接收到一条 ChatRequestMessage 消息时,它将首先解析出其中的接收方 to


接着,服务器会遍历所有已经连接到服务器上的客户端 channel,查找是否存在一个 channel 的属性值与接收方 to 相匹配。如果匹配成功,则说明接收方在线,并且服务器会将处理过的消息通过该 channel 发送至接收方;否则,服务器将认为接收方当前不在线。


ChatRequestMessageHandler 实现代码如下所示:


@ChannelHandler.Sharable  public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {      @Override      protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {          Channel channel = SessionFactory.getSession().getChannel(msg.getTo());          // 在线          if (channel != null) {              channel.writeAndFlush(new ChatResponseMessage(msg.getFrom(), msg.getContent()));          }          // 不在线          else {              ctx.writeAndFlush(new ChatResponseMessage(false, "对方用户不存在或离线,发送失败"));          }      }  }
复制代码


运行结果:



多人聊天功能实现


多人聊天是指在一个聊天室中,多个用户可以进行实时聊天的功能。在实现多人聊天之前,我们已经实现了用户登录功能和消息发送功能,这两个功能是多人聊天的基础。


为了实现多人聊天,我们需要添加一些新的功能:创建群聊、发送消息到群聊、查看成员列表、加入群聊和退出群聊。



其中,创建群聊是指用户可以自己创建一个聊天室,并邀请其他用户加入。发送消息到群聊是指用户可以将消息发送到所在的群聊中,让其他成员看到。查看成员列表是指用户可以查看当前群聊中的所有成员。加入群聊是指用户可以选择加入已有的群聊,开始和其他成员聊天。退出群聊是指用户可以主动退出一个群聊,不再接收该群聊的消息。

创建群聊


我们将仿造 QQ 群聊或者微信群聊的创建流程,创建指令为 gcreate [group name] [m1,m2,m3...]


首先是从用户那里收集一些信息,包括群聊的名称和其成员的列表。为了确保群组中没有重复的成员,我们可以使用一个 set 数据结构来存储成员名称。


收集完这些信息后,我们可以使用自定义类 GroupCreateRequestMessage 创建一个新消息,此消息将包含服务器创建群聊所需的所有信息,包括群组的名称和成员列表。然后通过网络连接将此消息发送到服务器。


客户端代码如下所示:


case "gcreate":      String[] members = commands[2].split(",");      Set<String> set = new HashSet<>(Arrays.asList(members));      set.add(username);      ctx.writeAndFlush(new GroupCreateRequestMessage(commands[1], set));      break;
复制代码


在服务器端,服务器将接收 GroupCreateRequestMessage 并解析其内容。然后,它将使用这些信息创建一个指定名称的新群聊,并邀请相关成员加入。如果群聊已经存在,则会创建失败。


服务端代码如下所示:


if(group == null){      // 发送创建成功消息      ctx.writeAndFlush(new GroupCreateResponseMessage(true, String.format("成功创建群聊 [%s]", groupName)));      // 发送成员受邀消息      List<Channel> channels = groupSession.getMembersChannel(groupName);      for (Channel channel : channels){          channel.writeAndFlush(new GroupCreateResponseMessage(true, String.format("您已被拉入群聊 [%s]", groupName)));      }  } else{      ctx.writeAndFlush(new GroupCreateResponseMessage(false, String.format("创建失败,已存在群聊 [%s]", groupName)));  }
复制代码


运行结果:



需要完整代码的读者请访问博主的 Github:GroupCreateRequestMessageHandler

发送消息


在实现创建群聊功能之后,我们需要实现发送消息的功能,以便在群聊中进行交流。为了确保每个在线成员都能够及时收到消息,我们需要采用一种广播机制来实现消息的分发。


具体而言,我们可以通过遍历所有的聊天室成员所对应的 channel,将消息发送给每一个在线用户。当然,这种方式并不是最高效的方法,因为如果有大量的在线用户,这会导致服务器性能下降。


因此,在实际应用中,可能会使用消息队列或者事件通知等更加高效的消息传递机制来实现。


代码如下所示:


for (Channel channel : channelList){      channel.writeAndFlush(new GroupChatResponseMessage(msg.getFrom(), msg.getGroupName(), msg.getContent()));  }
复制代码


运行结果:



需要完整代码的读者请访问博主的 Github:GroupChatRequestMessageHandler

查看成员


gmembers [group name]



需要完整代码的读者请访问博主的 Github:GroupMembersRequestMessageHandler

加入群聊


gjoin [group name]



需要完整代码的读者请访问博主的 Github:GroupJoinRequestMessageHandler

退出群聊


gquit [group name]



需要完整代码的读者请访问博主的 Github:GroupChatRequestMessageHandler


后记


通过本文的介绍,我们详细了解了如何使用 Netty 构建一个多客户端聊天室。在这个过程中,我们复习了 Netty 的基础知识,包括 Netty 编程模型ChannelEventLoopPipeline 等概念,并通过实现用户登录、消息发送、多人聊天、退出聊天等核心功能,加深了对 Netty 的理解。


通过本示例,我们不仅可以掌握 Netty 的基本使用方法,而且可以使用这些技术构建更高级别的网络应用程序。


以上就是 Netty 如何构建多客户端聊天室 的所有内容了,希望本篇博文对大家有所帮助!


参考:


发布于: 刚刚阅读数: 4
用户头像

sidiot

关注

还未添加个人签名 2023-06-04 加入

还未添加个人简介

评论

发布
暂无评论
【Netty】「项目实战」(一)如何构建多客户端聊天室_Java_sidiot_InfoQ写作社区