iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >用纯Java实现一个即时通讯系统
  • 208
分享到

用纯Java实现一个即时通讯系统

2023-06-15 12:06:09 208人浏览 泡泡鱼
摘要

这篇文章主要介绍“用纯Java实现一个即时通讯系统”,在日常操作中,相信很多人在用纯Java实现一个即时通讯系统问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用纯Java实现一个即时通讯系统”的疑惑有所帮助!

这篇文章主要介绍“用纯Java实现一个即时通讯系统”,在日常操作中,相信很多人在用纯Java实现一个即时通讯系统问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用纯Java实现一个即时通讯系统”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

 项目背景

和各位读者大致介绍下具体场景,线上的小程序中开放一些语音麦克风的房间,让用户进入房间之后可以互相通过语音聊天的方式进行互动。

用纯Java实现一个即时通讯系统

这里分享一下相关的技术设计方案。这款系统的核心点设计在于如何能让一个用户发出的语音通知到其他用户上边。语音数据在客户端同事的处理下最终变成了io数据流请求到了后端,后端只需要将这些数据流传达给各个不同的终端即可达到广播通知的效果。

单机版架构

最初期上线的时候,为了赶速度,快速试错,所以简单地采用了单机版架构去设计。结合技术栈为 SpringBootwebsocketMySQL技术。

线上一间语音房间的同时在线人数并不会特别多,大概在15-50人的区间段内,系统核心代码是通过springBoot内部的WEBSocket技术去进行数据的主动推送。

设计思路

整体的设计图比较简单,基本就是一台服务器存储WebSocket连接,如下图所示:

用纯Java实现一个即时通讯系统

用户进行WebSocket初始化连接的时候需要一个连接分配和存储的过程:

用纯Java实现一个即时通讯系统

早期的存储是存放在了服务器本地的一个Map集合中。

用纯Java实现一个即时通讯系统

当WebSocket进行连接的时候就会往内存中写入一条数据信息,当链接断开的时候,就将内存中的数据移除。然后进行语音广播的时候需要结合WebSocket内部的广播发送功能进行通知

用纯Java实现一个即时通讯系统

看似设计比较简单,但是在后期业务变得庞大的时候出现了瓶颈。因为随着参加语音活动用户的增加,越来越多的WebSocketSession对象需要被存储到内存当中,这种有状态性的存储对于单机扩容不灵活。

设计缺陷

假设原先的服务器扩容到了A,B两台机器,A用户在A机器上边建立了WebSocketSession,B用户在B机器上边建立的WebSocketSession连接。此时如果A想要和B进行对话发送,需要先查找到具体WebSocketSession存放在哪台机器上边。

当用户出现了网络异常,临时断开连接进行重连的时候,也可能会出现1所说的问题。

集群架构

设计思路

一旦出现需要发送语音通知的时候,发送一条广播的MQ消息,每个机器都接收到消息之后,触发自己的广播操作即可。

RocketMQ的接入系统设计里面mq采用的是广播模式,这和我们通常使用的集群模式有一定的区别。

消息队列RocketMQ版是基于发布或订阅模型的消息系统。消费者,即消息的订阅方订阅关注的Topic,以获取并消费消息。由于消费者应用一般是分布式系统,以集群方式部署,因此消息队列RocketMQ版约定以下概念:

  •  集群:使用相同Group ID的消费者属于同一个集群。同一个集群下的消费者消费逻辑必须完全一致(包括Tag的使用)。

  •  集群消费:当使用集群消费模式时,消息队列RocketMQ版认为任意一条消息只需要被集群内的任意一个消费者处理即可。

  •  广播消费:当使用广播消费模式时,消息队列RocketMQ版会将每条消息推送给集群内所有注册过的消费者,保证消息至少被每个消费者消费一次。

集群消费模式适用场景 适用于消费端集群化部署,每条消息只需要被处理一次的场景。此外,由于消费进度在服务端维护,可靠性更高。具体消费示例如下图所示。

用纯Java实现一个即时通讯系统

注意事项

  •  集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。

  •  集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上。

广播消费模式适用场景 适用于消费端集群化部署,每条消息需要被集群下的每个消费者处理的场景。具体消费示例如下图所示。

用纯Java实现一个即时通讯系统

注意事项

  •  广播消费模式下不支持顺序消息。

  •  广播消费模式下不支持重置消费位点。

  •  每条消息都需要被相同订阅逻辑的多台机器处理。

  •  消费进度在客户端维护,出现重复消费的概率稍大于集群模式。

  •  广播模式下,消息队列RocketMQ版保证每条消息至少被每台客户端消费一次,但是并不会重投消费失败的消息,因此业务方需要关注消费失败的情况。

  •  广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。

  •  广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。

  •  广播模式下服务端不维护消费进度,所以消息队列RocketMQ版控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

这里面的应用场景需要对集群内部对每个消费者都对服务器内存中的socket连接进行session是否存在对判断,因此需要采用mq的广播模式。

关于mq部分的接入代码

Consumer模块的配置:

package org.idea.web.socket.config;  import org.springframework.boot.context.properties.ConfigurationProperties;    @ConfigurationProperties(prefix = "rocketmq.consumer")  public class MqConsumerConfig {      private boolean isOn;      private String groupName;      private String nameSrvAddr;      private String topics;      private Integer consumeThreadMin;      private Integer consumeThreadMax;      private Integer consumeMessageBatchMaxSize;          }

Producer模块的配置展示:

package org.idea.web.socket.config;  import org.springframework.boot.context.properties.ConfigurationProperties;    @ConfigurationProperties(prefix = "rocketmq.producer")  public class MqProducerConfig {      private boolean isOn;      private String groupName;      private String nameSrvAddr;      private Integer maxMessageSize;      private Integer sendMsgTimeout;      private Integer retryTimesWhenSendFailed;          }

RocketMq内部的消费端Bean配置

package org.idea.web.socket.mq;  import lombok.extern.slf4j.Slf4j;  import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;  import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;  import org.apache.rocketmq.client.exception.MQClientException;  import org.apache.rocketmq.common.consumer.ConsumeFromWhere;  import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;  import org.idea.web.socket.config.MqConsumerConfig;  import org.idea.web.socket.config.MqProducerConfig;  import org.springframework.boot.autoconfigure.AutoConfigureAfter;  import org.springframework.boot.autoconfigure.AutoConfigureBefore;  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;  import org.springframework.boot.context.properties.EnableConfigurationProperties;  import org.springframework.context.annotation.Bean;  import org.springframework.context.annotation.Configuration;  import javax.annotation.Resource;    @Configuration  @Slf4j  @EnableConfigurationProperties({MqConsumerConfig.class})  public class MqConsumerAutoConfig {      @Resource      private MqConsumerConfig mqConsumerConfig;      @Resource      //这个接口需要手动实现顺序消费的逻辑 每次获取到消息队列的第一条数据      private MessageListenerHandler messageListenerConcurrently;      @Bean      @ConditionalOnMissingBean      public DefaultMQPushConsumer defaultMQPushConsumer() {          DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();          consumer.setNamesrvAddr(mqConsumerConfig.getNameSrvAddr());          consumer.setConsumerGroup(mqConsumerConfig.getGroupName());          consumer.setConsumeThreadMin(mqConsumerConfig.getConsumeThreadMin());          consumer.setConsumeThreadMax(mqConsumerConfig.getConsumeThreadMax());          consumer.reGISterMessageListener(messageListenerConcurrently);          consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);          //消费模型是什么?          consumer.setMessageModel(MessageModel.BROADCASTING);          //默认一次拉取一条消费          consumer.setConsumeMessageBatchMaxSize(mqConsumerConfig.getConsumeMessageBatchMaxSize());          /  @Configuration  @Slf4j  @EnableConfigurationProperties({MqProducerConfig.class})  public class MqProducerAutoConfig {      @Resource      private MqProducerConfig mqProducerConfig;      @Bean      @ConditionalOnMissingBean      //意味着DefaultMQProducer的配置可以被覆盖      public DefaultMQProducer defaultMQProducer() {          DefaultMQProducer producer = new DefaultMQProducer(mqProducerConfig.getGroupName());          producer.setNamesrvAddr(mqProducerConfig.getNameSrvAddr());          //没有则自动创建topic的key  //        producer.setCreateTopicKey("AUTO_CREATE_TOPIC_KEY");          producer.setMaxMessageSize(mqProducerConfig.getMaxMessageSize());         producer.setSendMsgTimeout(mqProducerConfig.getSendMsgTimeout());          producer.setRetryTimesWhenSendFailed(mqProducerConfig.getRetryTimesWhenSendFailed());          try {              producer.start();              log.info("【 MqProducerAutoConfig 】mq producer is started!");          } catch (Exception e) {              log.error("[MqProducerAutoConfig] start fail, e is ", e);          }          return producer;      }  }

然后是对RocketMq内部发送消息事件的一层函数封装

package org.idea.web.socket.mq;  import com.alibaba.fastJSON.jsON;  import lombok.extern.slf4j.Slf4j;  import org.apache.commons.lang3.StringUtils;  import org.apache.rocketmq.client.producer.DefaultMQProducer;  import org.apache.rocketmq.client.producer.SendResult;  import org.apache.rocketmq.common.message.Message;  import org.apache.rocketmq.remoting.common.RemotingHelper;  import org.idea.web.socket.config.MqProducerConfig;  import org.idea.web.socket.dto.BroadcastMqDTO;  import org.springframework.stereotype.Component;  import javax.annotation.Resource;  import java.io.UnsupportedEncodingException;    @Component  @Slf4j  public class BroadcastMqProducer {      @Resource      private DefaultMQProducer defaultMQProducer;      @Resource      private MqProducerConfig mqProducerConfig;      private static String TOPIC = "ws-topic";      private static String TAGS = "ws-tag";      public static Integer ALL_USER_RECEIVE_TYPE = 1;      public static Integer ONE_USER_RECEIVE_TYPE = 2;            public SendResult sendWebSocketToUser(String destSessionKey,String msg) {          if (StringUtils.isEmpty(msg)) {              log.error("[sendWebSocketToUser] msg can not be null!");              return null;          }          Message message = null;          SendResult sendResult = null;          try {              BroadcastMqDTO broadcastMqDTO = new BroadcastMqDTO();              broadcastMqDTO.setEventType(ONE_USER_RECEIVE_TYPE);              broadcastMqDTO.setMessage(msg);              broadcastMqDTO.setSessionKey(destSessionKey);              message = new Message(TOPIC, TAGS, (JSON.toJSONString(broadcastMqDTO)).getBytes(RemotingHelper.DEFAULT_CHARSET));              sendResult = defaultMQProducer.send(message);          } catch (Exception e) {              log.error("[sendWebSocketBroadcastMsg] e is ", e);          }          return sendResult;      }            public SendResult sendWebSocketBroadcastMsg(String msg) {          if (StringUtils.isEmpty(msg)) {              log.error("[sendWebSocketBroadcastMsg] msg can not be null!");              return null;          }          Message message = null;          SendResult sendResult = null;          try {              BroadcastMqDTO broadcastMqDTO = new BroadcastMqDTO();              broadcastMqDTO.setEventType(ALL_USER_RECEIVE_TYPE);              broadcastMqDTO.setMessage(msg);              message = new Message(TOPIC, TAGS, (JSON.toJSONString(broadcastMqDTO)).getBytes(RemotingHelper.DEFAULT_CHARSET));              sendResult = defaultMQProducer.send(message);          } catch (Exception e) {              log.error("[sendWebSocketBroadcastMsg] e is ", e);          }          return sendResult;      }  }

对消息的订阅模块实现代码如下:

package org.idea.web.socket.mq;  import com.alibaba.fastjson.JSON;  import com.oracle.tools.packager.Log;  import lombok.extern.slf4j.Slf4j;  import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;  import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;  import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;  import org.apache.rocketmq.common.message.MessageExt;  import org.idea.web.socket.dto.BroadcastMqDTO;  import org.idea.web.socket.manager.SocketManager;  import org.springframework.messaging.simp.SimpMessagingTemplate;  import org.springframework.stereotype.Component;  import org.springframework.util.CollectionUtils;  import org.springframework.web.socket.WebSocketSession;  import javax.annotation.Resource;  import java.util.List;  import static org.idea.web.socket.mq.BroadcastMqProducer.ALL_USER_RECEIVE_TYPE;  import static org.idea.web.socket.mq.BroadcastMqProducer.ONE_USER_RECEIVE_TYPE;    @Component  @Slf4j  public class MessageListenerHandler implements MessageListenerConcurrently {      @Resource      private SocketManager socketManager;      @Resource      private SimpMessagingTemplate template;      @Override      public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {          if (CollectionUtils.isEmpty(list)) {              Log.info("receive empty msg");              return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;          }          MessageExt messageExt = list.get(0);          byte[] bytes = messageExt.getBody();          String json = new String(bytes);          BroadcastMqDTO broadcastMqDTO = JSON.parseObject(json, BroadcastMqDTO.class);          log.info("[MessageListenerHandler] broadcastMqDTO is " + broadcastMqDTO);          if (ALL_USER_RECEIVE_TYPE.equals(broadcastMqDTO.getEventType())) {              log.info("[consumeMessage] 广播发送消息:触发----》消息内容为:" + broadcastMqDTO);              template.convertAndSend("/topic/sendTopic", broadcastMqDTO);          } else if (ONE_USER_RECEIVE_TYPE.equals(broadcastMqDTO.getEventType())) {              String sessionKey = broadcastMqDTO.getSessionKey();              WebSocketSession webSocketSession = socketManager.get(sessionKey);              if (webSocketSession != null) {                  template.convertAndSendToUser(sessionKey, "/queue/sendUser", broadcastMqDTO.getMessage());                  log.info("[consumeMessage] 点对点发送消息;触发----》消息内容为:" + broadcastMqDTO);              }          }          return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;      }  }

整体设计结构如下图:

用纯Java实现一个即时通讯系统

于是按照这个结构进行了一版本的紧急开发迭代,原先的单台服务器扩展为了服务集群。

业务拓展后续产品经理提出一个需求,要求支持在同一间房内的两个用户之间发送悄悄话功能。这就需要我们进行一个点对点之间传输通讯的功能了。因此需要在mq通知到每台机器的时候加一个本地Session遍历的逻辑,如果当前机器存有用户token对应的session变量,那么就单独针对那个Session进行WebSocket的发送通知。

用纯Java实现一个即时通讯系统

设计弊端一旦某台机器出现了异常崩溃,那么就意味着这台机器上的所有语音连接可能会出现中断情况。目前这一块的问题也在考虑解决,计划是将WebSocketSession存入到分布式缓存redis中保证数据可靠存储,但是在后续尝试的时候发现WebSocketSession对象没有实现序列化接口,在存储到Redis的时候会出现异常。目前这个问题还在寻找解决思路中,不知道各位读者朋友们有什么好的思路。

遇到的问题点用户请求直接访问到了我们的内部服务器,如果在请求的中间加入一台Nginx负载均衡则需要在nginx中配置一些额外信息。

项目的源代码比较多,这里我把核心部分的代码整理了一份,感兴趣的朋友可以到我的gitee上边去下载:

https://gitee.com/IdeaHome_admin/socket-framework 

到此,关于“用纯Java实现一个即时通讯系统”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

--结束END--

本文标题: 用纯Java实现一个即时通讯系统

本文链接: https://www.lsjlt.com/news/280199.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 用纯Java实现一个即时通讯系统
    这篇文章主要介绍“用纯Java实现一个即时通讯系统”,在日常操作中,相信很多人在用纯Java实现一个即时通讯系统问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”用纯Java实现一个即时通讯系统”的疑惑有所帮助!...
    99+
    2023-06-15
  • Redis在即时通讯系统中的作用及应用
    Redis在即时通讯系统中的作用及应用随着互联网的快速发展,即时通讯成为现代社会中重要的沟通方式。而要构建一个高效、稳定的即时通讯系统,数据存储是至关重要的环节之一。Redis作为一种高性能的键值数据库,被广泛应用于即时通讯系统中,具有出色...
    99+
    2023-11-07
    应用 redis 即时通讯
  • 如何使用PHP和WebSocket打造即时通讯系统
    如何使用PHP和WebSocket打造即时通讯系统引言:随着互联网的发展,即时通讯系统在各种应用场景中的重要性愈发凸显。在过去,实现实时通信往往需要依赖轮询或者长轮询的方式,这种方式在性能和用户体验上都存在一定的局限性。所幸的是,WebSo...
    99+
    2023-12-17
    PHP websocket 即时通讯
  • PHP实现在线即时通讯系统消息的发送和接收
    随着人们生活方式的改变和网络技术的不断发展,即时通讯已经成为现代人必不可少的沟通方式之一。在这种趋势下,各种在线即时通讯系统应运而生。既然在不同地方的人可以在同一时间内交换消息,那么在线即时通讯系统是如何实现消息的传递的呢?本文将介绍通过 ...
    99+
    2023-05-24
    PHP 消息发送 在线通讯
  • 利用java怎么实现一个即时提交功能
    本篇文章给大家分享的是有关利用java怎么实现一个即时提交功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。具体内容如下package com.tian.batis;impor...
    99+
    2023-05-31
    java ava
  • 如何设计一个安全的MySQL表结构来实现即时通讯功能?
    如何设计一个安全的MySQL表结构来实现即时通讯功能?随着互联网的快速发展,即时通讯成为了人们生活中不可或缺的一部分。而为了保证即时通讯的安全性,一个合理且安全的MySQL表结构设计也变得至关重要。本文将介绍如何设计一个安全的MySQL表结...
    99+
    2023-10-31
    MySQL 安全性 通讯功能
  • 如何用Go语言开发一个简单的即时通讯应用
    如何用Go语言开发一个简单的即时通讯应用随着互联网的发展和人们对实时沟通需求的增加,即时通讯应用在我们生活中扮演着越来越重要的角色。Go语言作为一种开源的高性能编程语言,越来越受开发者们的喜爱。本文将介绍如何使用Go语言开发一个简单的即时通...
    99+
    2023-11-20
    开发 即时通讯 关键词:Go语言
  • C语言中怎么实现一个通讯录系统
    C语言中怎么实现一个通讯录系统,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。全部代码如下所示:#include <iostream>#include&...
    99+
    2023-06-20
  • PHP使用Socket和EPOLL实现在线即时通讯系统消息的发送和接收
    随着互联网的不断发展,即时通讯(IM)系统已成为人们生活、工作中不可或缺的工具。而其中,如何保证消息的快速传输和实时性就成为了系统设计中至关重要的一环。本文将介绍如何使用PHP的Socket和EPOLL实现在线即时通讯系统消息的发送和接收。...
    99+
    2023-05-24
    PHP socket EPOLL
  • vue中如何利用mqtt服务端实现即时通讯
    今天小编给大家分享一下vue中如何利用mqtt服务端实现即时通讯的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我...
    99+
    2022-10-19
  • vue下怎么使用mqtt服务端实现即时通讯
    这篇文章主要介绍“vue下怎么使用mqtt服务端实现即时通讯”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“vue下怎么使用mqtt服务端实现即时通讯”文章能帮助大家解决问题。MQTT协议MQTT(M...
    99+
    2023-07-04
  • vue中如何使用mqtt服务端实现即时通讯
    小编给大家分享一下vue中如何使用mqtt服务端实现即时通讯,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!MQTT协议MQTT(Message Queuing T...
    99+
    2023-06-20
  • node.js中如何使用socket.io实现一个实时通讯应用
    今天小编给大家分享一下node.js中如何使用socket.io实现一个实时通讯应用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一...
    99+
    2023-06-17
  • PHP中怎么利用WebSocket实现一个在线聊天通讯系统
    今天就跟大家聊聊有关PHP中怎么利用WebSocket实现一个在线聊天通讯系统,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。新建WebSocket....
    99+
    2022-10-18
  • Java中怎么实现一个Socket通讯客户端
    Java中怎么实现一个Socket通讯客户端,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。Java Socket通讯代码 <xml version="...
    99+
    2023-06-17
  • Java实现简单通讯录管理系统
    本文实例为大家分享了Java实现通讯录管理系统的具体代码,供大家参考,具体内容如下 题目: 1、完成一个通讯录,需求: (1)添加联系人(联系人:编号,姓名,手机号,QQ,邮箱地址)...
    99+
    2022-11-12
  • Java实现通讯录管理系统项目
    本文实例为大家分享了Java实现通讯录管理系统的具体代码,供大家参考,具体内容如下 一、前言 我们学了这么久的知识了,光学知识不会用是一件很悲伤的事情,所以我们应学完 部分练一些项目...
    99+
    2022-11-12
  • Java实战之用Swing实现通讯录管理系统
    一、系统介绍  1.系统功能 登录系统 查询信息 新增信息 修改信息 删除信息 2.环境配置 JDK版本:1.8 Mysql:8...
    99+
    2022-11-12
  • Java中怎么利用Socket实现一个通讯客户端
    本篇文章给大家分享的是有关Java中怎么利用Socket实现一个通讯客户端,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。具体客户端代码如下:import java.n...
    99+
    2023-06-17
  • 构建一个即时消息应用之实现Conversation页面
    这篇文章主要讲解了“构建一个即时消息应用之实现Conversation页面”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“构建一个即时消息应用之实现Conversation页面”吧!聊天标题让...
    99+
    2023-06-16
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作