# sunrise-common **Repository Path**: lieng618/sunrise-common ## Basic Information - **Project Name**: sunrise-common - **Description**: 基于Netty封装的网络通信模块,使前后端通信变得更方便快捷。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-05 - **Last Updated**: 2026-03-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: Netty, 网络通信 ## README # sunrise-common ### 介绍 基于Netty封装的网络通信模块,使前后端通信变得更方便快捷。 抽象了网络通信模块,基于netty进行封装,创建服务器和客户端更加轻松。 ### 服务器 #### 结构 默认创建的服务器为BaseServer,服务器结构如下: nodeId为节点名称,可在创建时自定义名称。 messageManager为业务消息管理器,服务器默认消息管理器为ServerMessageManager。 pulseHandler为心跳消息处理类,服务器默认心跳处理类为BaseServerPulseHandler。 serverHandler类会处理对端的新连接和对端发来的网络数据,服务器默认处理类为BaseServerHandler。 ``` public class BaseServer { private String nodeId = this.getClass().getSimpleName() + "@" + System.currentTimeMillis(); private Channel channel; private boolean startSuccess; private BaseMessageManager messageManager; private Function serverHandler; private Function pulseHandler; } ``` #### 消息管理器 ServerMessageManager类继承于BaseMessageManager消息管理器基类,一个消息管理器仅会创建单个线程,继承此管理器进行扩展逻辑的实现。 ServerMessageManager类实现了基类的抽象方法: pulseHandlerOne(Object data)方法会处理由客户端发来的字节数据,将字节数据反序列化为BaseMessage结构,打印后将消息内容回发给客户端。 pulseSenderOne(Object data)方法会处理由服务端准备发给客户端的数据,默认情况下会直接通过channel发给客户端。 ``` /** * 服务器默认消息管理器 * 处理消息,回发给客户端 */ public class ServerMessageManager extends BaseMessageManager { public ServerMessageManager(String nodeId) { super(nodeId); } @Override protected void pulseHandlerOne(Object data) { var message = MessageUtils.fromMessage((byte[]) data, BaseMessage.class); LogCore.Server.debug("recv msg, cur NodeId = { {} }, from NodeId = { {} }, messageId = { {} }, data = { {} }", getNodeId(), message.getNodeId(), message.getMessageId(), message.getMsg()); var sendMsg = new BaseMessage(getNodeId(), message.getNodeId(), message.getMessageId(), message.getMsg()); BaseServerManager.sendToClient(sendMsg); } @Override protected void pulseSenderOne(Object data) { BaseServerManager.sendMsgToClient((BaseMessage) data); } } ``` #### 启动 运行TestServerStartUp类,可在单个进程内启动三个服务器。 如下所示,创建默认的服务器有多种方式。 server1节点名称为"node1",绑定到本地的1000端口。 server2传入的节点名称也为"node1",如果同一进程内启动,由于名称重复,则会在创建时分配一个新的名称,绑定到本地的1001端口。 server3并未传入节点名称,则会在创建时分配一个新的名称,绑定到本地的1002端口。 ``` var server1 = BaseServerManager.createBaseServer("node1"); server1.startListen(Utils.getLocalIpAddress(), 1000); var server2 = BaseServerManager.createBaseServer("node1"); server2.startListen(Utils.getLocalIpAddress(), 1001); var server3 = BaseServerManager.createBaseServer(); server3.startListen(Utils.getLocalIpAddress(), 1002); ``` 自定义创建流程如下:此处示例为仓库[sunrise](https://gitee.com/lieng618/sunrise)中Rpc模块中服务节点的实现。 自定义消息处理类RpcServerMessageManager继承了ServerMessageManager基类。 自定义连接处理类RpcServerHandler继承了BaseServerHandler。 使用BaseServer的构造函数进行创建,创建完成后调用setMessageManager和setServerHandler设置为自定义的处理器。 最后调用BaseServerManager.register(rpcServer),将服务注册到服务管理器中。 ``` public RpcNode(int serverId) { this.serverId = serverId; this.rpcServer = new BaseServer(this.getClass().getSimpleName() + serverId) { @Override public void onStart() { super.onStart(); if (dbService != null) { dbService.execute("update `rpc_server_system` set `status` = ?, `ip` = ? where `id` = ?", 1, Utils.getLocalIpAddress(), serverId); } } @Override public void onStop() { if (isStartSuccess()) { super.onStop(); if (dbService != null) { dbService.execute("update `rpc_server_system` set `status` = ? where `id` = ?", 0, serverId); } } } }; rpcServer.setMessageManager(new RpcServerMessageManager(rpcServer.getNodeId())); rpcServer.setServerHandler(r -> new RpcServerHandler(rpcServer.getNodeId())); BaseServerManager.register(rpcServer); } ``` ### 客户端 #### 结构 默认创建的客户端为BaseClient,客户端结构如下: nodeId为节点名称,可在创建时自定义名称。 serverNodeId为当前客户端节点所连接的服务器节点名称。 messageManager为业务消息管理器,客户端默认消息管理器为ClientMessageManager。 pulseHandler为心跳消息处理类,客户端默认心跳处理类为BaseClientPulseHandler。 clientHandler类会处理与服务器连接的激活和服务器发来的网络数据,客户端默认处理类为BaseClientHandler。 ``` public class BaseClient { private String nodeId = this.getClass().getSimpleName() + "@" + System.currentTimeMillis(); private Channel serverChannel; private String serverNodeId; private Boolean connectStatus = false; private BaseMessageManager messageManager; private Function clientHandler; private Function pulseHandler; private EventLoopGroup group; private Bootstrap b; } ``` #### 消息管理器 ClientMessageManager类继承于BaseMessageManager消息管理器基类,一个消息管理器仅会创建单个线程,继承此管理器进行扩展逻辑的实现。 ClientMessageManager类实现了基类的抽象方法: pulseHandlerOne(Object data)方法会处理由服务器发来的字节数据,将字节数据反序列化为BaseMessage结构,打印消息内容。 pulseSenderOne(Object data)方法会处理由客户端准备发给服务器的数据,默认情况下会直接通过channel发给服务器。 ``` /** * 客户端默认消息管理器 * 处理消息,解析打印 */ public class ClientMessageManager extends BaseMessageManager { public ClientMessageManager(String nodeId) { super(nodeId); } @Override protected void pulseHandlerOne(Object data) { var message = MessageUtils.fromMessage((byte[]) data, BaseMessage.class); LogCore.Client.debug("recv msg, cur NodeId = { {} }, from NodeId = { {} }, messageId = { {} }, data = { {} }", getNodeId(), message.getNodeId(), message.getMessageId(), message.getMsg()); } @Override protected void pulseSenderOne(Object data) { BaseClientManager.sendMsgToServer((BaseMessage) data); } } ``` #### 启动 运行TestClientStartUp类,可在单个进程内启动三个客户端。 如下所示,创建默认的客户端有多种方式。 client1节点名称为"client-node1",调用connectBlock()阻塞连接到本地的1000端口。 client2传入的节点名称也为"client-node1",如果同一进程内启动,由于名称重复,则会在创建时分配一个新的名称,调用connectBlock()阻塞连接到本地的1001端口。 client3并未传入节点名称,则会在创建时分配一个新的名称,调用connectBlock()阻塞连接到本地的1002端口。 一般情况可以调用connect()进行异步连接,由于本示例在调用连接后会立即发送消息,避免连接还未完成导致消息丢失,所以使用阻塞连接的方法。 ``` var client1 = BaseClientManager.createBaseClient("client-node1"); client1.connectBlock(Utils.getLocalIpAddress(), 1000); var client2 = BaseClientManager.createBaseClient("client-node1"); client2.connectBlock(Utils.getLocalIpAddress(), 1001); var client3 = BaseClientManager.createBaseClient(); client3.connectBlock(Utils.getLocalIpAddress(), 1002); ``` 自定义创建流程如下:此处示例为仓库[sunrise](https://gitee.com/lieng618/sunrise)中center模块中上报客户端的实现。 自定义消息处理类ReportClientMessageManager继承了ClientMessageManager基类。 自定义连接处理类ReportClientHandler继承了BaseClientHandler。 使用BaseClient的构造函数进行创建,创建完成后调用setMessageManager和setClientHandler设置为自定义的处理器。 最后调用BaseClientManager.register(connectToCenter),将客户端注册到客户端管理器中。 ``` public ReportClient(String nodeId, int type) { this.type = type; this.connectToCenter = new BaseClient(nodeId) { @Override public void onFail() { super.onFail(); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } connectMaster(masterIp, masterPort); } }; ReportClientMessageManager messageManager = new ReportClientMessageManager(connectToCenter.getNodeId()); this.connectToCenter.setClientHandler(r -> new ReportClientHandler(nodeId)); this.connectToCenter.setMessageManager(messageManager); BaseClientManager.register(connectToCenter); } ``` ### 通信 底层网络通信数据结构为SocketMessage,包含消息类型和字节数据,消息类型分为心跳和业务两种。 上层业务一般无需使用此类,此处的data字节数据为BaseMessage序列化后的数据。 ``` public class SocketMessage { int messageType; byte[] data; } public class MessageType { /** 请求命令类型:心跳 */ public static int idle = 0; /** 请求命令类型:业务 */ public static int biz = 1; } ``` 默认通信数据结构为BaseMessage,使用MessageUtils类进行序列化和反序列化。 ``` public class BaseMessage { private String nodeId; //消息所属节点 private String toNodeId; //发送到哪个节点 private long messageId; //消息唯一id private Object msg; //数据 } ``` 此示例为TestClientStartUp类。 可启动TestServerStartUp类服务器后,启动TestClientStartUp进行测试。 c->s时,无需指定toNodeId(因为只会发给自己所连接的服务器),只需指定消息的nodeId(消息所属客户端节点) 。 仅在下面第一种情况下,使用BaseClient自身的发送接口,可无需指定此消息所属的客户端节点id。 client1.sendToServer和BaseClientManager.sendToServer,都会加入到发送队列中,在下一帧进行发送。 BaseClientManager.sendMsgToServer,会立即通过通道进行发送。 ``` // 1.使用BaseMessage的默认构造函数,未指定此消息所属的客户端节点id // 使用client1.sendToServer发送 BaseMessage message1 = new BaseMessage(); message1.setMsg("message1"); client1.sendToServer(message1); //发送成功,由client1发送给所连接的服务器 // 2.使用BaseMessage的默认构造函数,未指定此消息所属的客户端节点id // 使用BaseClientManager.sendToServer发送 BaseMessage message2 = new BaseMessage(); message2.setMsg("message2"); BaseClientManager.sendToServer(message2); //发送失败,并不知道此消息由谁发出 // 3.BaseMessage构造函数传入此消息所属的客户端节点 // 使用client1.sendToServer发送 BaseMessage message3 = new BaseMessage(client1.getNodeId()); message3.setMsg("message3"); client1.sendToServer(message3); //发送成功,由client1发送给所连接的服务器 // 4.BaseMessage构造函数指定此消息所属的客户端节点 // 使用BaseClientManager.sendToServer发送 BaseMessage message4 = new BaseMessage(client1.getNodeId()); message4.setMsg("message4"); BaseClientManager.sendToServer(message4); //发送成功,由client1发送给所连接的服务器 // ... 省略其他构造函数 ``` 此示例为MixtureStartUp类。 可启动MixtureStartUp类进行测试。 s->c时,需同时指定消息的nodeId(消息所属服务器节点)和toNodeId(消息要发给哪个客户端节点),缺一不可。 仅在下面第一种情况下,使用BaseServer自身的发送接口,可无需指定此消息所属的服务器节点id。 server1.sendToClient和BaseServerManager.sendToClient,都会加入到发送队列中,在下一帧进行发送。 BaseServerManager.sendMsgToClient,会立即通过通道进行发送。 ``` // 1.未指定此消息所属的服务端节点id // 使用server1.sendToClient发送 var sendMsg1 = new BaseMessage("", client1.getNodeId(), 0, "s->c 1"); server1.sendToClient(sendMsg1); //发送成功,由server1发送给client1 // 2.未指定此消息所属的服务端节点id // 使用BaseServerManager.sendToClient发送 var sendMsg2 = new BaseMessage("", client1.getNodeId(), 0, "s->c 2"); BaseServerManager.sendToClient(sendMsg2); //发送失败,并不知道此消息由谁发出 // 3.指定此消息所属的服务端节点id // 使用server1.sendToClient发送 var sendMsg3 = new BaseMessage(server1.getNodeId(), client1.getNodeId(), 0, "s->c 3"); server1.sendToClient(sendMsg3); //发送成功,由server1发送给client1 // 4.指定此消息所属的服务端节点id // 使用BaseServerManager.sendToClient发送 var sendMsg4 = new BaseMessage(server1.getNodeId(), client1.getNodeId(), 0, "s->c 4"); BaseServerManager.sendToClient(sendMsg4); //发送成功,由server1发送给client1 // ... 省略其他构造函数 ``` ### 实践 项目[sunrise](https://gitee.com/lieng618/sunrise)中大量使用网络通信。