即时通讯:服务端SpringBoot+Netty-Socket.io,客户端Socket.io.js+Java版Socket.io-client

本文介绍了如何使用SpringBoot结合Netty-Socket.io实现即时通讯服务,客户端可以使用Socket.io.js或Java版Socket.io-client。文中探讨了消息推送的实时性需求,讨论了同步和异步处理的场景,以及分布式任务调度、数据库处理、消息队列等技术。此外,还提到了Redis和Netty在即时通讯中的作用,以及在高并发场景下的优化思考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介:服务端SpringBoot+Netty-Socket.io,客户端Socket.io.js或Java版Socket.io-client

基于socket.io:

引入:写在前面的话

1、当你使用IM通讯技术时,还在束缚于第三方SDK?

2、当你还在处于付费享用IM聊天,提供的服务,不妨咱自己来搭建聊天室,点对点聊天,群组聊天,实时推送等服务?

同时,这里也mark一下部分核心技术生态:

分布式任务调度框架:

分布式文件系统:

链路追踪:

数据同步:

Java生态圈:

 

场景描述:通过集成xxl-job分布式调度平台,设置cron时间表达式,开启job任务轮询方式,定时调度RestFul API完成相关业务逻辑。这里只举一例,比如消息的推送。

当然,若是单体应用的话,不走分布式,可依具体场景而定,结合springboot实现定时任务即可满足定时定点推送

/**
 * @Author: X.D.Yang
 * @Date: 2018/7/15 15:35
 * @Description:
 */
@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        //System.setProperty("es.set.netty.runtime.available.processors", "false");
        SpringApplication.run(Application.class, args);
    }
}
/**
 * @Author: X.D.Yang
 * @Date: 2018/7/15 15:40
 * @Description:
 */
@Component
public class QuartzService {

    private static final Logger logger = LoggerFactory.getLogger(QuartzService.class);

   //每天晚上十二点执行
    @Scheduled(cron = "0 0 0 * * ?")
    public void monitor() {
        //相关业务逻辑-todo
        logger.info("now time: {}", DateUtils.dateToStr(new Date()));
}

引发思考:虽然能够实现定时定点推送,但在一些实时性要求非常高的场景显然是需要改进的,这里也mark一下对消息一些处理的场景。

同步:发出调用时,没有得到结果前则该调用不会返回,即调用者在主动等待这个调用的结果。

RestFul API>调用xxxx中心接口推送>实时性较高,业务逻辑简单,易维护。

异步:调用发出后,调用直接返回了,没有返回结果,被调用者通过状态通知等通知调用者,或者通过回调函数处理回调结果。

xxljob+mq>通知xxxx中心推送>程序写入性能较高,在一定程度上可降低业务强耦合,但可能存在延时问题-无法保证轮询的频率和消费的频率完全一致-MQ的消费可能由于网络或其它原因导致用户写入的数据不一定是可以马上看到的。当然,异步还可以通过多线程去处理,可依具体场景而定,过多的线程调度及上下文切换是需要消耗大量CPU资源的。

需要思考的是若有这样一个场景,A 系统收到一个请求,需要在自己的系统中去操作数据库,同时还需要在 BC 俩个系统中操作数据库,A写库 100ms,BC分别写库要 350ms、450ms。最终请求总延时是 100 + 350 + 450  = 900ms,接近 1s,特别是在To C的产品中,用户对体验是最关心的,还有一些网络的延迟等,用户只会觉得你们做的系统太过lj慢透了,一般来说每个请求在 200 ms 以内完成,对用户几乎是无感知的,其实从感觉上说就像我们点个按钮,几ms以后就直接返回了,哇!那第一感觉这网站做得真好,贼快!

削峰填谷:我们都了解一般的MySQL,每秒 2k 个请求扛得差不多了,当每秒并发请求数量突然会暴增到 4k+ 条。但系统若又是直接基于 MySQL 的,没有Redis缓存,大量的请求涌入到 MySQL(说明:当然就算加了redis缓存也会有缓存穿透-雪崩,没有统一解决一切场景的方案,只有对业务场景更合不合适-就像是一个“升级打怪”的过程),或许就直接把 MySQL 给怼死了,导致服务不可用,在高并发高可用高性能DT-AI时代,这是不能够接受的。但高峰期一过,就成了低峰期,每秒请求数量可能也就几十个请求,对整个系统来说几乎没有任何压力,使用 MQ每秒 4k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,A 系统则从MQ 中每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量,这样就算高峰期,A 系统也绝对不会至于挂掉不可用。

需要思考的是MQ 每秒钟 4k 个请求进来,就 2k 个请求出去,结果就导致在高峰期时间段,可能有几十万甚至几百万的请求积压在 MQ 中,当然这个短暂的高峰期积压是 ok 的,在高峰期过了之后,每秒钟就几十个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。总的来说上游限速发行,下游限速执行。这里也对mq-kafka生产者跟消费者作下说明:

kafka:
    consumer:
        auto:
            commit:
                interval: 100
            offset:
            #该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
            #latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
            #earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
                reset: latest
        #并发数
        concurrency: 3
        #消息签收机制:手动签收
        enable:
            auto:
              commit: false
        #最大拉取数
        max:
          poll:
            records: 100
        #消费组
        group:
            id: consumer-group
        servers: ip:9092
        session:
            timeout: 6000
        zookeeper:
            connect: ip:2181
    producer:
        batch:
            size: 65536
        buffer:
            memory: 52428800
        max:
            request:
                size: 31457280
        servers: ip:9092

i、生产者>acks=0:生产者在成功写入消息之前不会等待任何来自服务器的响应;acks=1:只要集群的首领节点收到消息,生产者收到一个服务器成功响应;当acks=-1的时候分区leader必须等待消息被成功写入所有ISR副本(同步副本)才认为producer请求成功,提供最高的消息持久性保证,理论上吞吐率最差,生产者也可设置批量发送数据-batch-size 65536;
ii、消费者>批量消费@KafkaListener支持,设置batchListener为true:

@Bean
public KafkaListenerContainerFactory<?> batchFactory(ConsumerFactory consumerFactory){
    ConcurrentKafkaListenerContainerFactory<Integer,String> factory =
    new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory);
    factory.setConcurrency(10);
    factory.getContainerProperties().setPollTimeout(1400);
    //设置为批量消费,每个批次数量在Kafka配置参数中设置
    factory.setBatchListener(true);
    //设置手动提交
    ackModefactory.getContainerProperties().setAckMode(
    ContainerProperties.AckMode.MANUAL_IMMEDIATE);
  return factory; 
}

其中containerFactory = “batchFactory”指定为批量消费,

    //批量消费
    @KafkaListener(topics = {"yxd179"},containerFactory="batchFactory")
    public void consumerBatch(List<ConsumerRecord<?, ?>> records, Acknowledgment ack){
        log.info("接收消息数量:{}",record.size());
       //手动提交-业务逻辑成功处理后提交offset>消息重复消费
      ack.acknowledge();
}

当然,批量消费也可以结合springboot屏蔽kafka自动配置,引入我们自定义的配置:

@SpringBootApplication(scanBasePackages ={"com.yxd"},exclude = {KafkaAutoConfiguration.class})

其中新增Kafka配置项等不作具体阐述,多线程并发消费场景-不能保证原始分区消息的顺序,接入模拟大数据量批处理Test:

@Test
    public void testSendKafka() throws InterruptedException {
 
        int clientTotal = 10000;
        int threadTotal = 200;
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Semaphore信号量-流控手段-可对特定资源的允许同时访问的操作数量进行控制,例如:池化技术(连接池)中的并发数,有界阻塞容器的容量等
        final Semaphore semaphore = new Semaphore(threadTotal);
        //主线程等待多个工作线程结束,主线程调用-初始化计数
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    //获取到许可,才可继续执行任务,若获取失败,则进入阻塞
                    semaphore.acquire();
                    String log = "TEST TEST TEST TEST TEST TEST TEST TEST";
                    kafkaTemplate.send("yxd179", log);
                    /*ListenableFuture<SendResult<String, String>> future = 
                    kafkaTemplate.send(TOPIC, JSON.toJSONString(log));
                    //监听回调
                    future.addCallback(new ListenableFutureCallback<SendResult<String,  String>>() {
                       @Override
                       public void onFailure(Throwable throwable) {
                          log.info("## Send message fail insert...");
                          log.error(throwable.getMessage());
                       }

                       @Override
                       public void onSuccess(SendResult<String, String> result) {
                          log.info("## Send message success ...");
                       }
                    });*/
                    //处理完成之后,release释放许可,当然在一个线程中获得的许可可在另一个线程中释放
                    semaphore.release();
                } catch (Exception e) {
                    log.error("e >>> ", e);
                }
                //工作线程调用-计数减一
                countDownLatch.countDown();
            });
        }
        //主线程调用-阻塞,直到等待计数为0解除阻塞,各线程之间不再互相影响,可以继续做自己的事情了,不再执行下一个目标工作
        countDownLatch.await();
        executorService.shutdown();
}

Ok,Now:

我们回到主题-即时通讯-聊天:开启长连接方式,实现即时通讯-在线实时聊天-实时推送>

首先,在pom.xml中引入依赖,

        <!-- netty-socketio 其版本:1.7.12 -->
        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>${netty-socketio.version}</version>
        </dependency>

        <!-- socket.io-client 其版本:1.0.0,也可通过github源码编译更高版本jar -->
        <dependency>
            <groupId>io.socket</groupId>
            <artifactId>socket.io-client</artifactId>
           
### 若依框架前后端分离时集成 `socket.io-client` 的方法 #### 背景介绍 若依(RuoYi)是一款基于 Spring Boot 和 Vue.js 开发的企业级管理后台系统,支持前后端分离架构。为了实现前端与后端之间的实时通信功能,可以通过引入 WebSocket 技术来完成数据的双向传输。而 `socket.io-client` 是一种流行的 WebSocket 实现库,能够简化客户端服务端之间复杂的连接逻辑。 以下是关于如何在若依框架中集成并使用 `socket.io-client` 的具体说明: --- #### 后端配置 (Spring Boot) 1. **依赖引入** 在项目的 Maven 或 Gradle 中添加 `spring-boot-starter-websocket` 及 `netty-socketio` 作为基础依赖项。 ```xml <!-- pom.xml --> <dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.20</version> </dependency> ``` 2. **Socket.IO Server 初始化** 创建一个自定义类用于初始化 Socket.IO server 并绑定到指定端口上运行。 ```java @Configuration public class SocketIOConfig { private static final Logger logger = LoggerFactory.getLogger(SocketIOConfig.class); @Bean(initMethod="start", destroyMethod="stop") public SocketIOServer socketIOServer() { Configuration config = new Configuration(); config.setHostname("localhost"); config.setPort(8081); // 设置监听端口号 SocketIOServer server = new SocketIOServer(config); server.addEventListener("chat message", String.class, args -> { String data = (String)args.getSingleArg(); server.getBroadcastOperations().sendEvent("news", data); }); return server; } } ``` 3. **跨域设置** 如果前後端分離部署於不同域名下,则需處理 CORS 問題,在服務器啟動時進行相關設置。 ```java @Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; HttpServletRequest request= (HttpServletRequest)req; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With"); if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(req, res); } } @Override public void init(FilterConfig filterConfig) {} @Override public void destroy() {} } ``` --- #### 前端配置 (Vue.js) 1. **安装依赖包** 安裝 `socket.io-client` 到項目當中以便於前端能夠與後端建立連接。 ```bash npm install --save socket.io-client ``` 2. **创建全局实例对象** 将 `socket.io-client` 连接封装为单例模式供整个应用调用。 ```javascript import { io } from 'socket.io-client'; const SOCKET_SERVER_URL = process.env.VUE_APP_SOCKET_IO_SERVER || 'http://localhost:8081'; let socketInstance = null; export default function getSocketIoClient() { if (!socketInstance) { socketInstance = io(SOCKET_SERVER_URL, { transports: ['websocket'], // 强制只使用 websocket 协议 reconnectionAttempts: Infinity, timeout: 10000, autoConnect: true }); socketInstance.on('connect', () => console.log('[SOCKET] Connected')); socketInstance.on('disconnect', reason => console.warn(`[SOCKET] Disconnected due to ${reason}`)); socketInstance.on('error', error => console.error(`[SOCKET ERROR]:`, error)); window.addEventListener('beforeunload', () => { if(socketInstance && socketInstance.connected){ socketInstance.disconnect(); } }) } return socketInstance; } ``` 3. **组件内使用示例** 下面展示了一个简单的聊天室场景下的消息发送和接收处理流程。 ```vue <template> <div id="app"> <input v-model="messageText"/> <button @click="sendMessage">Send Message</button> <ul> <li v-for="(msg,index) in messages" :key="index">{{ msg }}</li> </ul> </div> </template> <script> import getSocketIoClient from '@/utils/socket-io-client'; export default { name: 'App', data(){ return{ messageText:'', messages:[] }; }, mounted(){ this.initSocketListener(); }, methods:{ sendMessage(){ const textValue=this.messageText.trim(); if(textValue.length===0)return ; try{ const s=getSocketIoClient(); s.emit('chat message',textValue,[^2]); this.messages.push(`You said:${textValue}`); this.messageText=''; }catch(e){ alert('Failed to send message'); } }, initSocketListener(){ const s=getSocketIoClient(); s.on('news',(data)=>{ this.messages.push(data); },); } } } </script> ``` --- #### iOS Native App 集成 (Objective-C/C++) 对于移动端开发而言,如果需要通过原生代码接入 WebRTC 功能并与服务端保持同步状态更新的话,可以考虑采用 C++ 版本的 `socket.io-client-cpp` 库来进行交互操作。 ##### 步骤概述: 1. 添加头文件路径至 XCode 工程; 2. 导入必要的模块声明; ```objc #import <socket.io-client-cpp/sio_client.h> ``` 3. 编写实际业务逻辑代码片段如下所示: ```cpp #include <iostream> #include "../include/sio_client.h" using namespace std; using namespace sio; class my_listener : public client::listener { public: virtual void on_connect() override { cout << "[C++] Connected!" << endl; } virtual void on_event(const string& event_name, shared_ptr<event_args> ev, bool is_ack, int ack_id) override { cout << "[C++] Received Event:" << event_name << ", Data:" << ev->get_string() << endl; if(is_ack){ vector<string> resp={"ACK"}; cli->socket()->acknowledge(ack_id,&resp); } } virtual void on_disconnect() override { cout << "[C++] Disconnected." << endl; } }; int main(int argc,char* argv[]) { listener* l=new my_listener(); client cli(l); cli.connect("ws://localhost:8081",[l](const connection_response& r)->void{ (*l).on_connect(); }); while(true){sleep(1);} } ``` --- ### 总结 上述内容涵盖了从环境搭建、基本概念理解以及实践案例等多个方面介绍了如何利用 `socket.io-client` 来增强若依系统的实时通讯能力。希望这些资料能帮助开发者快速掌握相关技能!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值