SpringBoot+Smart Socket应用总结

框架特点

高性能、高并发、低延迟、绿色节能。
代码量极少,可读性强。核心代码不到 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<>());
  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MELENCOLIA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值