Article directory
- 5. The core of IM development is to build a TCP gateway (on)
-
- 1. Write LimServer
- 2. Write LimWebSocketServer
- 3. Use snakeyaml dynamic configuration file
- 4. Talk about communication protocols in vernacular—detailed explanation of mainstream communication protocols
- 5. Private Protocol Encoding and Decoding—Design
- 6. Private Protocol Encoding and Decoding—Implementation
- 6. The core of IM development is to build a TCP gateway (below)
-
- 1. Login message - save user NioSocketChannel
- 2. Distributed cache middleware—Redisson quick start operation
- 3. User login gateway layer - save user Session
- 4. The user exits the gateway layer—deletes the user session offline
- 5. Server heartbeat detection
- 6. Detailed explanation of RabbitMQ installation, publish and subscribe, and routing mode
- 7. TCP access to RabbitMQ, communication with logic layer interaction
- 8. Selection of distributed TCP service registry
- 9. TCP service registration—Zookeeper registers TCP service
- 10. Service Transformation - TCP Service Distributed Transformation
- 11. The instant messaging system supports multi-terminal login mode—to cope with multi-terminal login scenarios
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
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);
}
}
}
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
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)
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
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;
}
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();
}
}
}
add it here
这样一来,我们为LImServer和LImWebSocketServer提供了编解码器,这样我们的客户端只要按照我们的协议发送数据,我们就会拿到争取的数据,我们也可以将信息进行编码,发送给客户端,客户端也要遵守我们的编码规则,也就可以正常的拿到服务端发送给客户端的数据
6. The core of IM development is to build a TCP gateway (below)
1. Login message - save user NioSocketChannel
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
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.
4. The user exits the gateway layer—deletes the user session offline
逻辑
- First delete the Channel
- 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
// 离线
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
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
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);
}
}
}
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
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
In this way, when we start the Starter service, the client connecting to Zookeeper can view the directory you created
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
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
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
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
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
Multi-terminal login mode imitating Tencent Im instant messaging system
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