bit0chat
bit0chat is a Netty-based IM instant messaging framework
PS: bit0chat, there is no 0 after the bit. Open Source China thinks that my article contains abusive information. After thinking about it for a long time, this may be the only reason!
Project address: https://github.com/all4you/bit0chat (delete 0)
characteristic:
- [x] IOC container : All objects can be managed through @Bean annotation, and object injection through @Autowired annotation
- [x] Custom protocol : a custom Packet protocol, business expansion is very simple
- [x] Codec : Built-in PacketCodec codec to solve the problem of unpacking and sticking
- [x] Business processor : The business processor PacketHandler is separated from Packet and supports various custom business processors
- [x] Optional business processing methods : The server supports synchronous or asynchronous business processing, which can be independently selected by the client in the Packet protocol. The default is asynchronous processing in the business thread pool.
- [x] Optional serialization method : supports multiple serialization methods, which can be independently selected by the client in the Packet protocol, the default is the ProtoStuff method
- [x] Stand-alone mode : supports stand-alone mode
- [x] Heartbeat detection : The server and the client have their own heartbeat checking mechanism, and the client supports disconnection and reconnection
EVERYTHING:
- [ ] Cluster mode : It supports the cluster deployment of the server, forming a router layer, and the client obtains the available server instances through the router
- [ ] Connection center : A Connection center, which is currently stored in memory, and needs to support the persistence of Connection in the future.
- [ ] Message center : query for message storage
- [ ] User Center : User and Group Management
quick start
The bit0chat-example module provides an implementation example of the server and the client. You can refer to this example to implement your own business.
Start the server
To start the server, you need to obtain an instance of Server, which can be obtained through ServerFactory.
At present, only Server in stand-alone mode is implemented. Through SimpleServerFactory, only one port needs to be defined to obtain a stand-alone Server instance, as shown below:
public class StandaloneServerApplication {
public static void main(String[] args) {
Server server = SimpleServerFactory.getInstance()
.newServer(8864);
server.start();
}
}
After the server starts successfully, the following information will be displayed:
start the client
At present, only the client directly connected to the server is implemented. Through SimpleClientFactory, you only need to specify a ServerAttr to obtain a client, and then connect the client to the server, as shown below:
public class DirectConnectServerClientApplication {
public static void main(String[] args) {
Client client = SimpleClientFactory.getInstance()
.newClient(ServerAttr.getLocalServer(8864));
client.connect();
doClientBiz(client);
}
}
After the client connects to the server, the following information will be displayed:
Experience the functionality of the client
At present, the client provides three kinds of Func, namely: login, view online user list, send single chat message, each Func has different command format.
Log in
You -lo houyi 123456
can . Currently, the user center has not been implemented. A fake user service is implemented by means of Mock, so entering any username and password will log in successfully, and a user id will be created for the user. .
After successful login, the display is as follows:
View online users
Start another client, and also execute the login. After successful login, you can execute the -lu
command to obtain the list of online users. Currently, the users are stored in the memory, and the obtained results are as follows:
Send a single chat message
Use the user gris to send a single chat message to the user houyi, just execute the -pc 1 hello,houyi
command
The second parameter is the user id of the user to send the message to, and the third parameter is the message content
Message sender, after sending the message:
Message receiver, received the message:
Client disconnection and reconnection
A heartbeat is maintained between the client and the server. Both parties will check whether the connection is available. The client will send a PingPacket to the server every 5s. After receiving the PingPacket, the server will reply with a PongPacket, which means that both parties are healthy.
When for some reason, the server does not receive the message sent by the client, the server will disconnect the client from the connection, and the same client will also do this check.
When the connection between the client and the server is disconnected, the channelInactive method of the client HealthyChecker will be triggered to reconnect the client.
Overall structure
single vision
The architecture of the stand-alone version only involves the server, the client, and the protocol layer between the two, as shown in the following figure:
In addition to the server and the client, there are three major centers: the message center, the user center, and the link center.
- Message center: mainly responsible for message storage and history, offline message query
- User Center: mainly responsible for services related to users and groups
- Link Center: It is mainly responsible for saving the link of the client. The server obtains the link of the client from the link center and pushes messages to it.
Cluster Edition
The stand-alone version cannot achieve high availability, and the performance and the number of users that can be served are also limited. Therefore, a scalable cluster version is required. The cluster version adds a routing layer on the basis of the stand-alone version, and the client obtains it through the routing layer. The available server address, and then communicate with the server, as shown in the following figure:
The client sends a message to another user. After the server receives the request, it obtains from the Connection Center which server the target user is "hanging" on. If it is under its own name, it is easiest to directly push the message to the target user. Yes, if it is on another server, the request needs to be forwarded to the target server, so that the target server can push the message to the target user.
custom protocol
The communication between the server and the client is realized through a custom protocol. The protocol has the following fields:
*
* <p>
* The structure of a Packet is like blow:
* +----------+----------+----------------------------+
* | size | value | intro |
* +----------+----------+----------------------------+
* | 1 bytes | 0xBC | magic number |
* | 1 bytes | | serialize algorithm |
* | 4 bytes | | packet symbol |
* | 4 bytes | | content length |
* | ? bytes | | the content |
* +----------+----------+----------------------------+
* </p>
*
the meaning of each field
bytes | use |
---|---|
1 | Magic number, default is 0xBC |
1 | serialization algorithm |
4 | Type of Packet |
4 | Content Length of Packet |
? | Contents of Packet |
The serialization algorithm will determine which serialization method the Packet uses when encoding and decoding.
The type of Packet will determine what kind of Packet the byte stream arriving at the server will be deserialized into, and which PacketHandler will process the Packet.
The content length will solve the problem of packet unpacking and sticking. When the server parses the byte stream, it will wait until the length of the byte reaches the length of the content before reading the byte.
In addition, a sync field is also stored in the Packet, which specifies whether the server needs to use an asynchronous business thread pool to process the data of the Packet.
health examination
The server and the client each maintain a health check service, that is, the IdleStateHandler provided by Netty. By inheriting this class and implementing the channelIdle method, the logic processing when the connection is "idle" can be realized. When there is idle, we currently We only care about reading idleness, and we can think that there is a problem with this link.
Then just close the link when there is a problem with the link, as shown below:
public class IdleStateChecker extends IdleStateHandler {
private static final int DEFAULT_READER_IDLE_TIME = 15;
private int readerTime;
public IdleStateChecker(int readerIdleTime) {
super(readerIdleTime == 0 ? DEFAULT_READER_IDLE_TIME : readerIdleTime, 0, 0, TimeUnit.SECONDS);
readerTime = readerIdleTime == 0 ? DEFAULT_READER_IDLE_TIME : readerIdleTime;
}
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) {
log.warn("[{}] Hasn't read data after {} seconds, will close the channel:{}",
IdleStateChecker.class.getSimpleName(), readerTime, ctx.channel());
ctx.channel().close();
}
}
In addition, the client needs to maintain an additional health checker. Under normal circumstances, he is responsible for sending heartbeats to the server regularly. When the status of the link becomes inActive, the checker will be responsible for reconnecting, as shown below:
public class HealthyChecker extends ChannelInboundHandlerAdapter {
private static final int DEFAULT_PING_INTERVAL = 5;
private Client client;
private int pingInterval;
public HealthyChecker(Client client, int pingInterval) {
Assert.notNull(client, "client can not be null");
this.client = client;
this.pingInterval = pingInterval <= 0 ? DEFAULT_PING_INTERVAL : pingInterval;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
schedulePing(ctx);
}
private void schedulePing(ChannelHandlerContext ctx) {
ctx.executor().schedule(() -> {
Channel channel = ctx.channel();
if (channel.isActive()) {
log.debug("[{}] Send a PingPacket", HealthyChecker.class.getSimpleName());
channel.writeAndFlush(new PingPacket());
schedulePing(ctx);
}
}, pingInterval, TimeUnit.SECONDS);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.executor().schedule(() -> {
log.info("[{}] Try to reconnecting...", HealthyChecker.class.getSimpleName());
client.connect();
}, 5, TimeUnit.SECONDS);
ctx.fireChannelInactive();
}
}
business thread pool
We know that Netty maintains two IO thread pools, one boss is mainly responsible for the establishment of the link, and the other worker is mainly responsible for reading and writing data on the link. We should not use IO threads to process our business, because it is likely to cause The IO thread is blocked, resulting in the inability to establish new links in time or the inability of data to be read and written in time.
In order to solve this problem, we need to process our business logic in the business thread pool, but this is not absolute, if the logic we want to execute is very simple and will not cause too much blocking, we can directly in the IO thread Processing, for example, the client sends a Ping and the server replies with a Pong. In this case, it is not necessary to process it in the business thread pool, because the IO thread will eventually be handed over to write data after processing. However, if a business logic needs to query the database or read files, it is often time-consuming, so these operations need to be encapsulated and handed over to the business thread pool for processing.
The server allows the client to specify which way to process the business in the transmitted Packet. After the server decodes the byte stream into a Packet, it will determine how to process the Packet according to the value of the sync field in the Packet. As follows:
public class ServerPacketDispatcher extends
SimpleChannelInboundHandler<Packet> {
@Override
public void channelRead0(ChannelHandlerContext ctx, Packet request) {
// if the packet should be handled async
if (request.getAsync() == AsyncHandle.ASYNC) {
EventExecutor channelExecutor = ctx.executor();
// create a promise
Promise<Packet> promise = new DefaultPromise<>(channelExecutor);
// async execute and get a future
Future<Packet> future = executor.asyncExecute(promise, ctx, request);
future.addListener(new GenericFutureListener<Future<Packet>>() {
@Override
public void operationComplete(Future<Packet> f) throws Exception {
if (f.isSuccess()) {
Packet response = f.get();
writeResponse(ctx, response);
}
}
});
} else {
// sync execute and get the response packet
Packet response = executor.execute(ctx, request);
writeResponse(ctx, response);
}
}
}
Not just IM frameworks
In addition to being an IM framework, bit0chat can also be used as a general communication framework.
As a carrier of communication, Packet can quickly realize its own business by inheriting AbstractPacket, and with PacketHandler as a data processor, the communication between the client and the server can be realized.