个人笔记篇-SpringBoot集成Socket

这篇具有很好参考价值的文章主要介绍了个人笔记篇-SpringBoot集成Socket。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

在我的另一篇文章中,有简单介绍过Socket的相关概念链接:SpringBoot简单集成WebSocket

初步了解后,本次再进行一个深入通俗的理解。

Socket作为一种通信机制,通常也被称为"套接字"。

它类似于人们之间的"打电话行为"。我们将每个人的电话号作为独立端口。两个人打电话之前则首先需要其中一方知晓另一方的"端口"。然后申请向对方进行拨号呼叫(请求连接)。

此时被连接方如果正好空闲,接起电话,则双方正式达成连接。开始正式通讯.只要有其中一方挂断电话,则关闭Socket连接。

Socket的两种类型

1.流式Socket(Stream).一种面向连接的Socket,针对面向连接的TCP服务应用,安全但效率低。属于业内较为常用的一种方式

2.数据报式Socket(DataGram).一种无连接的Socket.不安全,效率高.


2023-04-17 重构写法

1.优化了Socket的启动方式,不论是Jar包启动还是War部署都不用再额外配置。

2.优化了Socket的线程代码,更简洁的代码逻辑。

3.优化了Socket的连接保持,不再使用线程Sleep的方式,提高了响应速度。

4.去除了客户端监听机制,此类代码在实际业务场景中意义不大。

5.重构了Socket回写给客户端消息的方式,使用PrintWrite进行回写.


正文

下面开始在SpringBoot中实战整合Socket

在JDK1.8中.官方整合了Socket服务至java.net包中。因此不需要引入其它依赖。

1.先在配置文件中配置Socket监听的端口

#Socket配置
socket:
  port: 8082

2.配置Socket连接类

        这一步类似配置一个Controller,主要用于接受客户端的连接请求。

@Slf4j
@Component
public class SocketServers{
    
    //注入被开放的端口
    @Value("${socket.port}")
    private Integer port;  

    //这个是业务处理的接口
    @Autowired
    private IBusinessSocketService socketService;

    @PostConstruct
    public void socketStart(){
        //直接另起一个线程挂起Socket服务
        new Thread(this::socketServer).start();
    }
    
    private void socketServer() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            log.info("socket端口在:{}中开启并持续监听=====>", port);
            while (Boolean.TRUE) {
                Socket clientSocket = serverSocket.accept();
                //设置流读取的超时时间,这里设置为10s。超时后自动断开连接
                clientSocket.setSoTimeout(10000);
                //是否与客户端保持持续连接,这行代码在本示例中,并没有作用,因为后面的逻辑处理完成后,会自动断开连接.
                clientSocket.setKeepAlive(Boolean.TRUE);
                log.info("发现客户端连接Socket:{}:{}===========>", clientSocket.getInetAddress().getHostAddress(),
                        clientSocket.getPort());
                //这里通过线程池启动ClientHandler方法中的run方法.
                executorService.execute(new ClientHandler(clientSocket, socketService));
            }
        } catch (Exception e) {
            log.error("Socket服务启动异常!", e);
        }
    }
}

3.配置自定义客户端类

@Slf4j
public class ClientHandler implements Runnable {

    private final Socket clientSocket;

    private final IBusinessSocketService socketService;

    public ClientHandler(Socket clientSocket, IBusinessSocketService socketService) {
        this.clientSocket = clientSocket;
        this.socketService = socketService;
    }

    @Override
    @SneakyThrows
    public void run() {
        //SnowFlakeUtil 雪花ID生成工具类,后面会统一给出
        String logId = SnowFlakeUtil.getId();
        String hostIp = clientSocket.getInetAddress().getHostAddress();
        String port = String.valueOf(clientSocket.getPort());
        try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), Boolean.TRUE)) {
        //这里的StringUtil是自己写的工具类,后面会统一给出
            String requestInfo = StringUtil.readIoStreamToString(clientSocket.getInputStream());
            if (StringUtil.isNotEmpty(requestInfo)) {
                log.info("监听到客户端消息:{},监听日志ID为:{}", requestInfo, logId);
                socketService.executeBusinessCode(requestInfo, logId, out);
                clientSocket.shutdownOutput();
                TimeUnit.SECONDS.sleep(1L);
            }
        } catch (IOException e) {
            log.error("与客户端:[{}:{}]通信异常!错误信息:{}", hostIp, port, e.getMessage());
        } finally {
            log.info("客户端:[{}:{}]Socket连接已关闭,日志ID为:{}========>", hostIp, port, logId);
        }
    }
}

4.定义业务接口

public interface IBusinessSocketService {

    /**
     * 从Socket中接受消息并处理
     *
     * @param requestInfo 请求报文
     * @param logId       日志ID
     * @param writer      回写给客户端消息的回写类(Socket自带)
     */
    void executeBusinessCode(String requestInfo, String logId, PrintWriter writer);
}

5.定义业务实现类

@Slf4j
@Service
public class BusinessSocketServiceImpl implements IBusinessSocketService {

    @Override
    public void executeBusinessCode(String requestInfo, String logId, PrintWriter writer) {
        String responseMsg;
        boolean isSuccess = Boolean.TRUE;
        try {
            if (StringUtils.isEmpty(requestInfo)) {
                return;
            }
            //执行你的业务操作
            responseMsg = "回填给客户端的信息,可以是任何格式的对象";
        } catch (Exception e) {
            isSuccess = Boolean.FALSE;
            e.printStackTrace();
            responseMsg = "回填给客户端的信息(业务处理错误的情况下)";
        }
        try {
            //将响应报文通过PrintWriter回写给客户端
            writer.println(responseMsg);
        } catch (Exception e) {
            log.error("Socket客户端数据返回异常!当前日志ID:[{}],异常信息:{}", logId, e);
        }
    }
}

6.相关工具类

    6.1 SnowFlakeUtil

package com.util;

/**************************************************
 * copyright (c) 2021
 * created by yusen.zhang
 * date:       2021-9-18
 * description: 雪花算法工具类
 *
 **************************************************/
public class SnowFlakeUtil {

    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long twepoch = 1288834974657L;                              //  Thu, 04 Nov 2010 01:42:54 GMT 标记时间 用来计算偏移量,距离当前时间不同,得到的数据的位数也不同
    private long workerIdBits = 5L;                                     //  物理节点ID长度
    private long datacenterIdBits = 5L;                                 //  数据中心ID长度
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);             //  最大支持机器节点数0~31,一共32个
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);     //  最大支持数据中心节点数0~31,一共32个
    private long sequenceBits = 12L;                                    //  序列号12位, 4095,同毫秒内生成不同id的最大个数
    private long workerIdShift = sequenceBits;                          //  机器节点左移12位
    private long datacenterIdShift = sequenceBits + workerIdBits;       //  数据中心节点左移17位
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; //  时间毫秒数左移22位
    private long sequenceMask = -1L ^ (-1L << sequenceBits);                          // 用于和当前时间戳做比较,以获取最新时间
    private long lastTimestamp = -1L;

    //成员类,SnowFlakeUtil的实例对象的保存域
    private static class IdGenHolder {
        private static final SnowFlakeUtil instance = new SnowFlakeUtil();
    }

    //外部调用获取SnowFlakeUtil的实例对象,确保不可变
    public static SnowFlakeUtil get() {
        return IdGenHolder.instance;
    }

    //初始化构造,无参构造有参函数,默认节点都是0
    public SnowFlakeUtil() {
        this(0L, 0L);
    }

    //设置机器节点和数据中心节点数,都是 0-31
    public SnowFlakeUtil(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    //线程安全的id生成方法
    @SuppressWarnings("all")
    public synchronized long nextId() {
        //获取当前毫秒数
        long timestamp = timeGen();
        //如果服务器时间有问题(时钟后退) 报错。
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format(
                    "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        //如果上次生成时间和当前时间相同,在同一毫秒内
        if (lastTimestamp == timestamp) {
            //sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位
            sequence = (sequence + 1) & sequenceMask;
            //判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0
            if (sequence == 0) {
                //自旋等待到下一毫秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加,每个毫秒时间内,都是从0开始计数,最大4095
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // 最后按照规则拼出ID 64位
        // 000000000000000000000000000000000000000000  00000            00000       000000000000
        //1位固定整数   time                                       datacenterId   workerId    sequence
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;
    }

    //比较当前时间和过去时间,防止时钟回退(机器问题),保证给的都是最新时间/最大时间
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    //获取当前的时间戳(毫秒)
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * 获取全局唯一编码
     */
    public static String getId() {
        long id = SnowFlakeUtil.get().nextId();
        return Long.toString(id);
    }


}

        6.2 StringUtil

package com.util;

import lombok.SneakyThrows;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

/**
 * <Description> ******
 *
 * @author yuSen.zhang
 * @version 1.0
 * @date 2023/04/17
 */
public class StringUtil {

    @SneakyThrows
    public static String readToStreamToString(InputStream is) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[2048];
        int bytesRead;
        /*
        这一步可能会卡住,因为客户端如果传输完数据以后,
        如果没有调用socket.shutdownOutput()方法,会导致服务端不知道流是否已传输完毕,
        等待我们之前设置的10S流读取时间后,连接就会被自动关掉
        如果出现这种情况,服务端可以通过其它方式判断。例如换行符或者特殊字符等,只需要在
        while条件中加一个&&判断即可.例如我这里的业务结束标记是字符: ">",那么判断逻辑如下
        若客户端调用了shutdownOutput()方法,则不需要这个判断
        */
        while (!bos.toString().contains(">") && (bytesRead = is.read(buffer)) != -1) {
            bos.write(buffer, 0, bytesRead);
        }
        return bos.toString();
    }
}

7.测试Main方法(也可以另起一个SpringBoot服务)

 public static void main(String[] args) throws Exception {
        requestInfoToSocketServer();
 }


 private static void requestInfoToSocketServer() {
        try (Socket socket = new Socket("127.0.0.1", 8082);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
            out.println("发送给服务端的消息");
            //记得调用shutdownOutput()方法,否则服务端的流读取会一直等待
            socket.shutdownOutput();
            //开始接收服务端的消息
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("接收到服务端的回复:" + in.readLine());
        } catch (Exception e) {
            System.out.println("Socket传输数据异常!" + e.getMessage());
        }
 }

        若有不完善或者运行失败的情况,可以私信我或者在下方留言哦~文章来源地址https://www.toymoban.com/news/detail-403182.html

到了这里,关于个人笔记篇-SpringBoot集成Socket的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【SpringBoot笔记37】SpringBoot基于@ServerEndpoint、@OnMessage等注解的方式集成WebSocket

    这篇文章,主要介绍SpringBoot基于@ServerEndpoint、@OnMessage等注解的方式集成WebSocket。 目录 一、基于注解集成WebSocket 1.1、WebSocket常见注解 1.2、创建WebSocket服务端

    2024年02月14日
    浏览(32)
  • 动力节点RocketMQ笔记第三章RocketMQ集成SpringBoot

    22.1.1 创建项目,完整的pom.xml 22.1.2 修改配置文件application.yml 22.1.3 我们在测试类里面测试发送消息 往powernode主题里面发送一个简单的字符串消息 运行后查看控制台 22.1.4 查看rocketMq的控制台 查看消息细节

    2024年02月04日
    浏览(46)
  • 【微信小程序】个人信息页面/我的页面

    hello,小伙伴们大家好,小编是后端开发,没有系统接触过前端学习,最近公司要求开发小程序,斗胆承接,其中有个人信息页面比较常用,现记录下来,供初学者参考,有不符合规范的地方,希望各位大佬包含,评论区指点!废话不多说,直接上代码。 样式使用了flex布局

    2024年02月11日
    浏览(78)
  • 我的个人网站——宏夏Coding上线啦

    网站地址:宏夏Coding Github地址:🔥🔥宏夏coding网站,致力于为编程学习者、互联网求职者提供最需要的内容!网站内容包括求职秘籍,葵花宝典(学习笔记),资源推荐等内容。 大家好,我是宏夏c,目前是一名软件工程专业的大三学生。🙂 回顾起大一入学的那个初夏,我

    2024年02月09日
    浏览(42)
  • RocketMQ视频笔记第三章RocketMQ集成SpringBoot(动力节点)

    本篇文章是RocketMQ视频笔记的第三章,重点介绍了如何将RocketMQ集成到Spring Boot框架中。通过学习该文章,读者能够掌握如何使用RocketMQ和Spring Boot进行消息传递和处理。

    2024年02月05日
    浏览(139)
  • 我的个人网站 —— 直接使用GPT4

    前期回顾     打造极简风格动效 —— 5 分钟轻松实现惊艳、震撼人心的视觉效果_彩色之外的博客-CSDN博客 css Loading 实战教学 https://blog.csdn.net/m0_57904695/article/details/131156011?spm=1001.2014.3001.5501 ​ 目录  ✈  线上预览: ✅  G4 WEB 效果图例 :   📢 拷贝运行试试  🔱 手机版:

    2024年02月09日
    浏览(41)
  • 什么?博客园主题比我的个人博客好看??

    最近逛博客园,发现我的园子还挺好看,但是还不够好看,所以通过我百度发现SimpleMemory主题还可以继续添加新的东西,当然这些东西不一定非得用SimpleMemory主题才行,但是搭配SimpleMemory主题是真的好看呀(比我的博客好看多了)。 访问不进去GitHub,又不想花钱使用魔法,看

    2024年02月16日
    浏览(36)
  • 微信小程序个人中心、我的界面(示例二)

    微信小程序个人中心、我的界面,自定义顶部状态栏,实现滚动隐藏显示状态信息,界面简单,代码粘贴即用。更多微信小程序界面示例,请进入我的主页哦! 1、js代码 2、wxml代码 3、wxss代码 4、json代码 更多微信小程序示例的分享,请进入我的主页查看哦。。。

    2024年02月11日
    浏览(59)
  • 微信小程序个人中心、我的界面(示例一)

    微信小程序个人中心、我的界面,使用button按钮实现界面布局,更好的将分享好友、获取头像等功能展现出来,更多示例界面,请前往我的主页哦。 1、js代码: 2、wxml代码 3、wxss代码 4、json代码

    2024年02月11日
    浏览(92)
  • 微信小程序个人中心、我的界面(示例三)

    微信小程序个人中心、我的界面,超简洁界面,代码粘贴即用。更多微信小程序界面示例,请进入我的主页哦! 1、js代码 2、wxml代码 3、wxss代码 4、json代码 更多微信小程序示例的分享,请进入我的主页查看哦。。。

    2024年01月24日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包