前言

本篇博文是《从0到1学习 Netty》中实战系列的第一篇博文,主要内容是运用 Netty 构建包含登录、私聊、群聊、退出等功用的多客户端谈天室,往期系列文章请拜访博主的 Netty 专栏,博文中的一切代码悉数搜集在博主的 GitHub 库房中;

全体结构

本文将介绍如何运用 Netty 构建一个多客户端谈天室,包含用户登录、音讯发送、多人谈天、退出谈天等中心功用,让读者了解 Netty 的根本运用方法,并具备构建简略的谈天室的能力。

项目全体结构如下所示:

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

  • client 包:寄存客户端相关类;
  • message 包:寄存各种类型的音讯;
  • protocol 包:寄存自定义协议;
  • server 包:寄存服务器相关类;
    • handler 包:寄存音讯相关处理类;
    • service 包:寄存用户相关服务类;
    • session 包:寄存谈天相关会话类;

用户登录功用完成

为了让用户能够进行登录操作,咱们需求完成一个登录请求音讯的传递机制,流程示意图如下所示:

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

咱们将运用 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);  
    }  
}

运转成果:

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

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

这时能够创立一个 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();  
}
@Override
public 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();
    }
}

运转成果:

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

总而言之,运用 CountDownLatch 进行计数,并结合 AtomicBoolean 记载登录状况,能够有效地保证程序的并发安全性和正确性。

音讯发送功用完成

在完成用户登录功用之后,下一步需求着手完成谈天功用的开发,其间一个中心功用便是音讯发送功用。音讯发送功用旨在让谈天参与者双方都在线时能够完成实时通讯,流程示意图如下所示:

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

为了完成这一过程,咱们能够运用 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, "对方用户不存在或离线,发送失利"));  
        }  
    }  
}

运转成果:

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

多人谈天功用完成

多人谈天是指在一个谈天室中,多个用户能够进行实时谈天的功用。在完成多人谈天之前,咱们现已完成了用户登录功用和音讯发送功用,这两个功用是多人谈天的基础。

为了完成多人谈天,咱们需求增加一些新的功用:创立群聊、发送音讯到群聊、检查成员列表、参加群聊和退出群聊。

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

其间,创立群聊是指用户能够自己创立一个谈天室,并邀请其他用户参加。发送音讯到群聊是指用户能够将音讯发送到地点的群聊中,让其他成员看到。检查成员列表是指用户能够检查当时群聊中的一切成员。参加群聊是指用户能够选择参加已有的群聊,开始和其他成员谈天。退出群聊是指用户能够自动退出一个群聊,不再接纳该群聊的音讯。

创立群聊

咱们将仿制 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)));  
}

运转成果:

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

需求完好代码的读者请拜访博主的 Github:GroupCreateRequestMessageHandler;

发送音讯

在完成创立群聊功用之后,咱们需求完成发送音讯的功用,以便在群聊中进行交流。为了保证每个在线成员都能够及时收到音讯,咱们需求选用一种播送机制来完成音讯的分发。

详细而言,咱们能够经过遍历一切的谈天室成员所对应的 channel,将音讯发送给每一个在线用户。当然,这种方式并不是最高效的方法,因为假如有大量的在线用户,这会导致服务器功能下降。

因而,在实际运用中,可能会运用音讯行列或许事件告诉等愈加高效的音讯传递机制来完成。

代码如下所示:

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

运转成果:

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

需求完好代码的读者请拜访博主的 Github:GroupChatRequestMessageHandler;

检查成员

gmembers [group name]

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

需求完好代码的读者请拜访博主的 Github:GroupMembersRequestMessageHandler;

参加群聊

gjoin [group name]

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

需求完好代码的读者请拜访博主的 Github:GroupJoinRequestMessageHandler;

退出群聊

gquit [group name]

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

需求完好代码的读者请拜访博主的 Github:GroupChatRequestMessageHandler;

后记

经过本文的介绍,咱们详细了解了如何运用 Netty 构建一个多客户端谈天室。在这个过程中,咱们温习了 Netty 的基础知识,包含 Netty 编程模型、Channel、EventLoop 和 Pipeline 等概念,并经过完成用户登录、音讯发送、多人谈天、退出谈天等中心功用,加深了对 Netty 的理解。

经过本示例,咱们不只能够把握 Netty 的根本运用方法,而且能够运用这些技术构建更高级别的网络运用程序。

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

参考:

  • Netty API reference;
  • 黑马程序员Netty全套教程

上篇精讲:「优化进阶」(三)Netty 通讯协议设计:从 Redis、HTTP 和自定义协议看起

我是,等待你的重视;

创造不易,请多多支持;

系列专栏:探索 Netty:源码解析与运用案例共享