首页 > 编程语言 >仿微信聊天程序 - 11. 服务端

仿微信聊天程序 - 11. 服务端

时间:2023-08-13 10:23:09浏览次数:37  
标签:11 String private class new 仿微信 数据包 public 服务端

本文是仿微信聊天程序专栏的第十一篇文章,主要记录了【米虫IM-服务端】的实现。

界面设计

仿微信聊天程序的服务端正常来说可能不需要界面,但是为了配置和调试方便,还是开发了一下简单的界面,主要由两部分组成:

  1. 服务端域名(或IP)端口配置
  2. 收发数据包日志打印

Spring集成

仿微信聊天程序服务端需要对数据进行管理,后续也可以提供WEB的管理端,为了开发方便,选择集成Spring(SpringBoot)框架进行业务功能开发,主要是技术选型如下:

  1. 数据库:H2(可切换成MySQL)
  2. orm:spring-data-jpa

JavaFX和SpringBoot项目集成,只需要JavaFX的启动类加上@SpringbootApplication注解,然后在启动的时候调用SpringApplication.run()即可。

import javafx.application.Application;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App extends Application {
}

这里稍作调整,不在Application的start部分调用SpringApplication.run,而是采用“启动”/“停止”按钮来控制SpringBoot项目的启停。

@SpringBootApplication
public class App extends Application {
    VBox root() {
        // ... 省略其他控件的创建代码 ....
        startButton.setOnAction(e -> {
            stateText.setText("提示:服务启动中,请耐心等待.....");
            startButton.setDisable(true);
            new Thread(() -> {
                try {
                    System.setProperty(TCPConst.NETTY_HOST_KEY, hostTf.getText());
                    System.setProperty(TCPConst.NETTY_PORT_KEY, portTf.getText());
                    context = SpringApplication.run(App.class, args);
                    Platform.runLater(() -> {
                        stateText.setText("");
                        stopButton.setDisable(false);
                    });
                } catch (Throwable t) {
                    String error = "服务启动失败,原因:" + t.getMessage();
                    Platform.runLater(() -> stateText.setText(error));
                }
            }).start();
        });
        stopButton.setOnAction(e -> {
            stateText.setText("提示:服务停止中,请耐心等待.....");
            stopButton.setDisable(true);
            new Thread(() -> {
                try {
                    if (Objects.nonNull(context)) {
                        SpringApplication.exit(context);
                    }
                    Platform.runLater(() -> {
                        stateText.setText("");
                        startButton.setDisable(false);
                    });
                } catch (Throwable t) {
                    String error = "服务停止失败,原因:" + t.getMessage();
                    Platform.runLater(() -> stateText.setText(error));
                }
            }).start();

        });
    }
}

Netty集成

因为在JavaFX应用上已经集成了Spring,所以Netty的集成就相对比较简单,只需要将Netty的服务端启停,挂在Spring Bean的生命周期上即可。

/**
 * @author michong
 */
@Component
public class ImServer {

    @PostConstruct
    public void start() {
        // 在这里启动Netty服务
    }

    @PreDestroy
    public void stop() {
        // 在这里关闭Netty服务
    }
}

完整的ImServer代码如下:

@Component
public class ImServer {

    private final Logger logger = LoggerFactory.getLogger(ImServer.class);
    private EventLoopGroup bossGroup;
    private EventLoopGroup workGroup;

    @Autowired
    private MessageDispatcher messageDispatcher;

    @PostConstruct
    public void start() {

        bossGroup = new NioEventLoopGroup(2);
        workGroup = new NioEventLoopGroup(Math.max(Runtime.getRuntime().availableProcessors() * 16, 256));

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class);
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) {
                ChannelPipeline pipe = socketChannel.pipeline();
                pipe.addLast(new IdleStateHandler(TCPConst.IDLE_TIME_OUT_MILLISECONDS * 4, 0, 0, TimeUnit.MILLISECONDS));
                pipe.addLast(new PingChannelHandler(messageDispatcher));
                pipe.addLast(new PacketDecoder());
                pipe.addLast(new PacketEncoder());
                pipe.addLast(new PacketChannelHandler(messageDispatcher));
            }
        });

        String host = System.getProperty(TCPConst.NETTY_HOST_KEY);
        int port = Integer.parseInt(System.getProperty(TCPConst.NETTY_PORT_KEY));

        bootstrap.bind(host, port).addListener((ChannelFutureListener) cf -> {
            if (cf.isSuccess()) {
                logger.info("ImServer started. host={}, port={}", host, port);
            } else {
                stop();
                logger.info("ImServer start error.", cf.cause());
            }
        });
    }

    @PreDestroy
    public void stop() {
        try {
            if (bossGroup != null) {
                bossGroup.shutdownGracefully().sync();
            }
            if (workGroup != null) {
                workGroup.shutdownGracefully().sync();
            }
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }
    }
}

Netty-TCP

在《集成Netty》部分的ImServer代码中可以看到两个Handler,即:

  1. PacketDecoder:负责数据解码
  2. PacketEncoder:负责数据编码

这两个Handler负责对通讯的TCP数据包进行编解码,其中数据包的定义如下:

/**
 * @author michong
 */
public class Packet {
    /**
     * 消息类型
     */
    private byte type;
    /**
     * 内容长度(不含size和type长度)
     */
    private int size;
    /**
     * 消息内容
     */
    private byte[] content;
}

关于Netty-TCP编解码的部分可以查看之前发布的专栏《Netty-TCP》,专栏发布在微信小程序“Coding鱼塘”中,有兴趣的可以看看,包含数据包定义、客户端实现、服务端实现。

除了PacketDecoder和PacketEncoder之外,PingChannelHandler负责对心跳进行检查,而PacketChannelHandler负责对业务数据包进行处理。

实际上,PacketChannelHandler对业务数据包的处理是由MessageDispatcher负责的,PacketChannelHandler只是简单地将收到的数据包转交给MessageDispatcher而已。

public class PacketChannelHandler extends SimpleChannelInboundHandler<Packet> {

    private final MessageDispatcher messageDispatcher;

    public PacketChannelHandler(MessageDispatcher messageDispatcher) {
        this.messageDispatcher = messageDispatcher;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext context, Packet packet) {
        messageDispatcher.onChannelMessage(context.channel(), packet);
    }
}

而MessageDispatcher会对收到的数据包进行解析,根据不同的业务包,处理不同的业务,然后将处理结果返回给客户端:

@Component
public class MessageDispatcher {

    @Autowired
    private UserService userService;
    @Autowired
    private ContactsService contactsService;
    @Autowired
    private MessageService messageService;
    // 业务数据包处理
    public void onChannelMessage(Channel channel, Packet packet) {
        if (packet.getType() == PKT.PING || packet.getType() != PKT.TEXT) {
            return;
        }
        String content = new String(packet.getContent(), StandardCharsets.UTF_8);
        int index = IMIndexDef.getIndex(content);
        if (index != -1) {
            content = IMIndexDef.getContent(content);
        }

        Long userId = ChannelContext.getUserId(channel.id().asLongText());
        String ret = "";
        boolean valid = true;
        if (index != IMIndexDef.REGISTER_RP && index != IMIndexDef.LOGIN_RP) {
            if (Objects.isNull(userId)) {
                valid = false;
                ret = onException(new IllegalStateException("会话已过期,需要重新登录"));
            }
        }
        if (valid) {
            switch (index) {
                // 注册
                case IMIndexDef.REGISTER_RP:
                    ret = onRegisterRP(content);
                    break;
                // 登录    
                case IMIndexDef.LOGIN_RP:
                    ret = onLoginRP(channel, content);
                    break;
                // 查询用户
                case IMIndexDef.QUERY_USER_RP:
                    ret = onQueryUserRP(content);
                    break;
                // 添加好友
                case IMIndexDef.ADD_CONTACTS_RP:
                    ret = onAddContactsRP(userId, content);
                    break;
                // 删除好友
                case IMIndexDef.DEL_CONTACTS_RP:
                    break;
                // 转发信息
                case IMIndexDef.MESSAGE_RP:
                    ret = onMessageRP(userId, content);
                    break;
            }
        }
        if (!StringUtils.hasText(ret)) {
            ret = "";
        }
        Packet pkt = new Packet(PKT.TEXT, (index + ret).getBytes(StandardCharsets.UTF_8));
        channel.writeAndFlush(pkt);
    }
    // 客户端掉线处理
    public void onChannelOffline(String channelIdLongAsText) {
        ChannelContext.removeChannel(channelIdLongAsText);
    }
}

业务功能

业务功能开发跟传统的WEB开发没有太大区别,整个服务端的整体流程如下:

ImClient(仿微信聊天程序客户端) ----发送消息----> ImServer ----解码数据----> MessageDispatcher
    ^                                         |  ^                            |
    |                                         |  |                            V
    ------------------返回结果------------------   -----------返回结果---------业务处理 

即:ImServer收到数据时,将数据解码成Packet数据包,然后将数据包交给MessageDispatcher处理,而MessageDispatcher根据不同的数据包调用不同的业务处理。

以用户(User)业务功能为例,整个模块包含三个Java类:

User:用户实体(JPA-Entity)对应数据库tb_user表
UserRepository:JPA持久层
UserService:用户业务层

  • User
/**
 * @author michong
 */
@Entity
@Getter
@Setter
@Table(name = "tb_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nickname;
    private String username;
    private String password;
}
  • UserRepository
public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsernameAndPassword(String username, String password);
    User findByUsername(String username);
}
  • UserServer
@Service
public class UserService {
    @Autowired
    @Getter
    private UserRepository userRepository;

    public void register(RegisterREQ registerREQ) {
        // 测试程序、暂时不校验、不加密
        User user = new User();
        user.setNickname(registerREQ.nickname);
        user.setUsername(registerREQ.username);
        user.setPassword(registerREQ.password);
        userRepository.save(user);
    }

    public User login(LoginREQ loginREQ) {
        // 测试程序、暂时不校验、不加密
        return userRepository.findByUsernameAndPassword(loginREQ.username, loginREQ.password);
    }

    public User queryByUsername(String username) {
        return userRepository.findByUsername(username);
    }
}

业务测试

服务端开发完成后,在客户端实现Netty的接入,就可以实现业务功能测试了,下面是测试过程的一些收发数据包打印结果:

标签:11,String,private,class,new,仿微信,数据包,public,服务端
From: https://www.cnblogs.com/michong2022/p/17626215.html

相关文章

  • Oracle 11g
    Oracle读书笔记参考文档:FreeIT教程w3cschool教程《Oracle从入门到精通(第3版)明日科技》第1章Oracle11g概述1.1简述Oracle的发展史1.2关系型数据库的基本理论1.2.1关系型数据库与数据库管理系统1.2.2关系型数据库的E-R模型1.2.3关系型数据库的设......
  • Ubuntu 20.04 使用 vlmscd 搭建 KMS 服务端
    前言为了内网系统激活需要,搭建此客户端。1.下载二进制文件打开项目官网:https://github.com/Wind4/vlmcsd下载项目二进制文件:选择对应系统和架构选择性能较好的含musl库的静态版本将选择的版本重命名为vlmcsd.2.安装到Ubuntu系统中cpvlmcsd/usr/local/bin/ch......
  • 8.11模拟赛小结
    前言最无语的一集T1数对原题给定整数\(L,R\(L\\le\R)\),请计算满足以下条件的整数对\((x,y)\)的数量:\(L\\le\x,y\\le\R\)设\(g\)是\(x,y\)的最大公约数,则满足以下条件:\(g\\neq\1\)且\(\frac{x}{g}\\neq\1\)且\(\frac{y}{g}\\neq\1\)很简单的......
  • oracle归档日志暴增原因分析,Oracle归档日志满导致数据库性能异常慢 转发 https://b
    ============= oracle数据库archivelog暴增分析====================前言归档量突然增长到981G/天,导致归档目录使用率告警归档日志量异常暴增会导致磁盘空间爆满,数据库异常1、归档日志量统计SELECTTRUNC(FIRST_TIME)"TIME",SUM(BLOCK_SIZE*BLOCKS)/1024/1024/102......
  • 设置 X11 转发以在 Linux 中访问 GUI
    一、概述X11转发是一种在客户端和服务器之间传输图形界面的协议。它允许远程客户端在本地显示远程服务器上的图形应用程序,使用户可以在本地操作远程服务器上的图形界面。使用场景:远程服务器管理:管理员可以通过X11转发在本地管理远程服务器上的图形化工具和应用程序,而无需直接......
  • acwing 116.飞行员兄弟 (算法竞赛进阶指南 p48 t1 ) 题解
    原题链接https://www.acwing.com/problem/content/description/118/题目描述“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有16个把手的冰箱。已知每个把手可以处于以下两种状态之一:打开或关闭。只有当所有把手都打开时,冰箱才会打开。把手可以表示为一个4х4的矩阵,您可以......
  • 暑期熔炉8月11
    Java 中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。 在Java中一个异常的产生,主要有如下三种原因:Java内部错误发生异常,Java虚拟机产生......
  • 1148. 文章浏览 I
    1148.文章浏览I2023年8月12日20:21:301148.文章浏览I简单相关企业SQLSchemaPandasSchemaViews表:+---------------+---------+|ColumnName|Type|+---------------+---------+|article_id|int||author_id|int||viewer_id......
  • Linux 上的 DB2 11.1 GUI 安装
    概述 在这篇文章中,我们将介绍在Linux上安装DB211.1的步骤。在安装任何DB2LUW产品之前,您应该确保您的系统满足操作系统、硬件、软件、存储和内存要求。 注:以下步骤也适用于Linux上的DB211.5安装。解决方案第1步:检查以下链接以了解最新的安装要求 https://......
  • 8.7-8.11读后感
    这个星期对于大数据的学习,由于hbase一直弄不对,不知道为什么一直报错,连接都没有问题,不管是用服务端hbaseshell命令还是用JavaApi连接,都能链接成功,但是一到使用命令,查看命名空间,创建表格那些,就开始报错,这个星期一直再弄这个,也没什么太大的进展,另外就是我们的物联网比赛进入了分区......