框架特点
高性能、高并发、低延迟、绿色节能。
代码量极少,可读性强。核心代码不到 1500 行,工程结构、包层次清晰。
良好的线程模型、内存模型设计,保障服务高效稳定的运行。
支持自定义插件,并已提供了丰富的插件,包括:SSL/TLS通信插件、心跳插件、断链重连插件、服务指标统计插件、黑名单插件、内存池监测插件。
国内开源框架,一些内部注释为中文,可读性高
使用,核心接口:Protocol、MessageProcessor、NetMonitor
代码只用于协助理解不建议直接复制,不保证跑通
JDK 1.8
pom.xml
<!-- smart socket依赖, pro版本支持UDP通信,1.5版本只支持到jdk1.8,11及以上需要引用更高版本 -->
<dependency>
<groupId>org.smartboot.socket</groupId>
<artifactId>aio-pro</artifactId>
<version>1.5.42</version>
</dependency>
<!-- commons-lang3包含了一系列用于处理字符串、基本数值、对象反射、并发、创建和序列化以及系统属性的工具类。-->
<!-- 例如,它提供了StringUtils类,这个类包含了许多用于处理字符串的静态方法,-->
<!-- 如缩短字符串、检查字符串是否为空、将字符串转换为其他类型等-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
实现 Protocol 接口 覆写decode方法,此方法通过操作缓冲区解析通过输入流获取的报文,并将解析结果返回。
知识点
java.nio.Buffer 是 Java NIO(New I/O)中的一个核心类,它是所有缓冲类的超类,如 ByteBuffer, CharBuffer, IntBuffer, LongBuffer, FloatBuffer, 和 DoubleBuffer 等。这些缓冲类用于在 Java 程序中处理原始数据类型,并且经常与 NIO 的通道(Channel)一起使用,以实现高效的文件和网络 I/O 操作。
Buffer 的主要属性和方法如下:
属性
capacity: 缓冲区的容量,即它可以包含的元素的最大数量。这个值在缓冲区创建时被设定,并且不能改变。
limit: 缓冲区的第一个不能被读或写的元素的索引。换句话说,它是缓冲区中现有数据的末尾位置的下一个索引。对于新创建的缓冲区,limit 通常等于 capacity。
position: 下一个要被读或写的元素的索引。位置会自动由相应的 get() 和 put() 函数更新。
mark: 一个可选的索引,用于 reset() 方法将位置设置回之前保存的位置。不是所有的 Buffer 实现都需要支持 mark。
方法
clear(): 清除此缓冲区。position被设为 0,limit 被设为 capacity,并且 mark 被丢弃。
flip(): 准备一个新的序列。这会将 limit 设置为当前位置,然后将position设置为 0。
rewind(): 重绕此缓冲区。position将被设为 0,mark 被丢弃,但是 limit 保持不变。
position(int newPosition): 设置此缓冲区的position。
limit(int newLimit): 设置此缓冲区的 limit。
mark(): 在此缓冲区的position设置 mark。
reset(): 将此缓冲区的position设置为以前标记的位置。
remaining(): 返回当前position与 limit 之间的元素数。
import org.smartboot.socket.Protocol;
import org.smartboot.socket.transport.AioSession;
import org.springframework.stereotype.Component;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@Component
public class TcpProtocol implements Protocol<String> {
@Override
public String decode(ByteBuffer readBuffer, AioSession session) {
//获取缓冲区现有数据长度
int len = readBuffer.remaining();
//获取缓冲区现有数据
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
bytes[i] = readBuffer.get();
}
//解码
String data = new String(bytes, StandardCharsets.UTF_8);
//判断是否为空
if (StringUtils.isNotBlank(data)) {
readBuffer.position(readBuffer.limit());
return null;
}
//包头包尾校验
if (TcpConstant.PREFIX.charAt(0) != (data.charAt(0))
|| TcpConstant.SUFFIX.charAt(0) != (data.charAt(data.length() - 1))) {
readBuffer.position(readBuffer.limit());
return null;
}
return data.split(TcpConstant.PREFIX)[1].split(TcpConstant.PREFIX)[0];
}
}
实现MessageProcessor 接口 ,覆写process和stateEvent两个方法,process接收Protocol发送过来的报文,执行自定义操作,stateEvent相当于事件监听器,当会话状态发生变更时可执行对应操作。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smartboot.socket.MessageProcessor;
import org.smartboot.socket.StateMachineEnum;
import org.smartboot.socket.transport.AioSession;
import org.smartboot.socket.transport.WriteBuffer;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Component
public class ProtocolMessageProcessor implements MessageProcessor<String> {
private static final Logger logger = LoggerFactory.getLogger(ProtocolMessageProcessor.class);
@Override
public void process(AioSession session, String data) {
try {
TcpServer.recvDataHashMap.put(data, session);
// 消息处理逻辑
byte[] req = TcpConstant.CONNECTED.getBytes(StandardCharsets.UTF_8);
WriteBuffer outPutStream = session.writeBuffer();
outPutStream.write(req);
outPutStream.flush();
} catch (Exception e) {
logger.error("数据处理异常!", e);
}
}
/**
* 状态机事件,当枚举事件发生时由框架触发该方法
*
* @param session 本次触发状态机的AioSession对象
* @param stateMachineEnum 状态枚举
* @param throwable 异常对象,如果存在的话
* @see StateMachineEnum
*/
@Override
public void stateEvent(AioSession session, StateMachineEnum stateMachineEnum, Throwable throwable) {
if (stateMachineEnum == StateMachineEnum.DECODE_EXCEPTION
|| stateMachineEnum == StateMachineEnum.PROCESS_EXCEPTION) {
logger.error(TcpConstant.DATA_PROCESS_ERROR, throwable);
}
//客户端断开连接
if (StateMachineEnum.SESSION_CLOSED.equals(stateMachineEnum)) {
//根据session监听客户端状态
List<String> strings= getKeysByValue(TcpServer.recvDataHashMap,session);
//从Map中移除该客户端所有消息,TODO 可以向Controller反馈状态变化,进而反馈给前端。
for (String str: strings) {
TcpServer.recvDataHashMap.remove(str,session);
}
}
}
/**
*根据value获取key
*/
public static <K,V> List<K> getKeysByValue(ConcurrentHashMap<K, V> recvDataHashMap, V value){
return recvDataHashMap.entrySet()
.stream()
.filter(entry->entry.getValue().equals(value))
.map(Map.Entry::getKey).collect(Collectors.toList());
}
}
搭建TCP服务端
import com.jyjt.common.core.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smartboot.socket.transport.AioQuickServer;
import org.smartboot.socket.transport.AioSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
@Component
@RequiredArgsConstructor
public class TcpServer {
@Value("${smartsocket.port}")
private int smartSocketPort;
private static final Logger logger = LoggerFactory.getLogger(TcpServer.class);
public static ConcurrentHashMap<String, AioSession> recvDataHashMap = new ConcurrentHashMap<>();
public final ProtocolMessageProcessor processorTest;
public final TcpProtocol tcpProtocol;
public void start() {
try {
AioQuickServer server = new AioQuickServer(smartSocketPort, tcpProtocol, processorTest);
server.start();
logger.info(TcpConstant.STARTED);
} catch (IOException e) {
logger.error(TcpConstant.STARTED_FAILED, e);
}
}
/**
* 推送数据
*
* @param clientId 局域网ID
* @param data 自定义返回参数
*/
public void pushDataToClient(String clientId, String data) {
try {
//从Map获取Session对象,准备向客户端推送数据
AioSession as = recvDataHashMap.get(clientId);
//判断是否存在该客户端
if (StringUtils.isNull(as)) {
logger.info(TcpConstant.THERE_IS_NOT_CLIENT);
return;
}
//判断data是否为空,因为传输有前后缀,逻辑上data不能为“”
if (StringUtils.isBlank(data)) {
logger.error(TcpConstant.UNEXPECT_DATA);
return;
}
//将data转换为字节数组
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
OutputStream wb = as.writeBuffer();
wb.write(bytes);
wb.flush();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
/**
* 推送数据
*
* @param clientId 局域网标识
*/
public void pushDataToClient(String clientId) {
String data = TcpConstant.RELOAD;
try {
AioSession as = recvDataHashMap.get(clientId);
if (StringUtils.isNull(as)) {
logger.info(TcpConstant.THERE_IS_NOT_CLIENT);
return;
}
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
OutputStream wb = as.writeBuffer();
wb.write(bytes);
wb.flush();
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
常量类
public class TcpConstant {
/**
* 返回至Tcp客户端的报文
*/
public static final String RELOAD = "#reload&";
/**
* 连接成功返回至Tcp客户端的报文
*/
public static final String CONNECTED = "#connected&";
/**
* 字符串报文前缀
*/
public static final String PREFIX = "#";
/**
* 字符串报文后缀
*/
public static final String SUFFIX = "&";
//日志
public static final String STARTED = "启动成功";
public static final String STARTED_FAILED = "启动失败";
public static final String THERE_IS_NOT_CLIENT = "客户端没有启动";
public static final String UNEXPECT_DATA = "字符串不符合要求";
public static final String DATA_PROCESS_ERROR ="数据处理异常!";
public static final String DECODING_EXCEPTION = "协议解码异常!";
}
SpringBoot启动
import com.jyjt.collect.smartSocket.util.TcpServer;
import com.jyjt.common.security.annotation.EnableCustomConfig;
import com.jyjt.common.security.annotation.EnableRyFeignClients;
import com.jyjt.common.swagger.annotation.EnableCustomSwagger2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@EnableCustomConfig
@EnableCustomSwagger2
@EnableRyFeignClients
@SpringBootApplication
public class JyJtCollectApplication {
public static void main(String[] args) {
// SpringApplication.run(JyJtCollectApplication.class,args);
ApplicationContext applicationContext= SpringApplication.run(JyJtCollectApplication.class, args);
applicationContext.getBean(TcpServer.class).start();
System.out.println("(♥◠‿◠)ノ゙ 采集模块启动成功 ლ(´ڡ`ლ)゙ \n");
}
}
进阶介绍
状态机
在smart-socket中将状态机视为一种事件类型。
当事件被触发时会及时回调MessageProcessor的stateEvent方法,所有开发人员可以自由定制自己关心的状态机处理逻辑。比如:新建连接(NEW_SESSION),断开连接(SESSION_CLOSED)。
状态码 | 描述 |
---|---|
NEW_SESSION | 连接已建立并实例化Session对象 |
REJECT_ACCEPT | 拒绝接受连接,仅Server端有效。黑名单插件生效后会触发该状态机。 |
ACCEPT_EXCEPTION | 接受连接异常,仅Server端有效。 |
PROCESS_EXCEPTION | 业务处理异常。执行MessageProcessor.process(AioSession, Object)期间发生未捕获的异常。该状态机仅作为业务逻辑不够健壮的提示,不会影响网络连接。 |
DECODE_EXCEPTION | 协议解码异常。执行Protocol.decode(ByteBuffer, AioSession)期间发生未捕获的异常。该状态机触发后会自动关闭连接。 |
INPUT_SHUTDOWN | 读通道已被关闭,通会话正在关闭中。常由以下几种情况会触发该状态:对端主动关闭write通道,致使本端满足了EOF条件当前AioSession处理完读操作后检测到自身正处于SESSION_CLOSING状态 |
INPUT_EXCEPTION | 读操作异常。在底层服务执行read操作期间因发生异常情况触发了java.nio.channels.CompletionHandler.failed(Throwable, Object)。 |
OUTPUT_EXCEPTION | 写操作异常。在底层服务执行write操作期间因发生异常情况触发了java.nio.channels.CompletionHandler.failed(Throwable, Object) |
SESSION_CLOSING | 会话正在关闭中。执行了AioSession.close(false)方法,并且当前还存在待输出的数据。 一般无需关注该状态机。 |
SESSION_CLOSED | 会话关闭成功。 |
插件
本部分功能是由NetMonitor接口功能演化而来的,NetMonitor 定义的方法主要是针对 I/O 操作相关的切面,当对应的事件即将或已经被执行时,会触发 smart-socket 的回调动作。具体接口设计如下:
shouldAccept——入参:获取当前建立连接的通道对象;出参:非null:接收该连接,null:拒绝连接。
beforeRead——入参:即将执行read操作的AioSession;出参:无。
afterRead——入参:完成read操作的AioSession,以及本次读到的字节数;出参:无。
beforeWrite——入参:即将执行write操作的AioSession;出参:无。
afterWrite——入参:完成write操作的AioSession,以及本次输出的字节数;出参:无。
插件是在NetMonitor的基础上增加了两个方法
preProcess——入参:即将处理消息的AioSession,以及待处理的对象;出参:boolean ,true:接收该消息,false丢弃该消息。
stateEvent——状态机
心跳插件
当出现非正常的网络断连,通信双方可能无法感知到该情况(譬如拔网线、服务器断电)。 此时需要通过业务层面的心跳消息感知异常并释放网络资源。
构造参数
heartRate:心跳频率,每隔 hearRate 时长发送一次心跳消息。
timeout:超时时间,超过 timeout 时长没收到任何消息则触发超时回调timeoutCallback。
timeUnit:heartRate 和 timeout 参数的时间单位。
timeoutCallback:超时回调方法,默认断开网络连接,支持自定义实现
接口
sendHeartRequest:发送心跳消息,当空闲时长超过 heartRate 时触发。
isHeartMessage:接收到的消息是否为心跳消息。若为心跳消息,将不会进入 MessageProcessor#process方法。
Demo
processor.addPlugin(new HeartPlugin<String>(5, 7, TimeUnit.SECONDS) {
@Override
public void sendHeartRequest(AioSession session) throws IOException {
//定时执行
WriteBuffer writeBuffer = session.writeBuffer();
byte[] content = "heart message".getBytes();
writeBuffer.writeInt(content.length);
writeBuffer.write(content);
}
@Override
public boolean isHeartMessage(AioSession session, String msg) {
//对Protocol处理过的报文进行判定,若为心跳信息就不进入业务逻辑处理部分
return "heart message".equals(msg);
}
});
闲置超时插件
IdleStatePlugin 插件是对心跳插件 HeartPlugin 的补充,因为某些场景下通信双方并未涉及心跳消息。
当 TCP 连接在指定时长内无数据收发行为时,视为通信状态异常,并断开TCP连接。
构造参数
idleTimeout:空闲超时时长。超过此时长无通信数据,将断开连接。
readMonitor:是否对读通道作空闲超时监听。
writeMonitor:是否对写通道作空闲超时监听。
若 readMonitor 和 writeMonitor 同时为 false,将触发异常。
processor.addPlugin(new IdleStatePlugin<>(5000));
黑名单插件
接口
addRule:添加黑名单规则。
removeRule:移除黑名单规则。
Demo
BlackListPlugin ipBlackListPlugin = new BlackListPlugin();
ipBlackListPlugin.addRule(address -> {
String ip = address.getAddress().getHostAddress();
return !"127.0.0.1".equals(ip);
});
processor.addPlugin(ipBlackListPlugin);
加密通道插件
准备SSL证书
keytool -genkey -validity 36000 -alias www.smartboot.org -keyalg RSA -keystore server.keystore
Demo
服务端
ServerSSLContextFactory serverFactory = new ServerSSLContextFactory(SslDemo.class.getClassLoader().getResourceAsStream("server.keystore"), "123456", "123456");
SslPlugin sslServerPlugin = new SslPlugin(serverFactory, ClientAuth.OPTIONAL);
serverProcessor.addPlugin(sslServerPlugin);
客户端
ClientSSLContextFactory clientFactory=new ClientSSLContextFactory();
SslPlugin sslPlugin = new SslPlugin(clientFactory);
clientProcessor.addPlugin(sslPlugin);
流量防控插件
当面临恶意的流量攻击,亦或者正常业务的流量洪峰时,适当的流量防控有助于服务的整体稳定性。
构造参数
readRateLimiter:单个连接每秒支持的读取字节数。
writeRateLimiter:单个连接每秒支持的输出字节数。
Demo
processor.addPlugin(new RateLimiterPlugin<>(512, 1024));
码流监控插件
构造参数
inputStreamConsumer:输入码流处理器
outputStreamConsumer:输出码流处理器
processor.addPlugin(new StreamMonitorPlugin<>());