Netty in action: Designing an IM framework

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:

server-startup.jpg

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:

client-connect.jpg

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 123456can . 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:

login.jpg

View online users

Start another client, and also execute the login. After successful login, you can execute the -lucommand to obtain the list of online users. Currently, the users are stored in the memory, and the obtained results are as follows:

list-user.jpg

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,houyicommand

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:

send-p2p-msg.jpg

Message receiver, received the message:

received-p2p-msg.jpg

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.

client-reconnect.jpg

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:

stand-alone-arch.jpg

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:

cluster-arch.jpg

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.

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324133121&siteId=291194637