Backend Development 12 min read

Getting Started with Bitchat: A Netty‑Based Instant Messaging Framework

This guide introduces Bitchat, a Netty‑based IM framework, showing how to start the server and client, use built‑in commands for login, user listing and private messaging, explains its architecture, custom protocol, health‑check mechanisms, and how to handle business logic with asynchronous thread pools.

Java Captain
Java Captain
Java Captain
Getting Started with Bitchat: A Netty‑Based Instant Messaging Framework

Quick Start

The bitchat-example module provides a sample server and client implementation that can be used as a reference for custom business logic.

Start Server

Obtain a Server instance via SimpleServerFactory by specifying a port.

public class StandaloneServerApplication {
    public static void main(String[] args) {
        Server server = SimpleServerFactory.getInstance()
            .newServer(8864);
        server.start();
    }
}

After the server starts, it prints a startup message.

Start Client

Use SimpleClientFactory with a ServerAttr to create a client and connect to the server.

public class DirectConnectServerClientApplication {
    public static void main(String[] args) {
        Client client = SimpleClientFactory.getInstance()
            .newClient(ServerAttr.getLocalServer(8864));
        client.connect();
        doClientBiz(client);
    }
}

Upon successful connection, the client displays a connection message.

Client Functions

The client supports three commands: login, list online users, and send a private message.

Login

Execute -lo username password to log in. The demo uses a mock user service, so any credentials succeed and a user ID is generated.

List Online Users

After logging in with another client, run -lu to retrieve the online user list, which is stored in memory.

Send Private Message

Use -pc 1 hello,houyi where the second argument is the target user ID and the third is the message content.

Client Reconnection

The client and server maintain a heartbeat using Netty's IdleStateHandler . The client sends a PingPacket every 5 seconds; the server replies with a PongPacket . If the server does not receive a ping, it closes the channel, and the client attempts reconnection.

Overall Architecture

Standalone Mode

In standalone mode the architecture consists of a server, a client, and a protocol layer.

Message Center – stores messages, history, and offline queries.

User Center – handles user and group services.

Connection Center – maintains client connections for message push.

Cluster Mode

Cluster mode adds a routing layer so clients can discover available server addresses. When a server receives a message, it checks the Connection Center to determine whether the target user is attached to the same server or another; if the latter, the request is forwarded to the appropriate server.

Custom Protocol

The protocol defines several fields:

*
*
The structure of a Packet is like below:
* +----------+----------+----------------------------+
* |  size    |  value   |  intro                     |
* +----------+----------+----------------------------+
* | 1 bytes  | 0xBC     | magic number               |
* | 1 bytes  |          | serialize algorithm        |
* | 4 bytes  |          | packet symbol              |
* | 4 bytes  |          | content length             |
* | ? bytes  |          | content                    |
* +----------+----------+----------------------------+
*

Fields include magic number, serialization algorithm, packet type, content length, and the actual content. A sync field indicates whether the packet should be processed asynchronously.

Health Check

Both server and client use Netty's IdleStateHandler to detect idle connections. When a read idle event occurs, the channel is closed:

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();
    }
}

The client also runs a HealthyChecker that periodically sends PingPacket s and attempts reconnection on channel inactivity.

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 reconnect...", HealthyChecker.class.getSimpleName());
            client.connect();
        }, 5, TimeUnit.SECONDS);
        ctx.fireChannelInactive();
    }
}

Business Thread Pool

Netty provides boss and worker IO threads; heavy business logic (e.g., DB queries, file I/O) should be offloaded to a separate business thread pool. Packets contain a sync flag that determines whether they are processed synchronously on the IO thread or asynchronously via the business pool.

public class ServerPacketDispatcher extends SimpleChannelInboundHandler
{
    @Override
    public void channelRead0(ChannelHandlerContext ctx, Packet request) {
        if (request.getAsync() == AsyncHandle.ASYNC) {
            EventExecutor channelExecutor = ctx.executor();
            Promise
promise = new DefaultPromise<>(channelExecutor);
            Future
future = executor.asyncExecute(promise, ctx, request);
            future.addListener(new GenericFutureListener
>() {
                @Override
                public void operationComplete(Future
f) throws Exception {
                    if (f.isSuccess()) {
                        Packet response = f.get();
                        writeResponse(ctx, response);
                    }
                }
            });
        } else {
            Packet response = executor.execute(ctx, request);
            writeResponse(ctx, response);
        }
    }
}

Beyond an IM Framework

Bitchat can also serve as a generic communication framework. By extending AbstractPacket and implementing custom PacketHandler s, developers can create various business protocols on top of the same transport layer.

For more details, visit the GitHub repository: https://github.com/all4you/bitchat

JavaIMNettyServerframeworkProtocolClient
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.