# simple-netty-core
**Repository Path**: personalhyz/simple-netty-core
## Basic Information
- **Project Name**: simple-netty-core
- **Description**: 对netty的简单封装,自定义了socket通信协议。
mybatis插件:https://gitee.com/personalhyz/simple-netty-mybatis.git
- **Primary Language**: Java
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2024-11-13
- **Last Updated**: 2025-05-21
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Netty
## README
## Simple Socket Project
### simple netty core
#### 一、基本使用
##### 1、包引入
由于包并未上传至 `Maven` 的中心仓库,我们只能手动编译打包源代码,并且安装(`mvn install`)至本地 `Maven仓库` ,在 `pom.xml` 中通过本地 `Maven仓库` 引入:
```xml
org.simplesocket
simple-netty-core
版本
```
> 如果你想剔除包中某个依赖,例如剔除 `netty`,可以这么做:
```xml
org.simplesocket
simple-netty-core
版本
io.netty
netty-all
```
##### 2、快速启动一个服务器
不做过多的配置启动 `simple-netty` ,可以像这样:
```java
public static void main(String[] args) {
SimpleNettyContext.init().run();
}
```
像这么启动的话会使用默认配置,在本地 `127.0.0.1` 的 `8080` 端口上启动 `simple-netty` ,当然,只有测试时才会这么做,一般都要手动配置。
手动配置可以像这样:
```java
public static void main(String[] args) {
SimpleNettyContext.init(SimpleNettyConfig.builder()
.host("192.168.1.1") // 在 192.168.1.1上启动
.port(8080) // 在端口8088启动
.workerThreadNumber(12) // 指定工作线程为12个
.build()).run();
}
```
在这段配置中,我们指定了服务器在 `192.168.1.1` 的 `8080` 端口上启动,并配置了工作线程为 `12` 个。
> 其他配置请看类 `SimpleNettyConfig` 。
##### 3、编写路由处理方法
服务器启动了,我们可以开始编写业务代码,这里的流程与 `springboot` 很像,首先你得定义一个 `路由入口` 类,并添加上注解:
```java
@RouterEndpoint
public class TestRoute {
}
```
> 此时,类 `TestRoute` 就被标记成为了一个路由类,可以在其内部定义路由方法。
> 我们也可以像 `springboot` 那样定义路由方法:
```java
@Route("/test")
public String test(@InjectParam("name")String name,
@InjectParam("age")Integer age,
@InjectParam("school")School school,
@InjectContext SimpleNettyContext context,
@InjectRequest Request request,
@InjectCTX ChannelHandlerContext ctx){
System.out.println("收到参数 >>>>>>>>>>> ");
System.out.println(name);
System.out.println(age);
System.out.println(school.toString());
System.out.println("Request中有参数:" + request.getSessionID());
System.out.println("CTX: " + ctx.channel().remoteAddress().toString());
return "Ok.This message is from simple-netty !!!";
}
```
> 其中,注解 `@InjectParam` 的作用与 `springboot` 中的 `@RequestParam` 一样,都是为形参注入实参。
> `***` 特别注意,参数一定要添加 `@InjectParam` 注解,否则无法识别。
> `@InjectContext` 可以注入 `simple-netty-core` 的上下文对象。
> 注解 `@InjectRequest` 的作用是注入 `Request` 对象。而 `@InjectCTX` 的作用则是注入 `ChannelHandlerContext` 对象。
> 路由方法的返回值可以是任意类型,但是客户端得遵守这个类型方才能解析。
##### 5、客户端事件回调
在处理业务逻辑的时候,我们希望监控一些客户端的事件,例如:客户端初次连接、客户端连接成功、客户端断开连接、客户端重新连接等等。
于是便需要 `simple-netty` 通知我们,我们方能做一些操作。
> 只需实现一个接口,并添加上注解:
```java
@Callback
public class TestCallback implements ClientEventCallback {
}
```
> 并且实现它的我们关注的方法即可:
```java
@Override
public void onFirstConnect(String sessionID) {
System.out.println("onFirstConnect callback,value is:" + sessionID);
}
@Override
public void onConnected(ClientSessionContext csc) {
System.out.println("onConnected callback,value is:" + csc.getSessionID());
}
@Override
public void onDisconnect(ClientSessionContext csc) {
System.out.println("onDisconnect callback,value is:" + csc.getSessionID());
}
@Override
public void onReconnected(String previousSessionID) {
System.out.println("onReconnected callback,value is:" + previousSessionID);
}
```
在这些事件发生的时候,`simple-netty` 就会来调用我们实现的这些方法。
> `*` 其他回调正在开发中...
#### 二、SP通信协议(Simple Protocol)
##### 1、通信协议基本定义
客户端要想与 `simple-netty` 通信,必需要遵守一个协议,它的基本组成如下:
```txt
---------------------------------------------------------------------------------------------------
长度4字节 | 魔数4字节 | 类型1字节 | 状态1字节 | sessionID32字节 | 路径长度4字节 | 路径N字节 | 数据N字节
---------------------------------------------------------------------------------------------------
```
其中:
###### 1、长度
> 整条消息的总长度(不包括长度占用字符在内),占用四个字节
###### 2、魔数
> 用于校验此消息是否有效,`0xFADEDFAD` 转换为 `int` 是 `-86057043`,占用四个字节,32位bit位。
> faded 褪色的;消褪的;fad 时尚
###### 3、类型
> 标记本次数据包是做什么的,类型目前有:`NORMAL(0)`、`CONNECTED(1)`、`DISCUSS_KEYS(2)`、`RECONNECT(3)`。
> 它们分别对应,普通业务数据包、连接确认数据包、商议加密格式数据包、重连数据包
###### 4、状态
> 这个类似 `http` 中的 `200` 、`500` 等状态码,只不过我们是简单的简化的,它们是:`NORMAL(0)`、`SUCCESS(1)`、`FAIL(2)`、`ERROR(3)`、`UNAUTHORIZED(4)`
> 对应了:无状态、成功、失败、错误、未授权
###### 5、SessionID
> 这是服务端为了标识客户端而颁发的客户端唯一值,它是 `32` 位的 `uuid` 。
###### 6、路径长度
> 它标识了其后的 `路径` 占用的字节长度,用于读取不定长字符串路径。
###### 7、路径
> 不定长的字符串路径,占用字节未知,一般是现计算。用于路由到不同的请求处理方法上。
###### 8、数据
> 你可以理解为 `http` 中的请求体,真正的数据载体。
在这种格式下,我们的协议最短是:
```txt
---------------------------------------------------------------------------------------------------
长度4字节 | 魔数4字节 | 类型1字节 | 状态1字节 | sessionID32字节 | 路径长度4字节 | 路径0字节 | 数据0字节
---------------------------------------------------------------------------------------------------
```
> 4 + 4 + 1 + 1 + 32 + 4 + 0 + 0 = 46 字节
假如有数据:
```txt
长度4字节 | 魔数4字节 | 类型1字节 | 状态1字节 | sessionID32字节 | 路径长度4字节 | 路径 /test 5字节 | 数据 hello 5字节
```
> 那么长度为:4 + 4 + 1 + 1 + 32 + 4 + 5 + 5 = 58 字节
> 这个数据包我们看起来就长这样:58 | 0xFADEDFAD | 0 | 0 | cac496b1c3524c19ab2c84efaead314d | 5 | /test | hello
#### 三、连接握手
##### 1、握手过程
`simple-netty` 的握手过程相较于 `http` 来说至简,服务端只有两次握手,它们是:
```txt
客户端发起连接 -> 服务器收到连接,生成一个sessionID -> 客户端收到sessionID,生成一个RSA公钥 -> 服务端得到RSA公钥 ->
-> 服务端生成AES密钥,并使用客户端的RSA公钥加密AES密钥 -> 客户端得到加密后的AES并使用RSA私钥解密 -> 连接建立完成
```
整个过程的核心就是客户端从服务端获取 `AES` 密钥,它用于加密协议最后不定长的数据,`AES` 的加密解密效率比 `RSA` 快得多。
在获取 `AES` 的过程中,使用了 `RSA` 非对称加密来保护对称密钥 `AES` 的安全,客户端发给服务端的 `RSA` 公钥即使泄露了也没关系。
##### 2、断线重连
> `***` `1.0.0`版本有漏洞,请勿使用该功能
相较于连接服务器,断线重连也是一个重要的功能。
在客户端断开连接后,再去连接服务端的话,会被认为是一个新的客户端,而不被认为是之前连接过的客户端。
这就需要客户端在重新连接服务端后,将请求数据包的类型改为 `RECONNECT(3)` ,并且携带一个之前的断开连接前的 `sessionID` ,服务端会将这个 `sessionID` 与当前的连接关联起来,在关联完毕后,客户端可以将请求数据包中的 `sessionID` 字段改为之前的 `sessionID` ,从而达到恢复之前的会话功能。
#### 四、协议加密
##### 1、加密方式
在前面我们在握手中已经详细解释过服务端加密的方式了,加密就是使用:非对称加密`RSA` 来加密 对称加密`AES` 的密钥。
兼顾了效率和安全。
##### 2、密钥细节
客户端在加密解密时,最好参考源代码 `test/js` 目录下的 `aes-util.mjs` 的加解密方式,否则密钥格式不正确的话会加解密失败。
#### 五、加载第三方配置
##### 1、在整个服务器的上下文中添加自定义插件类
某些时候,我们需要在服务器启动时添加一些类供后续使用,例如在服务器启动时想要添加 `Mybatis` 的配置上下文,那么只需要实现接口 `ConfigLoader` ,并且实现 `load()` 方法,例如 `simple-netty-mybatis` 插件的部分插件加载细节:
```java
public class SimpleNettyMybatisConfig implements ConfigLoader{
@Override
public void load(SimpleNettyContext simpleNettyContext) {
SimpleNettyMybatisContext.init(simpleNettyContext,this);
}
}
```
服务器启动时,就会初始化 `Mybatis` 插件,将所需的 `Mybatis` 组件添加到服务器上下文,供后续使用。
##### 2、原理
服务器在启动时,会通过循环调用所有实现了 `load()` 方法的类,我们只需要在 `load()` 方法中启动我们自定义的插件就能将整个插件运行起来了,实现原理参考了 `spring` 的 `bean` 加载。
#### 六、自定义Channel参数(Channel Attr)
每个客户端都对应一个唯一的 `channel` ,这个 `channel` 中存储了 `simple-netty-core` 所需的一些信息,例如加密密钥、客户端会话ID等,在收到数据时,我们有时想要从发送数据的 `channel` 中获取一些值(或者存储一些值),那么就可以使用 `simple-netty-core` 提供的 `ChannelAttr` 类,通过:
```java
channel.attr(ChannelAttr.CLIENT_SESSION_CONTEXT).get()
```
即可获取参数存储对象 `ClientSessionContext` ,其中就存储了客户端的RSA公钥、服务器为其颁发的RSA公私钥、AES密钥等信息。我们也可以自定义一些信息放到其中(例如客户端登录后的账号ID等)。仅需要继承 `ClientSessionContext` 类,并且将 `simple-netty-core` 传递的 `ClientSessionContext` 转为自定义的继承类即可。例如,我们自定义了一个类:
```java
public class CustomClientSessionContext extends ClientSessionContext {
private String userAccount;
}
```
那么在使用时仅需:
```java
ClientSessionContext csc = channel.attr(ChannelAttr.CLIENT_SESSION_CONTEXT).get();
CustomClientSessionContext cCsc = csc.transform(CustomClientSessionContext.class); // 调用 transform() 方法强制转换
cCsc.setUserAccount(account)
channel.attr(ChannelAttr.CLIENT_SESSION_CONTEXT).set(cCsc); // 再次存储channel中,下次即可获取到刚存入的account值
```
> `***` 注意,这里不能使用类型强制转换,必需使用 `transform()` 方法来转,否则报错。
#### 七、全局异常处理
全局异常处理是一个必不可少的需求,所以 `simple-netty-core` 提供了全局异常处理的能力,使用时仅需实现接口 `GlobalExceptionHandler` 并在实现类上添加注解 `@GlobalExceptionProcessor` 即可。例如:
```java
@GlobalExceptionProcessor
public class GlobalException implements GlobalExceptionHandler {
@Override
public String handle(Throwable throwable) {
return "服务器错误!";
}
}
```
它会在路由方法(被 `@Route` 注解的方法)后捕捉前面未捕捉的异常,并且调用 `handle()` 方法向客户端返回自定义的值。
返回值的类型取决于 `GlobalExceptionHandler` 中泛型的值。