UDP中校验和使用比较简单的方式,CRC 算法来完成校验,循环冗余检验

比如要产生一个两个字节的校验和

我们用 short 变量来遍历数据,在给 short += 的时候,即使超出了 short 的两个字节的范围,溢出了,这里的话也不管溢出

UDP 数据报发送方,在发送之前,先计算一遍 CRC,把算好的 CRC 值放到 UDP 数据报中(设这个 CRC 值为 value1)

接下来这个数据包通过网络传输到达接收端,接收端收到这个数据之后,也会按照同样的算法,再算一遍 CRC 的值,得到的结果是 values,比较自己计算的 values2 和 收到的 value1 是否一致,如果一致的话,就说明数据是没问题的,如果不一致,传输过程中就出现了比特翻转了

上述的 CRC 算法中,如果只有一个 bit 位 发生比特翻转,此时 100% 能发现问题

但如果有两个/多个bit位发生翻转,有可能恰好校验和和之前一样

这样的情况概率比较低,可以忽略不计

除了 CRC 之外,还有一些更高精度的校验和算法,业界还有一个很常用的,md5 算法 / sha1 算法

md5 的特点
1.定长

无论原始数据多长,算出来的 md5 的最终值都是固定长度

2.分散

计算 md5 的过程中,原始数据只要变化一点点,算出来的 md5 值就会差异很大

网络传输中,如果出现 bit 翻转,意味着只是极少的 bit 翻转了,即使就只是翻转 1 bit,最终得到的 md5 都会差异非常大

这样的特性,也决定了 md5 也可以作为一个字符串 hash 算法

hash 表比较害怕哈希冲突的,如果你多个 key 映射到同一个数组下标上,就可能引起长度很长的 链表 / 树,导致 hash 性能下降

3.不可逆 

给你一个源字符串,计算 md5 值,这个过程非常简单(虽然比 crc 要复杂,但还是挺简单的)

但是 把一个 算好的 md5 的值还原会原始的字符串,理论上是不行的

原始字符串到 md5 这个过程,有很多信息损失,直接还原不行

如果通过计算现有字符串 md5 值去和这个值对,计算量太大了

TCP 协议

工作中最常用的协议

源端口,目的端口,和 UDP 是一样的

首部长度(报头长度),UDP 报头固定是 8 个字节

TCP 报头中的前 20 个字节是固定长度的

选项 “可选的” “可有可无的”

保留    reserved 保留位

UDP 这个协议,长度受到 2 个字节的限制,想要进行扩展,发现扩展不了,一旦你改变这里的报头长度,就会使机器发送的 UDP 数据报和其他机器不兼容,无法通信了

TCP 设定报头的时候,提前准备了几个保留位,后续一旦需要用了,就可以把保留位使用起来

后续一旦需要扩展功能,使用保留位就可以实现,就可以避免 tcp 的扩展引起不兼容问题

6 位标志位,TCP 非常核心的部分

16 位检验位       类似于 UDP 的校验和,把报头和数据载荷放到一起计算校验和、

TCP 的特点是 有连接  可靠传输  面相字节流    全双工

TCP 安身立命的本钱,初心就是解决 “可靠传输” 问题

网络通信过程是复杂的,无法确保发送方发出去的数据,100% 能够到达接收方

此处可靠性,只要尽可能的去进行发送了,发送方能够知道,对方是是否收到,就认为是 可靠传输

用来确保可靠性,最核心的机制,称为

1.“确认应答”

发送方发了个消息,只有接收方应答,发送方才知道刚才的消息被成功接收到了

一个数据包从发送方到接收方传输过程中走的路径可能不一样

第一个数据包,走路线一,第二个数据包走数据包二

有可能路线二非常畅通,路线一阻塞了,第二个数据包虽然迟但是是先到的

为了解决上述问题,引入了序号和确认序号,对于数据进行编号,应答报文里就告诉发送方说,这次应答是哪个数据

应答报文中的确认序号,是按照发送过去的最后一个字节的序号再加 1 来进行设定的

主机B 收到了 1 - 1000 这些字节数据之后,反馈一个应答报文,应答报文中的确认序号的值就是 1001

1001 的含义,有两个理解方式

1. < 1001 的数据,都已经收到了

2.发送方接下来要给 接收方 发 1001 开始的数据了

TCP 的确认应答是确保 TCP 可靠性的最核心机制

确认应答中,通过应答报文来反馈给发送方,当前数据正确收到了

应答报文也叫 ack 报文

平时 ACK 这个位是 0 ,如果当前报文是应答报文,此时报头中的这一位就是 1

网络上出现丢包

网络中的路由器/交换机,不仅仅是给一个通信提供服务

整个网络就可能存在,某个路由器/交换机,某个时刻突然负载量很高,短时间内可能有大量的数据包要经过这个设备转发

但是,一台设备能够处理的数据量是有限的,很可能瞬间的负载超出了这个设备的极限,多出来的部分就没了,就丢包了

2.超时重传

通过应答报文就可以告诉发送方,当前数据是不是成功收到

但是,网络上可能存在“丢包”情况,如果数据包丢了,没有到达对方自然就没有 ack 报文了

这个情况就要超时重传了

TCP 可靠性就是在对抗丢包,期望在丢包的背景下,也能够尽可能把包给传过去

发送方发了个数据之后,要等,等的时间里,收到了 ack

如果等了好久,ack 还没等到,此时发送方就认为数据的传输出现丢包了

当认为丢包之后,就会把刚才的数据包再传输一次(重传)

等待的过程有一个时间阈值,就是超时

上面的过程中,是认为没收到 ack 就是丢包,丢包,不一定是发的数据丢了,还有可能 ack 丢了

数据丢了,还是 ack 丢了,发送方角度来看,区分不了,都是 ack 没收到

此时如果是扣款请求,那么一次的请求就会变成两次,也会扣两次款

TCP socket 在内核中存在接收缓冲区(一块内存空间),发送方发来的数据,是要先放到接收缓冲区中的,然后应用程序调用 read / scanner.next 才能读到数据,这里的读操作其实是读取接收缓冲区

当数据到达接收缓冲区的时候,接收方首先会先判定一下看当前缓冲区中是否已经有这个数据了(或者这个数据曾经再接收缓冲区中存在过)

如果已经存在或者存在过了,就直接把重复发来的数据就丢弃了,就能确保应用程序,调用 read / scanner.next 的时候,不会出现重复数据了

接受方如何判定这个数据是否是“重复数据”

核心判定依据是数据的序号

1.数据还在接收缓冲区里,还没被 read 走

此时,拿着新收到的数据序号和缓冲区中所有的数据的序号对比一下,有一样的就是重复了,就要把新数据给丢弃了

2.数据再接收缓冲区中,已经被应用程序给 read 走了,此时新来的数据序号直接无法在接受缓冲区查到

应用程序读取数据的时候,是按照序号的先后顺序,连续读取的

一定是先读序号小的,后读序号大的

此时 socket api 中就可以记录上次读的最后一个字节的序号是多少

比如上次读的最后一个字节的序号是2000下一个应该是2001,但是读到的是 1001 那么就可以把这个新的数据包判定为“重复的包”直接丢弃了

需要注意的是,即使是丢弃,但还是会发送ack的,不然还是会继续重传

接收方希望应用程序读到的数据顺序是正确的,否则对于接收方应用程序的逻辑也会有一定影响

上述谈到的ack 重传,保证顺序,自动去重,都是 TCP 内置的,使用 TCP api 的时候

outputStream.write()它就自动生效了

超时是会重传,但一味的重传而得不到回应就会采取策略

1.重传的次数是有限的,重传到一定程度还没有ack,就尝试重置连接,如果重置失败,就直接放弃连接
2.重传的超时时间阈值也不是固定不变的,随着重传的次数增加,而增大,它的频率会越来越低

3.连接管理

建立连接

TCP 是有连接的

客户端执行,socket = new Socket(severIp,serverPort)就是在建立连接

真正的建立连接实在操作系统内核完成的

内核完成上述建立连接的过程称为“三次握手”

上述流程上讲,是有四次交互,但是实际过程中,其中的两次交互,能够合二为一,最终形成“三次握手”

客户端是主动的一方,第一次交互一定是客户端主动发起的

syn 是一个特殊的 TCP 数据报

1.没有载荷,不会携带应用层数据

2.六个标志位中的第五位,为 1

syn 不带有应用层载荷,但是也会带有 IP 报头/ 以太网数据帧帧头更会有TCP 报头

TCP 报头中包含了客户端自己的端口

IP 报头包含了客户端自己的 IP 

两种可能性

1.服务器同意了

2.服务器没同意

所谓的建立连接的过程,本质上就是通信双方各自给对方发起一个 syn,给子给对方回应一个 ack

(客户端的信息告知服务器,这个操作确实再第一次握手时就完成了,但是最终确立出这个连接要建立,确立后续要进行的通信,还是得所有的流程走完)

虽然第一次握手,客户端已经把自己的信息告诉服务器了,但是服务器具体是否确定存储这个信息,还是得再等等

等到所有握手环节完成,服务器才会最终保存客户端相关信息

上述的过程中间的两次可以合并

syn 就是第五位为 1

ack 就是第二位为 1

完全可以有一个数据包,第五位 和 第二位 都是 1

此时这个数据包就同时起到两个作用,既能够应答上个请求,也能发起 syn

为什么要进行握手,意义何在?

三次握手对于“可靠传输”起到了一定作用,关键的可靠传输还是通过确认应答以及超时重传来保证的

1.三次握手,可以先针对通信路径,进行投石问路,初步确认一下通信链路是否畅通(可靠性的前提)

B 发信息后,A 回了消息, B 可以确定 B 的发送信息和接受信息方面没问题,B 又再回了一个消息,A 收到了消息,可以确定 自己的 发送信息 和 接收信息方面没问题

那么为啥必须是三次握手?四次可以吗?两次可以吗?

四次就多余了,A 和 B 都已经知道自己的情况了,没必要多一步

两次就少了,B 知道自己的情况了,但是 A 并不知道 它发的信息 B 收到了没有

2.三次握手,也是在验证通信双方,发送能力和接受能力是否正常
3.三次握手的过程中也会协商一些必要的参数

TCP 中也是有很多的参数要进行协商的,往往是以 “选项” 部分来体现的

最少 0 字节,最多 40 字节(TCP 报头总长度最多 60,去掉前面固定的 20,还剩 40)

TCP 的通信序号起始值一般是选择一个比较大的数字来开头的

第一次连接的过程中,传输的有一个数据包,遇到了阻塞,一直没有到达对端

等到的时候已经“改朝换代”,之前的连接早没了,是新的连接

此时这份数据应该被丢弃

数据报时按照 ip + 端口 进行识别的

第一个连接,是用客户端 A 来连的,第二个连接用客户端 B 来连的

此时数据到达这一边,早已物是人非,这个时候再处理这个数据就不合适,丢弃是一个上策

通过数据我们来识别之前的数据包,两者的序号差异是十分明显的

三次握手的基本流程,是通信双方各自给对方发起 syn,各自给对方回应 ack

中间的两次合并成一次了

断开连接

四次挥手

连接本质上就是让通信双方保存对方的信息,每个 客户端 / 服务器,都要保存很多对端的信息

一旦多了就要用“数据结构”

断开连接的本质目的,就是为了把对端的信息,从数据结构中给删除掉/释放掉

fin ---> Finnish

四次挥手,可以是客户端发起也可以是服务器发起

socket.close()就会出发 FIN,进程结束也会出发 FIN

关闭 cocket 文件(结束进程,也会关闭文件)

通信双方各自给对方发起 FIN,再各自给对方反馈 ACK

四次挥手,中间两次交互能否合并?

对于三次握手来说,中间两次几乎是同时完成的

而对于四次挥手来说,中间的两次时间间隔比较长,无法进行合并

四次挥手和三次握手
相似之处:都是通信双方各自给对方发起一个 syn / fin,各自给对方返回 ack

数据传输的顺序,syn / ack / syn / ack       fin / ack / fin / ack

不同之处:

三次握手中间两次一定能合并   四次挥手不一定

三次握手是客户端主动,四次挥手,客户端 / 服务器都可以主动

FIN 是通过 close 来触发,进程结束会涉及到 close,主动调用也是可以的呀

如果服务器挂了,会触发主动的四次挥手,更多的时候是服务器主动调用 close 来结束连接

为了防止服务器挂了,往往会做出一些冗余

每个镜像就只承担总请求量的一部分,每个机器承担的压力就小了,总的承担请求量就更多了

保持高可用的基本原则在于通过冗余的方式,确保任何一个机器出现问题,还有备份能顶上

只要机器足够多,此时就可以确保可用性是很高的

LISTEN 状态,表示服务器这边,创建好 serverSocket 了,并绑定端口号完成

ESTABLISHED 已确立的,客户端 和 服务器连接已经建立完毕(三次握手完了)

CLOSE WAIT 表示,接下来代码中需要调用 close 来主动发起 fin 收到对方 fin 之后进入这个状态(被动断开连接)
TIME_WAIT 就是表示本端给对方发起 FIN 之后,对端也给我发 FIN 此时本端进入 TIME_WAIT给最后一个ACK的重传留一些时间(主动断开连接)

一般情况下 CLOSE_WAIT不太容易观察到

代码中会比较快速的关闭 socket,这个时候,状态就从 CLOSE_WAIT -> LAST_ACK

TIME_WAIT 存在的意义,主要防止最后一个 ACK 丢包

服务器如果没有收到最后一个 ACK,就会重传 FIN

客户端如果在这个环节,把TCP连接释放掉,此时意味着重传的 FIN 就无法被返回 ACK 了(保存对端信息的数据结构存在,才能给这个连接提供各种操作,才能返回 ACK)

此处的 TIME_WAIT 等待也不是无休止的等待,最多等 2 MSL(MSL 是一个系统内核的配置项,表示客户端到服务器之间,消耗的最长时间,这个时间一般都是非常大的时间)

在四次挥手的过程中,会涉及到确认应答 和 超时重传,如果没有 ACK 就视为丢包

服务器如果没有收到最后一个 ACK,就会重传 FIN

4.滑动窗口

确认应答,超时重传,连接管理 -> 可靠传输

可靠传输,付出了代价,传输效率,单位时间,能传输的数据量变少了

确认应答机制下,每次发送方收到一个 ack 才会发下一个数据,大量时间都消耗在了等待 ack 

希望保证可靠传输的前提下,能够让效率尽量高点,让消耗时间成本尽可能少点

滑动窗口可以在保证可靠传输的基础上,提高效率(提高效率是降低损失,不是增加速度)

虽然提高了效率,但相比 UDP 没有可靠性来说还是低于它的

引入了滑动窗口

批量传输

之前发一个数据,等待 ack,再发下一条数据

现在先发一个数据,不等ack,再发下一个继续再往下发

连续发了一定数据之后,同意等一波 ack

把多次请求的等待时间,使用同一份时间来等,减少了总的等待时间

滑动窗口

这四份数据是已经批量传输出去了,传输出这四份数据之后,就等待 ack,暂时就不传了,把白色区域称为“窗口大小”

批量发了四个数据,就会对应四个 ack,此时四个也不一定是同时到达,而是有先有后,等回来一个 ack 就往后发一组

上述的发送 / 返回 ack的过程都很快,窗口往后移动就很快,直观上就感觉是一个 “滑动” 的效果

如果丢包了,滑动窗口咋办?

1.ack丢了

对于这种情况的话,不用做任何处理,对于可靠性没有影响,也不需要进行重传

接收到了 2001 ack 就代表它之前的数据都收到了

2.数据丢了

我们仔细看图就能明白,主机B的回应报文本该是 3001,但是现在却是 1001,就是在告诉发送方,1001 - 2000丢包了没有收到

在进行重复确认应答就是在告诉对方,我要的不仅是当前的数据还有 1001 这个数据

发送方在多次索要 1001 ack的确认报文之后,认为 1001 丢包了,就进行了重传

重传了 1001 之后,确认序号是 7001 因为之前的数据已经收到了

上述重传的过程中,整体的效率非常高,这里的重传做到了“针对性”重传,哪个丢了重传哪个,收到的数据没必要重传,整体的效率没有额外的损失,把这种重传称为“快速重传”

确认应答    超时重传

滑动窗口     快速重传

两者并不冲突,可以同时存在

滑动窗口中也有确认应答,把等待策略稍作调整,转成批量了,批量前提是短时间发了很多数据,如果发的数据很少,就会退化成确认应答

如果当前传输过程是按照滑动窗口(短时间传输大量数据),按照快速重传保证可靠性,此时判定丢包的标准就是看连续有多个 ack 索要同一个数据

如果当前传输过程不是按照滑动窗口(没有传很多数据),此时仍然按照之前的超时重传,保证可靠性,此时判定丢包标准就是达到超时时间还没有 ack 到达

5.流量控制

通过滑动窗口可以提高传输效率

窗口越大,更多的数据复用同一块时间等待,效率就更高(批量传多少数据不需要等待 ack,此时数据的量就称为“窗口大小”)

窗口大小是否能无限大?可靠传输的前提是任何提升效率的行为都不应该影响到可靠性

如果发的过快接收方处理不过来,就会出现丢包(接收方缓冲区满了)

接收缓冲区丢包了(满了)就算是重传也没用,反而会浪费硬件资源

与其等它满了,倒不如提前感知,放缓蓄水速度,让发送方发送速度和接收方处理速度,能够步调一致,让接收方反过来影响发送方速度

通过该字段来给发送方反馈发送速度,这个字段在普通报文中没有意义,在ack报文中才有意义

通过这个大小反馈给发送方接下来要发送的窗口设置成多少合适

接收方会按照自己接收缓冲区剩余空间的大小,作为 ack 中的窗口大小的数值,下一步发送方就会根据这个数值来调整自己的窗口大小

TCP 报头选项中,包含了一个参数叫做窗口扩展因子,真是的要设置的窗口大小,是 16 位窗口大小 * 2 ^ 窗口扩展因子

6.拥塞控制     都是要限制发送方发送数据的速率

流量控制是站在接收方的角度来制约发送方速率的

如果当前接收方处理速度很快,但是中间通信路径出现问题了,某个地方堵塞了,发送的速度就算再快,也没用

木桶效应

能装多少水,取决于“最短的板”

针对这种情况,核心思路,把中间路径经过的所有设备,视为一个整体,通过“实验”的方式,找到一个比较合适的传输效率

比如:按照某个窗口大小发送数据之后,出现丢包,就视为中间路径存在拥堵,就减少窗口大小,没出现丢包,就尝试增大窗口

上述策略使得窗口大小可以进行动态变化

总的原则是 流量控制 和 拥塞控制,谁产生的窗口大小更小,谁就说了算

窗口大小怎么试出来?
1.慢启动,刚开始传输的数据,速率是比较小的,采用的窗口大小也就比较小
2.如果上述传输的数据,没有出现丢包,说明网络还是畅通的,就是增大窗口大小,增大方式是按照指数来增长的
由于慢启动开始的时候窗口非常小,在网络畅通的情况下,通过指数增大可以使得窗口大小快速变大,可以保证传输效率
3.指数增长  不会一直持续保持的,可能会增长太快,一下就导致网络拥堵,这里引入一个“阈值”,当拥塞窗口达到阈值之后,此时,指数增大就成了线性增长
线性增长能够使当下窗口持久的保持在一个比较高的速率,并且不容易一下就造成丢包
4.线性增长也是一直在增长,积累一段时间之后,传输速度可能太快引起丢包,一旦出现丢包,就把拥塞窗口重置成较小的值,回到最初的慢启动

这里也会根据刚才丢包的窗口大小,重新设置指数增长到线性增长的阈值

动态平衡

拥塞窗口大小始终在变

主要是因为中间路径拥堵情况不停发生改变

我们会发现新的版本并没有回到慢启动,主要是现在的通信质量相比之前有了很大提升

7.延时应答

基于滑动窗口,尽可能提高一点效率

结合滑动窗口以及流量控制,能够通过延时应答 ack 方式,把反馈的窗口大小,搞大一点

核心在于在允许的范围内,把窗口尽可能搞大

接收方收到数据之后,不会立即返回 ack,而是稍等一下,等一会再返回 ack,等了这一会,相当于给接收方的应用程序这里,腾出更多的时间,来消费这里的数据

如果立即返回ack,此时接受缓冲区的大小相对比较小

如果不是立即返回,接收方应用程序就能用这些时间来消费掉一些数据

通过滑动窗口来传输数据

在滑动窗口下,如果 ack 丢了,没啥影响,延时应答具体怎么延时,不是靠单纯的时间,而是可以按照 ack丢了 的方式来处理

正常每个数据都有 ack,此时就可以隔几个数据再返回一个 ack(每个几个数据,就能起来延时应答的效果)另外也能减少 ack 传输的数量,也能起到节省开销的效果

这个情况如果延时时间到达了一定的程度,即使个数没够还是会返回 ack

8.捎带应答 基于延时应答,引入的机制,能够提升传输效率

修改窗口大小,可以提升效率

捎带应答,是走另一条路,尽可能把能合并的数据包进行合并,从而起到提高效率的效果

、很多时候客户端和服务器之间是长连接,要进行若干次请求

在捎带应答的加持之下,后续每次传输请求响应,都可能出发捎带应答,都可能把接下来要传输的业务数据和上次的 ack 合二为一

因为 延时应答 + 捎带应答 使得后续的四次挥手有可能合并成三次

9.面向字节流

面向字节流有一个非常重要的问题“粘包问题”

此处的包是 “ TCP 的载荷中的应用数据包”

tcp 传输数据到了接收方之后,接收方要根据 socket api 来 read 出来,read 出来的结果就是应用层数据包

由于整个 read 过程非常灵活,可能会使代码中无法区分出当前的数据从哪到哪是一个完整的应用数据包

此处假定,这三个 tcp 数据报都是携带的完整的应用层数据包

应用程序调用 read 读取数据,由于是字节流,读取的过程十分灵活

读取数据之后,就需要把数据转成应用层数据包

一次可以读 a 也可以读出 aaa 也可以 aaab

究竟哪种才是正确的?多个应用层数据包混淆不清称为“粘包”

粘包问题,不是 TCP 独有的问题,只要是面向字节流都有相同的问题

解决问题的关键在于“明确包之间的边界”

1.通过特殊符号,作为分隔符
2.指定出包的位置,比如在包一开始的位置加上一个特殊的空间来表示整个数据的长度、

上述的问题在设计应用层协议的时候都应该考虑进去

UDP 没有这种问题,UDP 传输基本单位是 UDP 数据报,在 UDP 这一层已经分开了,只要约定好,每个 UDP 数据报都只承载一个应用层数据包,不需要额外的手段来区分了

10.异常情况

考虑丢包最严重的情况,甚至说网络出现故障如何处理?

1.其中有一方出现了进程崩溃

进程无论是正常结束,还是异常崩溃,都会触发到回收文件资源,关闭文件的效果,就会触发四次挥手

TCP 连接的生命周期,可以比进程更长一点,虽然进程已经退出了,但是TCP连接还在,仍然可以继续进行四次挥手

虽然说是异常崩溃,实际上和正常的四次挥手结束,没啥区别,进程不在了,是通过系统中仍然持有的连接信息,完成后续的挥手过程的

2.其中有一方出现了关机

当有个主机出发了关机操作,就会先强制终止所有的进程,终止进程自然会触发 4次挥手

点了关机之后,此时四次挥手不一定能挥完,系统马上就关闭了

如果挥的快,就能够顺利挥完,此时,本端和对端都能都能正确的删除掉保存的连接信息

如果挥的不快只要也能把第一个 fin 发给对端,至少能告诉对方,我这边要结束了

对端收到 fin 之后,对端也要进入释放连接流程了,返回 ack,并且也发fin,这里发的 fin 不会有 ack 了,fin 没有收到 ack 之后,势必要进行重传

当重传几次之后,发现还是不行,还是没有 ack,这个时候,单方面的释放连接信息

3.其中一方出现了断电

如果直接断电,机器瞬间关机,此时就来不及发送 fin

1.断电是接收方,发送方就会突然发现,没有 ack了,就要重传

重传几次之后,还是不行

TCP 就会尝试“复位”连接(相当于清楚原来的 TCP 中的各种临时数据,重新开始)

需要用到一个 TCP 中的“复位报文段”

2.断电的是发送方,接收方本来在阻塞等待发送方的消息

结果一直没有收到消息

这个情况接收区需要区分出,发送方是挂了还是好着暂时没发,TCP 中也是如此,接收方一段时间之后,没有收到对方的消息,就会出发“心跳包”来询问对方情况

心跳包也是不携带应用层数据的特殊数据包,周期性,没有心跳则视为对方挂了

即使主机之间使用TCP 通信,仍然会在应用层自己设计心跳包,而不是使用 TCP的,TCP 的心跳机制周期比较长,是 s 级甚至分钟级的心跳

4.网线断开

本质上是3中的 1 和 2 结合

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐