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

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

简介:服务端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>
           
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个使用Netty实现一个客户端对三个服务端Java代码示例: ```java import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; public class MultiServerClient { private List<Channel> channels = new ArrayList<>(); public void connect(String[] serverList) { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ClientHandler()); } }); for (String server : serverList) { String[] addr = server.split(":"); String host = addr[0]; int port = Integer.parseInt(addr[1]); ChannelFuture f = b.connect(new InetSocketAddress(host, port)).sync(); channels.add(f.channel()); } } catch (Exception e) { e.printStackTrace(); } } public void send(String message) { if (!channels.isEmpty()) { Channel channel = channels.get(0); // 这里选择第一个 Channel 实例发送数据 channel.writeAndFlush(message); } } public void close() { for (Channel channel : channels) { channel.close(); } channels.clear(); } private static class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 处理服务端响应的数据 } } public static void main(String[] args) { MultiServerClient client = new MultiServerClient(); String[] serverList = {"localhost:8080", "localhost:8081", "localhost:8082"}; client.connect(serverList); client.send("Hello, world!"); client.close(); } } ``` 在`connect()`方法中,循环遍历服务端列表,对于每个服务端,创建一个`InetSocketAddress`实例,然后调用`Bootstrap`的`connect()`方法连接服务端,并将返回的`ChannelFuture`实例中的`Channel`保存到`channels`列表中。 在`send()`方法中,从`channels`列表中选择一个`Channel`实例,通过`writeAndFlush()`方法发送数据。 在`main()`方法中,创建一个`MultiServerClient`实例,传入服务端列表,连接服务端,发送数据,然后关闭连接。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值