IM Instant Messaging System [SpringBoot+Netty] - Combing (2)

project source code

Table of contents
IM Instant Messaging System [SpringBoot+Netty] - Combing (1)
IM Instant Messaging System [SpringBoot+Netty] - Combing (3)
IM Instant Messaging System [SpringBoot+Netty] - Combing (4)
IM Instant Messaging System [SpringBoot+Netty] - Combing (5)

5. The core of IM development is to build a TCP gateway (on)

1. Write LimServer


==LimServer ==

public class LimServer {
    
    
    // 日志类
    private final static Logger logger = LoggerFactory.getLogger(LimServer.class);

    // 端口号
    private int port;

    // 端口号和两个Group的值都是从配置文件中取出来的
    EventLoopGroup mainGroup;
    EventLoopGroup subGroup;
    ServerBootstrap server;

    public LimServer(Integer port){
    
    
    	this.port = port;
        // 两个Group
        mainGroup = new NioEventLoopGroup();
        subGroup = new NioEventLoopGroup();
        // server
        server = new ServerBootstrap();
        server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                // 服务端可连接队列大小
                .option(ChannelOption.SO_BACKLOG, 10240)
                // 参数表示允许重复使用本地地址和端口
                .option(ChannelOption.SO_REUSEADDR, true)
                // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性
                .childOption(ChannelOption.TCP_NODELAY, true)
                // 保活开关2h没有数据服务端会发送心跳包
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
    
    
                    }
                });
    }
    server.bind(port);
}

Starter

public class Starter {
    
    
    public static void main(String[] args) throws FileNotFoundException {
    
    
     	new LimServer(9000);
    }
}

After simply writing these two parts, use the network debugging assistant to connect to port 9000 of the machine. If there is no error, the connection is successful.

2. Write LimWebSocketServer


LimWebSocketServer

public class LimWebSocketServer {
    
    

    private final static Logger logger = LoggerFactory.getLogger(LimWebSocketServer.class);
	int port;
    EventLoopGroup mainGroup;
    EventLoopGroup subGroup;
    ServerBootstrap server;

    public LimWebSocketServer(int port) {
    
    
        this.port= port;
        mainGroup = new NioEventLoopGroup();
        subGroup = new NioEventLoopGroup();
        server = new ServerBootstrap();
        server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 10240) // 服务端可连接队列大小
                .option(ChannelOption.SO_REUSEADDR, true) // 参数表示允许重复使用本地地址和端口
                .childOption(ChannelOption.TCP_NODELAY, true) // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性
                .childOption(ChannelOption.SO_KEEPALIVE, true) // 保活开关2h没有数据服务端会发送心跳包
                .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
    
    
                        ChannelPipeline pipeline = ch.pipeline();
                        // websocket 基于http协议,所以要有http编解码器
                        pipeline.addLast("http-codec", new HttpServerCodec());
                        // 对写大数据流的支持
                        pipeline.addLast("http-chunked", new ChunkedWriteHandler());
                        // 几乎在netty中的编程,都会使用到此hanler
                        pipeline.addLast("aggregator", new HttpObjectAggregator(65535));
                        /**
                         * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
                         * 本handler会帮你处理一些繁重的复杂的事
                         * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
                         * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
                         */
                        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                    }
                });
    }
	server.bind(port);
	logger.info("web start");
}

Starter

public class Starter {
    
    
    public static void main(String[] args) throws FileNotFoundException {
    
    
     	new LimServer(9000);
     	new LimWebSocketServer(19000);
    }
}

Then at startup, use web.html to verify

insert image description here

3. Use snakeyaml dynamic configuration file


<!-- yaml解析 -->
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
</dependency>
@Data
public class BootstrapConfig {
    
    

    private TcpConfig lim;

    @Data
    public static class TcpConfig{
    
    
        // tcp 绑定的端口号
        private Integer tcpPort;

        // webSocket 绑定的端口号
        private Integer webSocketPort;

        // boss线程 默认=1
        private Integer bossThreadSize;

        //work线程
        private Integer workThreadSize;

        // 心跳超时时间  单位ms
        private Long heartBeatTime;

        // 登录模式
        private Integer loginModel;


        // redis配置文件
        private RedisConfig redis;

        /**
         * rabbitmq配置
         */
        private Rabbitmq rabbitmq;

        /**
         * zk配置
         */
        private ZkConfig zkConfig;

        /**
         * brokerId
         */
        private Integer brokerId;

        private String logicUrl;
    }
}

Make the data in the required configuration file an entity class for subsequent reception, reception and transformation

LimServer

public class LimServer {
    
    
    // 日志类
    private final static Logger logger = LoggerFactory.getLogger(LimServer.class);

    // 端口号
    private int port;

    // 端口号和两个Group的值都是从配置文件中取出来的
    BootstrapConfig.TcpConfig config;
    EventLoopGroup mainGroup;
    EventLoopGroup subGroup;
    ServerBootstrap server;

    public LimServer(BootstrapConfig.TcpConfig config){
    
    
        this.config = config;
        // 两个Group
        mainGroup = new NioEventLoopGroup(config.getBossThreadSize());
        subGroup = new NioEventLoopGroup(config.getWorkThreadSize());
        // server
        server = new ServerBootstrap();
        server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                // 服务端可连接队列大小
                .option(ChannelOption.SO_BACKLOG, 10240)
                // 参数表示允许重复使用本地地址和端口
                .option(ChannelOption.SO_REUSEADDR, true)
                // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性
                .childOption(ChannelOption.TCP_NODELAY, true)
                // 保活开关2h没有数据服务端会发送心跳包
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
    
    
                    }
                });
    }
    public void start(){
    
    
        this.server.bind(config.getTcpPort());
    }
}

LimWebSocket

public class LimWebSocketServer {
    
    

    private final static Logger logger = LoggerFactory.getLogger(LimWebSocketServer.class);

    BootstrapConfig.TcpConfig config;
    EventLoopGroup mainGroup;
    EventLoopGroup subGroup;
    ServerBootstrap server;

    public LimWebSocketServer(BootstrapConfig.TcpConfig config) {
    
    
        this.config = config;
        mainGroup = new NioEventLoopGroup();
        subGroup = new NioEventLoopGroup();
        server = new ServerBootstrap();
        server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 10240) // 服务端可连接队列大小
                .option(ChannelOption.SO_REUSEADDR, true) // 参数表示允许重复使用本地地址和端口
                .childOption(ChannelOption.TCP_NODELAY, true) // 是否禁用Nagle算法 简单点说是否批量发送数据 true关闭 false开启。 开启的话可以减少一定的网络开销,但影响消息实时性
                .childOption(ChannelOption.SO_KEEPALIVE, true) // 保活开关2h没有数据服务端会发送心跳包
                .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
    
    
                        ChannelPipeline pipeline = ch.pipeline();
                        // websocket 基于http协议,所以要有http编解码器
                        pipeline.addLast("http-codec", new HttpServerCodec());
                        // 对写大数据流的支持
                        pipeline.addLast("http-chunked", new ChunkedWriteHandler());
                        // 几乎在netty中的编程,都会使用到此hanler
                        pipeline.addLast("aggregator", new HttpObjectAggregator(65535));
                        /**
                         * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
                         * 本handler会帮你处理一些繁重的复杂的事
                         * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
                         * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
                         */
                         pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
    
                    }
                });
    }

    public void start(){
    
    
        this.server.bind(this.config.getWebSocketPort());
    }
}
lim:
  tcpPort: 9000
  webSocketPort: 19000
  bossThreadSize: 1
  workThreadSize: 8
  heartBeatTime: 3000 # 心跳超时时间,单位 ms
  brokerId: 1000
  loginModel: 3
  logicUrl: http://127.0.0.1:8000/v1

These two port numbers and the size of the two groups are dynamically configured using the configuration file

Starter

public class Starter {
    
    
    public static void main(String[] args) throws FileNotFoundException {
    
    
        if(args.length > 0){
    
    
            start(args[0]);
        }
    }

    private static void start(String path) throws FileNotFoundException {
    
    
        try {
    
    
            // 加载yml文件
            Yaml yaml = new Yaml();
            InputStream fileInputStream = new FileInputStream(path);
            // 搞一个实体
            BootstrapConfig bootstrapConfig = yaml.loadAs(fileInputStream, BootstrapConfig.class);
            // 启动
            new LimServer(bootstrapConfig.getLim()).start();
            new LimWebSocketServer(bootstrapConfig.getLim()).start();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            System.exit(500);
        }
    }
}

insert image description here
       In this way, all configuration files can be modified by modifying the yaml file, then modifying the BootstrapConfig entity class, and finally configuring it in Starter

4. Talk about communication protocols in vernacular—detailed explanation of mainstream communication protocols


4.1. Text protocol

  • A protocol that is close to human written expression, such as the http protocol
  • Features:
    • Good readability and easy debugging
    • Scalability is also good (by key:value extension)
    • Parsing efficiency is average

insert image description here

4.2. Binary protocol


  • In a section of transmission content, one or several fixed digits represent a fixed meaning (similar to the private protocol used to solve half-packet and sticky packets in netty above), such as the ip protocol
  • Features:
    • Poor readability and difficult to debug
    • Poor scalability (good design can be avoided)
    • High analytical efficiency

4.3, xml protocol

  • features
    • Standard protocol, cross-domain intercommunication
    • The advantages of xml, good readability, good scalability
    • Parsing is expensive
    • Low effective data transfer rate (with a large number of tags)

insert image description here

4.4. Protocols that can be used on the ground


xmpp protocol

  • Advantages: based on xml protocol, easy to understand, widely used, easy to expand
  • Disadvantages: large traffic, high power consumption on the mobile terminal, complicated interaction process

mqtt protocol

  • Advantages: adapt to multiple platforms, compared with xmpp, the data package is smaller
  • Disadvantages: the protocol is simple, the public protocol cannot customize some data formats

Proprietary protocol (based on binary protocol)

  • Advantages: Do whatever you want, strong customization, small traffic
  • Disadvantages: huge workload, poor scalability, need to consider comprehensively

5. Private Protocol Encoding and Decoding—Design


insert image description here

6. Private Protocol Encoding and Decoding—Implementation


6.1, Codec of LimServer

ByteBufToMessageUtils

public class ByteBufToMessageUtils {
    
    

    public static Message transition(ByteBuf in){
    
    

        /** 获取command*/
        int command = in.readInt();

        /** 获取version*/
        int version = in.readInt();

        /** 获取clientType*/
        int clientType = in.readInt();

        /** 获取messageType*/
        int messageType = in.readInt();

        /** 获取appId*/
        int appId = in.readInt();

        /** 获取imeiLength*/
        int imeiLength = in.readInt();

        /** 获取bodyLen*/
        int bodyLen = in.readInt();

        if(in.readableBytes() < bodyLen + imeiLength){
    
    
            in.resetReaderIndex();
            return null;
        }

        byte [] imeiData = new byte[imeiLength];
        in.readBytes(imeiData);
        String imei = new String(imeiData);

        byte [] bodyData = new byte[bodyLen];
        in.readBytes(bodyData);


        MessageHeader messageHeader = new MessageHeader();
        messageHeader.setAppId(appId);
        messageHeader.setClientType(clientType);
        messageHeader.setCommand(command);
        messageHeader.setLength(bodyLen);
        messageHeader.setVersion(version);
        messageHeader.setMessageType(messageType);
        messageHeader.setImei(imei);

        Message message = new Message();
        message.setMessageHeader(messageHeader);

        if(messageType == 0x0){
    
    
            String body = new String(bodyData);
            JSONObject parse = (JSONObject) JSONObject.parse(body);
            message.setMessagePack(parse);
        }

        in.markReaderIndex();
        return message;
    }
}

MessageDecoder (decoding)

public class MessageDecoder extends ByteToMessageDecoder {
    
    

    @Override
    protected void decode(ChannelHandlerContext ctx,
                          ByteBuf in, List<Object> out) throws Exception {
    
    
        //请求头(指令
        // 版本
        // clientType
        // 消息解析类型
        // appId
        // imei长度
        // bodylen)+ imei号 + 请求体

        if(in.readableBytes() < 28){
    
    
            return;
        }

        Message message = ByteBufToMessageUtils.transition(in);
        if(message == null){
    
    
            return;
        }

        out.add(message);
    }
}

MessageEncoder (encoding)

public class MessageEncoder extends MessageToByteEncoder {
    
    

    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
    
    
        if(msg instanceof MessagePack){
    
    
            MessagePack msgBody = (MessagePack) msg;
            String s = JSONObject.toJSONString(msgBody.getData());
            byte[] bytes = s.getBytes();
            out.writeInt(msgBody.getCommand());
            out.writeInt(bytes.length);
            out.writeBytes(bytes);
        }
    }
}

The entity class that receives the message

@Data
public class Message {
    
    
    // 请求头
    private MessageHeader messageHeader;
    // 请求体
    private Object messagePack;

    @Override
    public String toString() {
    
    
        return "Message{" +
                "messageHeader=" + messageHeader +
                ", messagePack=" + messagePack +
                '}';
    }
}
@Data
public class MessageHeader {
    
    
    //消息操作指令 十六进制 一个消息的开始通常以0x开头
    //4字节
    private Integer command;
    //4字节 版本号
    private Integer version;
    //4字节 端类型
    private Integer clientType;
    /**
     * 应用ID
     */
//    4字节 appId
    private Integer appId;
    /**
     * 数据解析类型 和具体业务无关,后续根据解析类型解析data数据 0x0:Json,0x1:ProtoBuf,0x2:Xml,默认:0x0
     */
    //4字节 解析类型
    private Integer messageType = 0x0;

    //4字节 imel长度
    private Integer imeiLength;

    //4字节 包体长度
    private int length;

    //imei号
    private String imei;
}
@Data
public class MessagePack<T> implements Serializable {
    
    

    private String userId;

    private Integer appId;

    /**
     * 接收方
     */
    private String toId;

    /**
     * 客户端标识
     */
    private int clientType;

    /**
     * 消息ID
     */
    private String messageId;

    /**
     * 客户端设备唯一标识
     */
    private String imei;

    private Integer command;

    /**
     * 业务数据对象,如果是聊天消息则不需要解析直接透传
     */
    private T data;

//    /** 用户签名*/
//    private String userSign;
}

insert image description here

add it here

6.2, Codec of LimWebSocketServer

WebSocketMessageDecoder

public class WebSocketMessageDecoder extends MessageToMessageDecoder<BinaryWebSocketFrame> {
    
    
    @Override
    protected void decode(ChannelHandlerContext ctx, BinaryWebSocketFrame msg, List<Object> out) throws Exception {
    
    
        ByteBuf content = msg.content();
        if (content.readableBytes() < 28) {
    
    
            return;
        }

        Message message = ByteBufToMessageUtils.transition(content);
        if(message == null){
    
    
            return;
        }

        out.add(message);
    }
}

WebSocketMessageEncoder

public class WebSocketMessageEncoder extends MessageToMessageEncoder<MessagePack> {
    
    

    private static Logger log = LoggerFactory.getLogger(WebSocketMessageEncoder.class);

    @Override
    protected void encode(ChannelHandlerContext ctx, MessagePack msg, List<Object> out)  {
    
    
        try {
    
    
            String s = JSONObject.toJSONString(msg);
            ByteBuf byteBuf = Unpooled.directBuffer(8+s.length());
            byte[] bytes = s.getBytes();
            byteBuf.writeInt(msg.getCommand());
            byteBuf.writeInt(bytes.length);
            byteBuf.writeBytes(bytes);
            out.add(new BinaryWebSocketFrame(byteBuf));
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
}

insert image description here

add it here

      这样一来,我们为LImServer和LImWebSocketServer提供了编解码器,这样我们的客户端只要按照我们的协议发送数据,我们就会拿到争取的数据,我们也可以将信息进行编码,发送给客户端,客户端也要遵守我们的编码规则,也就可以正常的拿到服务端发送给客户端的数据

6. The core of IM development is to build a TCP gateway (below)

1. Login message - save user NioSocketChannel


insert image description here

      Here is to create a Handler and then parse the command in the message to make the login logic correspondingly, and maintain each channel by saving the channel logged in by each user

2. Distributed cache middleware—Redisson quick start operation


Quick Start for Redisson Operations

3. User login gateway layer - save user Session


       First consider what Redis data structure to use, because this application will use multi-terminal login, so using the HashMap data structure, you can use a key to store sessions on multiple terminals, which is better than the String type

insert image description here

       Redisson is used here, so we need to make some configuration properties, modify the BootStrap and Yaml files, and then create the Redis management class, and finally put the configured Redis in the Starter to start, and save the set UserSession in the Handler to Go in the map.

insert image description here

insert image description here

insert image description here

4. The user exits the gateway layer—deletes the user session offline


逻辑

  1. First delete the Channel
  2. Then delete the session stored in Redis

5. Server heartbeat detection


       It is similar to the heartbeat detection mentioned above when netty got started. There is no read operation or write operation, or all operations will trigger userEventTriggered, and then perform some operations you specified. Here we realize that there is no operation to trigger a heartbeat every 10 seconds. Detection, detect your last ping time and current time, if it exceeds your specified timeout time, it will be considered that the user is offline, and the offline logic will be triggered

insert image description here
insert image description here

insert image description here

// 离线
public static void offLineUserSession(NioSocketChannel channel){
    
    
    // 删除session
    String userId = (String) channel.attr(AttributeKey.valueOf(Constants.UserId)).get();
    Integer appId = (Integer) channel.attr(AttributeKey.valueOf(Constants.AppId)).get();
    Integer clientType = (Integer) channel.attr(AttributeKey.valueOf(Constants.ClientType)).get();
    String imei = (String) channel
            .attr(AttributeKey.valueOf(Constants.Imei)).get();

    SessionScoketHolder.remove(appId, userId, clientType, imei);

    // 修改redis中的session的ConnectState
    RedissonClient redissonClient = RedisManager.getRedissonClient();
    RMap<String, String> map
            = redissonClient.getMap(appId + Constants.RedisConstants.UserSessionConstants + userId);
    // 获取session
    String sessionStr = map.get(clientType.toString() + ":" + imei);
    if(!StringUtils.isBlank(sessionStr)){
    
    
        // 将session转换为对象
        UserSession userSession = JSONObject.parseObject(sessionStr, UserSession.class);
        // 修改连接状态为离线
        userSession.setConnectState(ImConnectStatusEnum.OFFLINE_STATUS.getCode());
        // 再写入redis中
        map.put(clientType.toString() + ":" + imei, JSONObject.toJSONString(userSession));
    }
}

Trigger offline logic, the difference from the logout above is to modify the session state in Redis to become offline, which is directly deleted

6. Detailed explanation of RabbitMQ installation, publish and subscribe, and routing mode


Installation Tutorial

If you have Tencent Cloud and Alibaba Cloud virtualization, you can directly build a docker RabbitMQ, which is more convenient. Just search online for tutorials

quick start

7. TCP access to RabbitMQ, communication with logic layer interaction


Implement a Mq tool class

public class MqFactory {
    
    
    // ConnectionFactory
    private static ConnectionFactory factory = null;
    // 这里一个存放channel的map
    private static ConcurrentHashMap<String, Channel> channelMap = new ConcurrentHashMap<>();
    public static void init(BootstrapConfig.Rabbitmq rabbitmq){
    
    
        // 如果连接为空才进行初始化
        if(factory == null){
    
    
            factory = new ConnectionFactory();
            factory.setHost(rabbitmq.getHost());
            factory.setUsername(rabbitmq.getUserName());
            factory.setPassword(rabbitmq.getPassword());
            factory.setPort(rabbitmq.getPort());
            factory.setVirtualHost(rabbitmq.getVirtualHost());
        }
    }

    // 通过channel名字来获取不同的channel
    public static Channel getChannel(String channelName) throws IOException, TimeoutException {
    
    
        Channel channel = channelMap.get(channelName);
        if(channel == null){
    
    
            channel = getConnection().createChannel();
            channelMap.put(channelName, channel);
        }
        return channel;
    }

    // 获取connection
    private static Connection getConnection() throws IOException, TimeoutException {
    
    
        Connection connection = factory.newConnection();
        return connection;
    }
}

Create a MqReciver class

@Slf4j
public class MessageReciver {
    
    
    private static String brokerId;
    public static void startReciverMessage() {
    
    
        try {
    
    
            Channel channel = MqFactory.getChannel(Constants.RabbitConstants.MessageService2Im
                    + brokerId);
            // 绑定队列
            channel.queueDeclare(Constants.RabbitConstants.MessageService2Im + brokerId,
                    true,false, false, null);
            // 绑定交换机
            channel.queueBind(Constants.RabbitConstants.MessageService2Im  + brokerId,
                    Constants.RabbitConstants.MessageService2Im,
                    brokerId);

            channel.basicConsume(Constants.RabbitConstants.MessageService2Im + brokerId, false
                    , new DefaultConsumer(channel){
    
    
                        // 获取到rabbitmq中的信息
                        @Override
                        public void handleDelivery(String consumerTag, Envelope envelope,
                                                   AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                            try {
    
    
                                String msgStr = new String(body);
                                log.info(msgStr);
                            }
                        }
                    });
        } catch (Exception e) {
    
    
            throw new RuntimeException(e);
        }
    }
}

insert image description here
In this way, TCP can get the message from RabiitMq. In this case, as long as the logic layer delivers the message to the corresponding switch, TCP can receive it, that is, the interaction with the logic layer is opened.

8. Selection of distributed TCP service registry


      If you want to access multiple services, a single machine must not be able to support such a large concurrency. To consider distribution, you have to consider the problem of service discovery, so service registration must be introduced to solve this problem


      The current development will split the service, which will lead to problems directly discovered by the gateway and the logic service. Here is a local method, which is to configure the IP address of the service in the gateway. A service has these IP addresses, and B service has these IP addresses. For these IPs, if this solution is adopted, when we add or delete a server, all gateway services must manually modify the configuration and restart, which is not too long and unrealistic


      If it is an http server, we can forward the request to an available service through a reverse proxy, but this solution does not work in an instant messaging system, because it is a stateful service, which is different from a normal http service. The service will keep the user's active information. For example, our client has established a connection channel with the A server. The user's information is stored in the A server and can communicate with each other. When we use the B server, there is no interactive data between the user and the B server. We can't get the information in the Channel


CAP theory

  • consistency
  • availability
  • Partition tolerance

mainstream registry

  • Eureka: SpringCloud is supported, but maintenance has been stopped, it is best not to use it
  • Consul: a lightweight registry, but the coverage of the Java field is not very wide
  • Kubernetes: It is better to use K8S together
  • Nacos: is the preferred registration center, it can support both AP and CP
  • Zookeeper: Temporary node and watch mechanism, creating a connection will generate a node, and will notify when the node changes, with strong perception

本系统采取Zookeeper作为注册中心

9. TCP service registration—Zookeeper registers TCP service


Installation Tutorial

ZKit

/**
 * @author li
 * 直接用来创建Zookeeper目录的
 * @data 2023/4/19
 * @time 14:37
 */
public class ZKit {
    
    

    private ZkClient zkClient;

    public ZKit(ZkClient client){
    
    
        this.zkClient = client;
    }

    // im-coreRoot/tcp/ip:port
    public void createRootNode(){
    
    
        boolean exists = zkClient.exists(Constants.ImCoreZkRoot);
        if(!exists){
    
    
            zkClient.createPersistent(Constants.ImCoreZkRoot);
        }

        boolean tcpExists = zkClient.exists(Constants.ImCoreZkRoot +
                Constants.ImCoreZkRootTcp);
        if(!tcpExists){
    
    
            zkClient.createPersistent(Constants.ImCoreZkRoot +
                    Constants.ImCoreZkRootTcp);
        }

        boolean webExists = zkClient.exists(Constants.ImCoreZkRoot +
                Constants.ImCoreZkRootWeb);
        if(!webExists){
    
    
            zkClient.createPersistent(Constants.ImCoreZkRoot +
                    Constants.ImCoreZkRootWeb);
        }
    }

    // ip:port
    public void createNode(String path){
    
    
        if(!zkClient.exists(path)){
    
    
            zkClient.createPersistent(path);
        }
    }
}

RegistryZk

@Slf4j
public class RegistryZk implements Runnable{
    
    

    private ZKit zKit;

    private String ip;

    private BootstrapConfig.TcpConfig tcpConfig;

    public RegistryZk(ZKit zKit, String ip, BootstrapConfig.TcpConfig tcpConfig) {
    
    
        this.zKit = zKit;
        this.ip = ip;
        this.tcpConfig = tcpConfig;
    }

    @Override
    public void run() {
    
    
        // 注册Zookeeper
        // 先注册1级目录
        zKit.createRootNode();

        // 再注册2级目录
        String tcpPath = Constants.ImCoreZkRoot + Constants.ImCoreZkRootTcp
                + "/" + ip + ":" + this.tcpConfig.getTcpPort();
        zKit.createNode(tcpPath);
        log.info("Registry zookeeper tcpPath success, msg=[{}]", tcpPath);

        String webPath = Constants.ImCoreZkRoot + Constants.ImCoreZkRootWeb
                + "/" + ip + ":" + this.tcpConfig.getWebSocketPort();
        zKit.createNode(webPath);
        log.info("Registry zookeeper webPath success, msg=[{}]", webPath);
    }
}

Starter

insert image description here

In this way, when we start the Starter service, the client connecting to Zookeeper can view the directory you created

insert image description here

10. Service Transformation - TCP Service Distributed Transformation


Because the instant messaging system we want to implement is a stateful service, we have to consider more

insert image description here

       For example, each netty1 maintains the corresponding users, netty1 maintains u1, u10, u100, netty2 maintainers u2, u20, u200, when we send a message from u1 to u2, there is no channel connected to netty2 in netty1, the message will be lost, which is still inappropriate at present, so we need to modify it

A centralized solution is provided here

10.1, broadcast mode

insert image description here
      This is very simple to implement, you can use RabiitMq, but it is easy to generate a message storm, if you want to send 100 messages, it will become 200, resulting in some wasteful and ineffective communication

10.2. Consistent Hash

insert image description here
      The implementation of this method is that the user u1 will do a hash calculation when registering according to some attributes such as id, and directly register him with the calculated netty, such as u1, and calculate it based on this 1 On netty1, u2 calculates that it is on netty2 according to 2. When u1 wants to send a message to u2, it will calculate which netty2 u2 is in according to u2, and send it to him point-to-point. There is no need to send so many copies, but there are disadvantages It is also obvious that it is heavily dependent on the stability of service discovery, and it is necessary to detect the existence of netty2 in a timely manner, and to notify in time when netty2 is offline

10.3. Build the routing layer

insert image description here

       By building the routing layer, for example, storing the registered user and the corresponding netty service ip in it, when u1 sends a message to u2, it will search for it at the routing layer and then send it. The reliability is relatively high, and it can be solved by mq Coupling, the routing layer is stateless and can be expanded horizontally, and can be extended to multiple. The disadvantage is that it is very complicated. If there is an extra layer, there will be more code and some more components, and the routing layer needs to be maintained independently, and it will also depend on the routing layer. Dependency and Reliability

11. The instant messaging system supports multi-terminal login mode—to cope with multi-terminal login scenarios


insert image description here
Multi-terminal login mode imitating Tencent Im instant messaging system

insert image description here
insert image description here

Do some configuration stuff


Multi-terminal login Under stateful distribution, it is recommended to use the broadcast (or consistent hash) mode, because you do not know how many terminals a user logs in on, this is the easiest way.

UserLoginMessageListener

public class UserLoginMessageListener {
    
    

    private final static Logger logger = LoggerFactory.getLogger(UserLoginMessageListener.class);

    private Integer loginModel;

    public UserLoginMessageListener(Integer loginModel){
    
    
        this.loginModel = loginModel;
    }

    // 监听用户登录
    public void listenerUserLogin(){
    
    
        RTopic topic = RedisManager.getRedissonClient().getTopic(Constants.RedisConstants.UserLoginChannel);

        // 使用Redisson的订阅模式做  监听  当有用户的某个端登录就会
        topic.addListener(String.class, new MessageListener<String>() {
    
    
            @Override
            public void onMessage(CharSequence charSequence, String message) {
    
    
                logger.info("收到用户上线:" + message);

                UserClientDto userClientDto = JSONObject.parseObject(message, UserClientDto.class);

                // 获取所有的CHANNELS
                List<NioSocketChannel> nioSocketChannels
                        = SessionScoketHolder.get(userClientDto.getAppId(), userClientDto.getUserId());

                for (NioSocketChannel nioSocketChannel : nioSocketChannels) {
    
    
                    // 单端登录
                    if(loginModel == DeviceMultiLoginEnum.ONE.getLoginMode()){
    
    

                        // 获取clietType
                        Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();

                        // 获取imei号
                        String imei = (String)nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get();

                        if(!(clientType + ":" + imei).equals(userClientDto.getClientType() + ":" + userClientDto.getImei())){
    
    
                            // TODO 踢掉客户端
                            // 告诉客户端 其他端登录
                            MessagePack<Object> messagePack = new MessagePack<>();
                            messagePack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
                            messagePack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
                            messagePack.setCommand(SystemCommand.MUTUALLOGIN.getCommand());
                            nioSocketChannel.writeAndFlush(messagePack);
                        }
                    }else if(loginModel == DeviceMultiLoginEnum.TWO.getLoginMode()){
    
    

                        if(userClientDto.getClientType() == ClientType.WEB.getCode()){
    
    
                            continue;
                        }

                        Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();

                        if(clientType == ClientType.WEB.getCode()){
    
    
                            continue;
                        }

                        // 获取imei号
                        String imei = (String)nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get();

                        if(!(clientType + ":" + imei).equals(userClientDto.getClientType() + ":" + userClientDto.getImei())){
    
    
                            // TODO 踢掉客户端
                            MessagePack<Object> messagePack = new MessagePack<>();
                            messagePack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
                            messagePack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
                            messagePack.setCommand(SystemCommand.MUTUALLOGIN.getCommand());
                            nioSocketChannel.writeAndFlush(messagePack);

                        }
                    }else if(loginModel == DeviceMultiLoginEnum.THREE.getLoginMode()){
    
    

                        Integer clientType = (Integer) nioSocketChannel.attr(AttributeKey.valueOf(Constants.ClientType)).get();

                        String imei = (String)nioSocketChannel.attr(AttributeKey.valueOf(Constants.Imei)).get();

                        if(clientType == ClientType.WEB.getCode()){
    
    
                            continue;
                        }

                        Boolean isSameClient = false;

                        // 如果新登录的端和旧的端都是手机端,做处理
                        if((clientType == ClientType.IOS.getCode()
                                || clientType == ClientType.ANDROID.getCode()) &&
                                (userClientDto.getClientType() == ClientType.IOS.getCode()
                                        || userClientDto.getClientType() == ClientType.ANDROID.getCode())){
    
    
                            isSameClient = true;
                        }

                        // 如果新登录的端和旧的端都是电脑端,做处理
                        if((clientType == ClientType.MAC.getCode()
                                || clientType == ClientType.WINDOWS.getCode()) &&
                                (userClientDto.getClientType() == ClientType.MAC.getCode()
                                        || userClientDto.getClientType() == ClientType.WINDOWS.getCode())){
    
    
                            isSameClient = true;
                        }

                        if(isSameClient && !(clientType + ":" + imei).equals(userClientDto.getClientType() + ":" + userClientDto.getImei())){
    
    
                            // TODO 踢掉客户端
                            MessagePack<Object> messagePack = new MessagePack<>();
                            messagePack.setToId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
                            messagePack.setUserId((String) nioSocketChannel.attr(AttributeKey.valueOf(Constants.UserId)).get());
                            messagePack.setCommand(SystemCommand.MUTUALLOGIN.getCommand());
                            nioSocketChannel.writeAndFlush(messagePack);

                        }
                    }
                }
            }
        });
    }
}

This thing is a bit abstract to understand

Because this is the subscription mode of Redis, it should be started together when starting Redis

insert image description here

Guess you like

Origin blog.csdn.net/weixin_52487106/article/details/130654128