可靠传输的TCP协议send成功就意味着数据一定发出去了?

本文来自小白debug的原创分享,原题“【修正版】动图图解!代码执行send成功后,数据就发出去了吗?”,下文有修订和排版优化。

1、引言

回复过很多IM初学者关于MobileIMSDK  通信层代码的疑问,最基础的问题就是“明明用的是TCP协议,而TCP协议也被称为可靠的通信协议,那为什么TCP代码中明确能知道数据是否发送成功,为什么仍然需要应用层去实现消息应答和重传这种逻辑?”。

要真正讲清楚这个问题,还真不是三言两语能讲的明白。。。

本篇文章我们以TCP协议的网络编程逻辑,从Socket缓冲区的角度去拆解,为什么号称可靠传输的TCP协议,在代码中调用send并成功发出数据,并不意味着这个数据就一定通过物理网络发出去了。

cover-opti

技术交流:

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)

(本文已同步发布于:http://www.52im.net/thread-4868-1-1.html)

2、系列文章

本文是系列文章中的第 21篇,大纲如下:

《不为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)》

《不为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)》

《不为人知的网络编程(三):关闭TCP连接时为什么会TIME_WAIT、CLOSE_WAIT》

《不为人知的网络编程(四):深入研究分析TCP的异常关闭》

《不为人知的网络编程(五):UDP的连接性和负载均衡》

《不为人知的网络编程(六):深入地理解UDP协议并用好它》

《不为人知的网络编程(七):如何让不可靠的UDP变的可靠?》

《不为人知的网络编程(八):从数据传输层深度解密HTTP》

《不为人知的网络编程(九):理论联系实际,全方位深入理解DNS》

《不为人知的网络编程(十):深入操作系统,从内核理解网络包的接收过程(Linux篇)》

《不为人知的网络编程(十一):从底层入手,深度分析TCP连接耗时的秘密》

《不为人知的网络编程(十二):彻底搞懂TCP协议层的KeepAlive保活机制》

《不为人知的网络编程(十三):深入操作系统,彻底搞懂127.0.0.1本机网络通信》

《不为人知的网络编程(十四):拔掉网线再插上,TCP连接还在吗?一文即懂!》

《不为人知的网络编程(十五):深入操作系统,一文搞懂Socket到底是什么》

《不为人知的网络编程(十六):深入分析与解决TCP的RST经典异常问题》

《不为人知的网络编程(十七):冰山之下,一次网络请求背后的技术秘密》

《不为人知的网络编程(十八):UDP比TCP高效?还真不一定!》

《不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?》

《不为人知的网络编程(二十):网络ping不通到底有多少原因?一文搞明白!》

《不为人知的网络编程(二十一):可靠传输的TCP协议send成功就意味着数据一定发出去了?》(☜ 本文)

3、什么是 socket 缓冲区

编程的时候,如果要跟某个IP建立连接,我们需要调用操作系统提供的 socket API。socket 在操作系统层面,可以理解为一个文件。

我们可以对这个文件进行一些方法操作:

  • 1)用listen方法:可以让程序作为服务器监听其他客户端的连接;
  • 2)用connect:可以作为客户端连接服务器;
  • 3)用send或write:可以发送数据,recv或read可以接收数据。

在建立好连接之后,这个 socket 文件就像是远端机器的 "代理人" 一样。比如,如果我们想给远端服务发点什么东西,那就只需要对这个文件执行写操作就行了。

1

那写到了这个文件之后,剩下的发送工作自然就是由操作系统内核来完成了。既然是写给操作系统,那操作系统就需要提供一个地方给用户写。同理,接收消息也是一样。

这个地方就是 socket 缓冲区:

  • 1)用户发送消息的时候写给 send buffer(发送缓冲区);
  • 2)用户接收消息的时候写给 recv buffer(接收缓冲区)。

也就是说:一个socket 会带有两个缓冲区,一个用于发送,一个用于接收(如下图所示)。因为这是个先进先出的结构,有时候也叫它们发送、接收队列。

2

4、怎么观察 socket 缓冲区

如果想要查看 socket 缓冲区,可以在linux环境下执行 netstat -nt 命令:

# netstat -nt

Active Internet connections (w/o servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State     

tcp        0     60 172.22.66.69:22         122.14.220.252:59889    ESTABLISHED

这上面表明了,这里有一个协议(Proto)类型为 TCP 的连接,同时还有本地(Local Address)和远端(Foreign Address)的IP信息,状态(State)是已连接。

还有Send-Q 是发送缓冲区,下面的数字60是指,当前还有60 Byte在发送缓冲区中未发送。而 Recv-Q 代表接收缓冲区, 此时是空的,数据都被应用进程接收干净了。

5、执行 send 发送的字节,会立马发送吗?

我们在使用TCP建立连接之后,一般会使用 send 发送数据:

int main(int argc, char *argv[])

{

    // 创建socket

    sockfd=socket(AF_INET,SOCK_STREAM, 0))

    // 建立连接 

    connect(sockfd, 服务器ip信息, sizeof(server)) 

    // 执行 send 发送消息

    send(sockfd,str,sizeof(str),0)) 

    // 关闭 socket

    close(sockfd);

    return 0;

}

上面是一段伪代码,仅用于展示大概逻辑,我们在建立好连接后,一般会在代码中执行 send 方法。那么此时,消息就会被立刻发到对端机器吗?

答案是不确定!执行 send 之后,数据只是拷贝到了socket 缓冲区。至 什么时候会发数据,发多少数据,全听操作系统安排。

tcp_sendmsg 逻辑:

3

在用户进程中,程序通过操作 socket 会从用户态进入内核态,而 send方法会将数据一路传到传输层。在识别到是 TCP协议后,会调用 tcp_sendmsg 方法。

// net/ipv4/tcp.c

// 以下省略了大量逻辑

int tcp_sendmsg()

{ 

  // 如果还有可以放数据的空间

  if (skb_availroom(skb) > 0) {

    // 尝试拷贝待发送数据到发送缓冲区

    err = skb_add_data_nocache(sk, skb, from, copy);

  } 

  // 下面是尝试发送的逻辑代码,先省略    

}

在 tcp_sendmsg 中, 核心工作就是将待发送的数据组织按照先后顺序放入到发送缓冲区中, 然后根据实际情况(比如拥塞窗口等)判断是否要发数据。如果不发送数据,那么此时直接返回。

6、如果Socket缓冲区满了会怎么办

前面提到的情况里是,发送缓冲区有足够的空间,可以用于拷贝待发送数据。

6.1 如果发送缓冲区空间不足,或者满了,执行发送,会怎么样?

这里分两种情况。

首先:socket在创建的时候,是可以设置是阻塞的还是非阻塞的。

int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

比如通过上面的代码,就可以将 socket 设置为非阻塞 (SOCK_NONBLOCK)。

当发送缓冲区满了,如果还向socket执行send。。。。

1)如果此时 socket 是阻塞的,那么程序会在那干等、死等,直到释放出新的缓存空间,就继续把数据拷进去,然后返回。

send阻塞:

4

2)如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息,意思是  Try again , 现在缓冲区满了,你也别等了,待会再试一次。

send非阻塞:

5

我们可以简单看下源码是怎么实现的,还是回到刚才的 tcp_sendmsg 发送方法中:

int tcp_sendmsg()

{ 

  if (skb_availroom(skb) > 0) {

    // ..如果有足够缓冲区就执行balabla

  } else {

    // 如果发送缓冲区没空间了,那就等到有空间,至于等的方式,分阻塞和非阻塞

    if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)

        goto do_error;

  }  

}

里面提到的  sk_stream_wait_memory 会根据socket是否阻塞来决定是一直等等一会就返回。

int sk_stream_wait_memory(struct sock *sk, long *timeo_p)

{

    while (1) {

    // 非阻塞模式时,会等到超时返回 EAGAIN

        if (等待超时))

            return -EAGAIN;    

     // 阻塞等待时,会等到发送缓冲区有足够的空间了,才跳出

        if (sk_stream_memory_free(sk) && !vm_wait)

            break;

    }

    return err;

}

6.2 如果接收缓冲区为空,执行 recv 会怎么样?

接收缓冲区也是类似的情况。当接收缓冲区为空,如果还向socket执行 recv。

1)如果此时 socket 是阻塞的,那么程序会在那干等,直到接收缓冲区有数据,就会把数据从接收缓冲区拷贝到用户缓冲区,然后返回。

recv阻塞:

6

2)如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息。

recv非阻塞:

7

下面用一张图汇总一下,方便大家保存面试的时候用哈哈哈。

socket读写缓冲区满了的情况汇总:

8

7、如果Socket缓冲区满了会怎么办?

7.1概述

首先我们要知道,一般正常情况下,发送缓冲区和接收缓冲区都应该是空的。如果发送、接收缓冲区长时间非空,说明有数据堆积,这往往是由于一些网络问题或用户应用层问题,导致数据没有正常处理。

那么正常情况下,如果 socket 缓冲区为空,执行 close。就会触发四次挥手。

TCP四次挥手:

9

这个也是面试老八股文内容了,这里我们只需要关注第一次挥手,发的是 FIN 就够了。

相关文章可以进一步阅读:

  1. 《理论经典:TCP协议的3次握手与4次挥手过程详解》
  2. 《脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手》

7.2 如果接收缓冲区有数据时,执行close了,会怎么样?

socket close 时,主要的逻辑在 tcp_close() 里实现。

先说结论,关闭过程主要有两种情况:

1)如果接收缓冲区还有数据未读,会先把接收缓冲区的数据清空,然后给对端发一个RST;

2)如果接收缓冲区是空的,那么就调用 tcp_send_fin() 开始进行四次挥手过程的第一次挥手。

void tcp_close(struct sock *sk, long timeout)

{

  // 如果接收缓冲区有数据,那么清空数据

    while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {

        u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -

              tcp_hdr(skb)->fin;

        data_was_unread += len;

        __kfree_skb(skb);

    }

   if (data_was_unread) {

    // 如果接收缓冲区的数据被清空了,发 RST

        tcp_send_active_reset(sk, sk->sk_allocation);

     } else if (tcp_close_state(sk)) {

    // 正常四次挥手, 发 FIN

        tcp_send_fin(sk);

    }

    // 等待关闭

    sk_stream_wait_close(sk, timeout);

}

recvbuf非空:

10

7.3 如果发送缓冲区有数据时,执行close了,会怎么样?

以前以为,这种情况下,内核会把发送缓冲区数据清空,然后四次挥手。

但是发现源码并不是这样的:

void tcp_send_fin(struct sock *sk)

{

  // 获得发送缓冲区的最后一块数据

    struct sk_buff *skb, *tskb = tcp_write_queue_tail(sk);

    struct tcp_sock *tp = tcp_sk(sk);

  // 如果发送缓冲区还有数据

    if (tskb && (tcp_send_head(sk) || sk_under_memory_pressure(sk))) {

        TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN; // 把最后一块数据值为 FIN

        TCP_SKB_CB(tskb)->end_seq++;

        tp->write_seq++;

    } else {

    // 发送缓冲区没有数据,就造一个FIN包

  }

  // 发送数据

    __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);

}

此时,还有些数据没发出去,内核会把发送缓冲区最后一个数据块拿出来。然后置为 FIN。

socket 缓冲区是个先进先出的队列,这种情况是指内核会等待TCP层安静把发送缓冲区数据都发完,最后再执行 四次挥手的第一次挥手(FIN包)。

有一点需要注意的是,只有在接收缓冲区为空的前提下,我们才有可能走到 tcp_send_fin() 。而只有在进入了这个方法之后,我们才有可能考虑发送缓冲区是否为空的场景。

11

8、拓展阅读:UDP有缓冲区吗?

8.1 UDP也有缓冲区吗

说完TCP了,我们聊聊UDP。这对好基友,同时都是传输层里的重要协议。既然前面提到TCP有发送、接收缓冲区,那UDP有吗?

以前我以为:

"每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。"

后来我发现我错了:UDP socket 也是 socket,一个socket 就是会有收和发两个缓冲区。跟用什么协议关系不大。

有没有是一回事,用不用又是一回事。

8.2 UDP不用发送缓冲区?

事实上,UDP不仅有发送缓冲区,也用发送缓冲区。

一般正常情况下,会把数据直接拷到发送缓冲区后直接发送。还有一种情况,是在发送数据的时候,设置一个 MSG_MORE 的标记。

ssize_t send(int sock, const void *buf, size_t len, int flags); // flag 置为 MSG_MORE

大概的意思是告诉内核,待会还有其他更多消息要一起发,先别着急发出去。此时内核就会把这份数据先用发送缓冲区缓存起来,待会应用层说ok了,再一起发。

我们可以看下源码:

int udp_sendmsg()

{

    // corkreq 为 true 表示是 MSG_MORE 的方式,仅仅组织报文,不发送;

    int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;

    //  将要发送的数据,按照MTU大小分割,每个片段一个skb;并且这些

    //  skb会放入到套接字的发送缓冲区中;该函数只是组织数据包,并不执行发送动作。

    err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,

                 sizeof(struct udphdr), &ipc, &rt,

                 corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

    // 没有启用 MSG_MORE 特性,那么直接将发送队列中的数据发送给IP。

    if (!corkreq)

        err = udp_push_pending_frames(sk);

}

因此,不管是不是 MSG_MORE, IP都会先把数据放到发送队列中,然后根据实际情况再考虑是不是立刻发送。

而我们大部分情况下,都不会用  MSG_MORE,也就是来一个数据包就直接发一个数据包。从这个行为上来说,虽然UDP用上了发送缓冲区,但实际上并没有起到"缓冲"的作用。

9、参考资料

[1] TCP/IP详解 - 第21章·TCP的超时与重传

[2] 快速理解TCP协议一篇就够

[3] 假如你来设计TCP协议,会怎么做?

[4] 手把手教你写基于TCP的Socket长连接

[5] 到底什么是Socket?一文即懂!

[6] 我们在读写Socket时,究竟在读写什么?

[7] 拔掉网线再插上,TCP连接还在吗?一文即懂!

[8] 深入操作系统,一文搞懂Socket到底是什么

[9] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?

[10] 从客户端的角度来谈谈移动端IM的消息可靠性和送达机制


(本文已同步发布于:http://www.52im.net/thread-4868-1-1.html)

相关推荐

鸿蒙原生即时通讯/IM系统RainbowTalk v2.8版发布,基于MobileIMSDK框架、ArkTS编写

💧 此版本解决了若干bug,以及ui和功能细节优化,同时支持最新鸿蒙7.0系统。

hellojackjiang2011的博客 165

深度剖析IM消息乱序的根本原因及优化方法

本文深度剖析即时通讯IM消息乱序的问题,梳理IM客户端时间戳不可信、多端并发、群消息扇出重试三大核心陷阱,拆解微信、钉钉、企业微信的时序设计方案,并从锚点设计、多端去交错、幂等兜底、可观测性等维度,给出IM项目消息时序性保证的落地优化策略。

hellojackjiang2011的博客 293

IM聊天不掉线?生产级WebSocket断线重连、心跳保活实战

本文将给出一套生产级Web端即时通信方案,包括心跳检测保活 + 指数退避重连,代码直接能跑。

hellojackjiang2011的博客 323

IM全文检索技术(五):vivo敏感词匹配系统的技术实践

本文将从算法选型入手,结合两个实际场景来介绍谛听系统的敏感词匹配算法。包括底层算法选型思路和两种实际场景下提升敏感词精准度、降低漏杀率的方案。

hellojackjiang2011的博客 189

Nginx生产级WebSocket实时长连接负载均衡,一文搞定

本文从基础配置、生产配置、Docker / K8s、安全、排障、性能调优,系统性讲清 WebSocket 在 Nginx 中的正确打开方式。

hellojackjiang2011的博客 308

即时通讯IM离线消息该怎么存?全面盘点主流存储方案

离线消息是IM系统的核心能力,当用户不在线时,消息必须可靠存储,待用户上线后完整推送。本文将对比主流的离线消息存储方案,帮你找到最适合业务场景的架构。

hellojackjiang2011的博客 310

移动端实时音视频聊天框架 RainbowAV v2.2.7发布

1)

hellojackjiang2011的博客 247

都HTML5了,Web端即时通讯技术到底该用什么?一文即懂!

本文分享的是 Web 端最常用即时通讯技术(包括 WebSocket、SSE、WebRTC、轮询等)的原理、优缺点与适用场景等,以及生产环境下的高并发场景下的进阶实践。

hellojackjiang2011的博客 341

基于开源MobileIMSDK框架,即时通讯IM产品RainbowChat v12.1发布

此版本新增了短信验证码的注册和登录功能,解决了Android 16手机上发送文件失败等问题。

hellojackjiang2011的博客 209

IM千人群聊该不该用消息写扩散?详细算算成本账!

本文将为你详解 IM 群聊消息的写扩散技术原理以及写扩散过程中的服务端详细性能成本,同时对比了市面上IM大厂的技术方案,给出分规模架构选型与以及性能优化策略等。

hellojackjiang2011的博客 402

直播系统聊天技术(十):基于实时消息总线的活动系统架构设计

本文分享的是网易云音乐技术团队基于实时消息总线技术,解决了直播活动系统的模块灵活组合、消息治理与异步履约等问题,希望能给你带来启发。

hellojackjiang2011的博客 369

轻量级Web端IM即时通讯框架MobileIMSDK-Web,基于Socket.io库

MobileIMSDK-Web是一套Web端即时通讯框架,超轻量级、极少依赖,高度提炼、简单易用。

hellojackjiang2011的博客 286

socket长连接在手游场景下的技术实践

本文介绍了37手游基于B站goim框架自研长连接系统的实践。系统采用分层设计,支持多协议和发布/订阅机制,用于直播弹幕、实时推送等场景,实现了高性能与业务适配。

hellojackjiang2011的博客 404

基于开源MobileIMSDK框架,即时通讯IM产品RainbowChat v12.0发布

RainbowChat 是一套基于 MobileIMSDK 开源通信框架的产品级移动端IM系统。可能是市面上开放源码的,唯一一款同时支持TCP、UDP、WebSocket三种网络通信协议的IM产品。

hellojackjiang2011的博客 518

社交场景下的统一即时通讯im消息流交互层模块化技术实践

这些IM场景,在消息流的展示形式上是极为相似的,同时每个业务又有着自己特殊的交互需求。基于此,我们对IM消息流能力做了标准化的构建,来减少IM功能的业务接入成本;同时也是为了统一各个业务的技术方案,减少跨业务开发的理解和维护成本。

hellojackjiang2011的博客 571

现代IM系统中聊天消息的同步和存储方案探讨

本文内容主要涉及IM系统中的消息系统架构,探讨一种适用于大用户量的消息同步以及存储系统的架构实现,能够支持消息系统中的高级特性“多端同步”以及“消息漫游”。在性能和规模上,能够做到全量消息云端存储,百万TPS以及毫秒级延迟的消息同步能力。

hellojackjiang2011的博客 75

即时通讯安全篇(十六):对称加密 vs 非对称加密?一文搞懂

本文将以通俗案例讲解加密本质,介绍对称加密(含 AES、迪菲–赫尔曼密钥交换)与非对称加密(RSA)原理、特点及应用,并阐释其数学基础。

hellojackjiang2011的博客 397

网络编程入门如此简单(五):UDP跟TCP相比,到底差了什么?

本文将通俗易懂地为你类比解释UDP与TCP的核心差异,包括如何基于UDP实现TCP的可靠传输:通过模拟三次握手、添加序列号与确认机制解决顺序和丢包问题,利用滑动窗口控制流量,并引入拥塞控制算法来动态调整发送速率等。

hellojackjiang2011的博客 448

基于WebSocket的IM即时通信方案在H5游戏场景下的技术实践

本文将从H5游戏场景下的实时信息更新需求切入,探讨下在类似这种高并发、低延迟的业务需求中,如何使用 IM 方案来解决频繁的数据更新问题,也顺便介绍下 WebSocket 的基本运作流程等。

hellojackjiang2011的博客 1069

鸿蒙Next原生IM即时通讯RainbowTalk,纯ArkTS编写,基于开源MobileIMSDK框架

RainbowTalk 是一套基于 MobileIMSDK 开源通信框架的产品级纯血鸿蒙NEXT端IM系统。RainbowTalk 由纯ArkTS编写、全新开发,没有套壳、也没走捷径,原生“纯血”。

hellojackjiang2011的博客 1229

如何保障分布式IM聊天系统的消息可靠性(即消息不丢)

本文主要聚焦分布式IM聊天系统消息可靠性问题,即如何保证消息不丢失。

hellojackjiang2011的博客 732

如何保障分布式IM聊天系统的消息有序性(即消息不乱)

本篇主要总结和分享分布式IM聊天系统架构中关于消息有序性的设计和实践。

hellojackjiang2011的博客 835

B站即时通讯IM消息系统的新架构升级实践

本文要分享的是B站IM消息系统的新架构升级实践总结,内容包括原架构的问题分析,新架构的整体设计以及具体的升级实现等。

hellojackjiang2011的博客 714

AI大模型爆火的SSE技术到底是什么?万字长文,一篇读懂SSE!

本文从SSE(Server-Sent Events)技术的原理到示例代码,为你通俗易懂的讲解SSE技术的方方面面。

hellojackjiang2011的博客 786

腾讯新闻APP的消息推送Push架构技术重构实践

本文主要分享的是腾讯技术团队近年来对腾讯新闻消息推送PUSH系统做的架构优化和技术实践。

hellojackjiang2011的博客 843

移动端弱网优化专题(十五):字节跳动移动端网络HttpDNS优化实践

本文要分享的是字节跳动团队针对火山HTTPDNS Cache2.0通过自研网段库与动态划分算法,将缓存粒度从“城市-运营商”细化为“网段”,解决了传统方案的城市级调度污染问题。配合缓存分级、预取等优化,在提升调度精准度的同时保证了高命中率,最终实现了服务端调度准确性提升和客户端性能优化。

hellojackjiang2011的博客 720

转转客服IM聊天系统背后的技术挑战和实践分享

在当今互联网时代,高效的用户服务是提升用户体验的关键。转转自研的客服IM聊天系统作为用户与客服沟通的桥梁,承担着传递信息、解决问题的关键角色。然而,消息数据的流转并非一帆风顺,本文将深入探讨IM系统在消息传递过程中遇到的问题和挑战,以及相应的技术解决方案。

hellojackjiang2011的博客 1096

ChatGPT如何实现聊天一样的实时交互?快速读懂SSE实时“推”技术

本文将带你快速认识SSE实时通信协议,包括它的技术原理、常见使用场景、与同类技术的对比以及简单的示例代码等。

hellojackjiang2011的博客 124

全面适配iOS 26液态玻璃,基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v10.2发布

RainbowChat是一套基于开源IM即时通讯聊天框架 MobileIMSDK 的产品级移动端IM系统。

hellojackjiang2011的博客 1157

即时通讯安全篇(三):一文读懂常用加解密算法与网络通讯安全

作为开发者,也会经常遇到用户对数据安全的需求,当我们碰到了这些需求后如何解决,如何何种方式保证数据安全,哪种方式最有效,这些问题经常困惑着我们。52im社区本次着重整理了常见的通讯安全问题和加解密算法知识与即时通讯/IM开发同行们一起分享和学习。

hellojackjiang2011的博客 121
上一篇: 即时通讯安全篇(三):一文读懂常用加解密算法与网络通讯安全
下一篇: 全面适配iOS 26液态玻璃,基于开源IM即时通讯框架MobileIMSDK:RainbowChat-iOS端v10.2发布
jackjiang2025
jackjiang2025 网际时代技术分享账号 网际时代技术分享账号
博客等级 码龄8年 700粉丝 318原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值