{"id":20003285,"url":"https://github.com/all4you/bitchat","last_synced_at":"2025-04-07T07:17:56.834Z","repository":{"id":35453975,"uuid":"181920813","full_name":"all4you/bitchat","owner":"all4you","description":"一个基于Netty的网络框架，同一端口支持HTTP/自定义TCP协议/WebSocket协议，支持多种序列化方式","archived":false,"fork":false,"pushed_at":"2024-06-05T16:11:20.000Z","size":3637,"stargazers_count":235,"open_issues_count":6,"forks_count":94,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-03-31T06:03:48.413Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/all4you.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-04-17T15:37:26.000Z","updated_at":"2025-02-27T11:06:45.000Z","dependencies_parsed_at":"2024-12-16T14:44:47.485Z","dependency_job_id":"0e98e28f-0b68-4477-ae3b-085815f3a62a","html_url":"https://github.com/all4you/bitchat","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/all4you%2Fbitchat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/all4you%2Fbitchat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/all4you%2Fbitchat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/all4you%2Fbitchat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/all4you","download_url":"https://codeload.github.com/all4you/bitchat/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247608160,"owners_count":20965953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-13T05:25:00.763Z","updated_at":"2025-04-07T07:17:56.816Z","avatar_url":"https://github.com/all4you.png","language":"Java","funding_links":[],"categories":["网络编程"],"sub_categories":["Spring Cloud框架"],"readme":"# bitchat\n\n**bitchat** 是一个基于 Netty 的网络框架\n\n **特性:**\n\n- [x] **自定义协议**  : 一个自定义的 Packet 协议，业务的扩展非常简单\n- [x] **支持WebSocket协议**  : 在同一端口上支持自定义的Packet协议以及Http、WebSocket协议\n- [x] **编解码器**  : 内置 PacketCodec 和 FrameCodec 编解码器，解决拆包粘包的问题\n- [x] **统一的业务处理器**  : 通过抽象的 Processor 统一了 Packet 协议和 WebSocket 协议的处理流程\n- [x] **可选的业务处理方式**  : 服务端支持同步或异步的业务处理， 可以由客户端在 Packet 协议中自主选择，默认是在业务线程池中异步处理\n- [x] **可选的序列化方式**  : 支持多种序列化方式，可以由客户端在 Packet 协议中自主选择，默认是 ProtoStuff方式\n- [x] **单机模式**  : 支持单机模式\n- [x] **心跳检测** : 服务端与客户端自带心跳检查机制，客户端支持断线重连\n- [x] **Channel管理** : 管理所有连接上的 Channel，并支持通过 Rest 接口查询\n- [x] **Session管理** : 管理所有登录并绑定到 Channel 上的 Session，并支持通过 Rest 接口查询\n\n**TODO：**\n\n- [ ] **集群模式**  : 支持服务端的集群方式部署，形成一个 Router 层，客户端通过 Router 获取可用的服务端实例\n\n\n\n## 服务端入口\n\n服务端启动的入口为：`io.bitchat.server.ServerShell` \n\n目前只实现了单机模式下的 Server ，通过 ServerBootstrap 只需要定义一个端口即可获取一个单机的 Server 实例，如下所示：\n\n```java\npublic class ServerShell {\n    public static void main(String[] args) {\n        ServerStartupParameter param = new ServerStartupParameter();\n        JCommander.newBuilder()\n                .addObject(param)\n                .build()\n                .parse(args);\n        ServerMode serverMode = ServerMode.getEnum(param.mode);\n        RouterServerAttr routerServerAttr = RouterServerAttr.builder()\n                .address(param.routerAddress)\n                .port(param.routerPort)\n                .build();\n        Integer serverPort = param.serverPort;\n    \n        ServerBootstrap bootstrap = new ServerBootstrap();\n        bootstrap.serverMode(serverMode)\n                .routerServerAttr(routerServerAttr)\n                .start(serverPort);\n    }\n}\n```\n\n\n\n\n\n## 自定义协议\n\n通过一个自定义协议来实现服务端与客户端之间的通讯，协议中有如下几个字段：\n\n```java\n*\n* \u003cp\u003e\n* The structure of a Packet is like blow:\n* +----------+----------+----------------------------+\n* |  size    |  value   |  intro                     |\n* +----------+----------+----------------------------+\n* | 1 bytes  | 0xBC     |  magic number              |\n* | 1 bytes  |          |  serialize algorithm       |\n* | 1 bytes  |          |  the type 1:req 2:res 3:cmd|\n* | 4 bytes  |          |  content length            |\n* | ? bytes  |          |  the content               |\n* +----------+----------+----------------------------+\n* \u003c/p\u003e\n*\n```\n\n每个字段的含义\n\n| 所占字节 | 用途              |\n| -------- | ----------------- |\n| 1        | 魔数，默认为 0xBC |\n| 1        | 序列化的算法      |\n| 1        | Packet 的类型     |\n| 4        | Packet 的内容长度 |\n| ?        | Packet 的内容     |\n\n序列化算法将会决定该 Packet 在编解码时，使用何种序列化方式。\n\nPacket 的类型将会决定到达服务端的字节流将被反序列化为何种 Packet，也决定了该 Packet 将会被哪个 PacketHandler 进行处理。\n\n内容长度将会解决 Packet 的拆包与粘包问题，服务端在解析字节流时，将会等到字节的长度达到内容的长度时，才进行字节的读取。\n\n除此之外，Packet 中还会存储一个 handleAsync 字段，该字段将指定服务端在处理该 Packet 的数据时是否需要使用异步的业务线程池来处理。\n\n\n\n## 健康检查\n\n服务端与客户端各自维护了一个健康检查的服务，即 Netty 为我们提供的 IdleStateHandler，通过继承该类，并且实现 channelIdle 方法即可实现连接 “空闲” 时的逻辑处理，当出现空闲时，目前我们只关心读空闲，我们既可以认为这条链接出现问题了。\n\n那么只需要在链接出现问题时，将这条链接关闭即可，如下所示：\n\n```java\npublic class IdleStateChecker extends IdleStateHandler {\n\n    private static final int DEFAULT_READER_IDLE_TIME = 15;\n\n    private int readerTime;\n\n    public IdleStateChecker(int readerIdleTime) {\n        super(readerIdleTime == 0 ? DEFAULT_READER_IDLE_TIME : readerIdleTime, 0, 0, TimeUnit.SECONDS);\n        readerTime = readerIdleTime == 0 ? DEFAULT_READER_IDLE_TIME : readerIdleTime;\n    }\n\n    @Override\n    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) {\n        log.warn(\"[{}] Hasn't read data after {} seconds, will close the channel:{}\", \n        IdleStateChecker.class.getSimpleName(), readerTime, ctx.channel());\n        ctx.channel().close();\n    }\n\n}\n```\n\n另外，客户端需要额外再维护一个健康检查器，正常情况下他负责定时向服务端发送心跳，当链接的状态变成 inActive 时，该检查器将负责进行重连，如下所示：\n\n```java\npublic class HealthyChecker extends ChannelInboundHandlerAdapter {\n\n    private static final int DEFAULT_PING_INTERVAL = 5;\n\n    private Client client;\n\n    private int pingInterval;\n\n    public HealthyChecker(Client client, int pingInterval) {\n        Assert.notNull(client, \"client can not be null\");\n        this.client = client;\n        this.pingInterval = pingInterval \u003c= 0 ? DEFAULT_PING_INTERVAL : pingInterval;\n    }\n\n    @Override\n    public void channelActive(ChannelHandlerContext ctx) throws Exception {\n        super.channelActive(ctx);\n        schedulePing(ctx);\n    }\n\n    private void schedulePing(ChannelHandlerContext ctx) {\n        ctx.executor().schedule(() -\u003e {\n            Channel channel = ctx.channel();\n            if (channel.isActive()) {\n                Packet pingPacket = PacketFactory.newPingPacket();\n                log.debug(\"[{}] Send a Ping={}\", HealthyChecker.class.getSimpleName(), pingPacket);\n                channel.writeAndFlush(pingPacket);\n                schedulePing(ctx);\n            }\n        }, pingInterval, TimeUnit.SECONDS);\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) throws Exception {\n        ctx.executor().schedule(() -\u003e {\n            log.info(\"[{}] Try to reconnecting...\", HealthyChecker.class.getSimpleName());\n            client.connect();\n        }, 5, TimeUnit.SECONDS);\n        ctx.fireChannelInactive();\n    }\n\n}\n```\n\n\n\n## 业务线程池\n\n我们知道，Netty 中维护着两个 IO 线程池，一个 boss 主要负责链接的建立，另外一个 worker 主要负责链接上的数据读写，我们不应该使用 IO 线程来处理我们的业务，因为这样很可能会对 IO 线程造成阻塞，导致新链接无法及时建立或者数据无法及时读写。\n\n为了解决这个问题，我们需要在业务线程池中来处理我们的业务逻辑，但是这并不是绝对的，如果我们要执行的逻辑很简单，不会造成太大的阻塞，则可以直接在 IO 线程中处理，比如客户端发送一个 Ping 服务端回复一个 Pong，这种情况是没有必要在业务线程池中进行处理的，因为处理完了最终还是要交给 IO 线程去写数据。但是如果一个业务逻辑需要查询数据库或者读取文件，这种操作往往比较耗时间，所以就需要将这些操作封装起来交给业务线程池去处理。\n\n服务端允许客户端在传输的 Packet 中指定采用何种方式进行业务的处理，服务端在将字节流解码成 Packet 之后，会根据 Packet 中的 handleAsync 字段的值，确定怎样对该 Packet 进行处理，如下所示：\n\n```java\npublic class PacketHandler extends \n    SimpleChannelInboundHandler\u003cPacket\u003e {\n    \n    private void onRequest(ChannelHandlerContext ctx, Packet packet) {\n        // if the packet should be handled async\n        if (packet.isHandleAsync()) {\n            EventExecutor channelExecutor = ctx.executor();\n            // create a promise\n            Promise\u003cPacket\u003e promise = new DefaultPromise\u003c\u003e(channelExecutor);\n            // async execute and get a future\n            Future\u003cPacket\u003e future = executor.asyncExecute(promise, ctx, packet);\n            future.addListener(new GenericFutureListener\u003cFuture\u003cPacket\u003e\u003e() {\n                @Override\n                public void operationComplete(Future\u003cPacket\u003e f) throws Exception {\n                    if (f.isSuccess()) {\n                        Packet response = f.get();\n                        writeResponse(ctx, response);\n                    }\n                }\n            });\n        } else {\n            // sync execute and get the response packet\n            Packet response = executor.execute(ctx, packet);\n            writeResponse(ctx, response);\n        }\n    }\n}\n```\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fall4you%2Fbitchat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fall4you%2Fbitchat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fall4you%2Fbitchat/lists"}