什么是通信?我认为,但凡是通信双方通过直接使用器官交流的方式都不是通信,只有通信双方通过某种媒介发生交流才能算通信。按照这个定义,那么,面对面交谈,隔山喊话,打手势都不属于通信的范畴。但是如果借助某种媒介来进行交流,比如纸,电波,光纤,甚至是人,只要符合借助媒介的交流方式,那就是通信。
通信在人类社会发展历史中扮演了极其重要的角色,是人类社会发展的一个强需求,从烽火、信使到驿站,再到邮政、无线电波、电话,以及现在的卫星与网络,可以说随着人类社会进步,人们对通信的需求越来越强,依赖也越来越深,对通信的速度、质量要求越来越高。
21 世纪最重要的是什么?是信息。而信息的传播途径,几乎就是通信。所以,得通信者得天下。看看互联网界吧,可以说所有的互联网公司都可以归纳为围绕通信做业务的企业。
IM,Instant Message 的简称,网络时代,最重要的通信实现,便是 IM 。大如 QQ 、微信、陌陌,小到小公司自己做的即时通讯系统,都是 IM 。 IM 不仅能做聊天工具,作为通信基础它还深入教育、金融、电商、医疗、游戏等各行各业。
要做一个稳定、可靠、高并发的 IM 并不是一件易事,这不光涉及到巨大的研发成本,还有不菲的硬件成本,维护成本。所以通常只有大公司才会研发搭建自己的 IM ,而中小型公司通常会采用市面上成熟的商业化 IM 产品,比如网易云信,融云,环信。
TCP 和 UDP 都是传输层协议,它们有不同的优缺点,也适用于不一样的场景。
TCP | UDP |
---|---|
可靠性 | TCP 有握手机制和流量控制机制、重传机制,它是可靠的, 而 UDP 不可靠 |
是否建立连接 | TCP 在传输数据前,会建议一条虚拟连接,而 UDP 不需要。 |
资源占用 | 服务器需要维护大量 TCP 连接,且 TCP 连接状态重,所以 TCP 在资源占用上高于 UDP 。 |
传输速度 | UDP 传输前不需要握手,所以速度比 TCP 更快。 |
TCP 的连接状态共有 11 种,要理解 TCP 连接的建立关闭,将连接状态结合在一起理解,收益更大。
下面是一张非常著名的 TCP 状态变化图,出自《TCP/IP详解》一书。三次握手,四次挥手,同时打开,同时关闭都可以在图中推演出来。
状态 | 说明 |
---|---|
CLOSED | 初始状态,表示 TCP 连接是“关闭着的”或“未打开的”。 |
LISTEN | 表示服务器端的某个 SOCKET 处于监听状态,可以接受客户端的连接。 |
SYN_SENT | 表示客户端第一次握手发送 SYN 报文后的状态。 |
SYN_RCVD | 表示服务端在第二次握手时,接收到了 SYN 报文的状态。当 TCP 连接处于此状态时,再收到客户端的 ACK 报文,它就会进入到ESTABLISHED 状态。 |
ESTABLISHED | 表示 TCP 连接已经成功建立。 |
FIN_WAIT_1 | 主动关闭方关闭连接时发送 FIN 报文后的状态。 |
FIN_WAIT_2 | 进入 FIN_WAIT_1 后,收到对方 ACK 报文,进入FIN_WAIT_2 状态。如果对方一直不 close() ,那么 FIN_WAIT_2 会一直保持,直到系统重启。 |
TIME_WAIT | 表示收到了对方的 FIN 报文,并发送出了 ACK 报文。 TIME_WAIT 状态下的 TCP 连接会等待 2*MSL(Max Segment Lifetime ,最大分段生存期,指一个 TCP 报文在 Internet 上的最长生存时间。每个具体的 TCP 协议实现都必须选择一个确定的 MSL 值, RFC 1122 建议是 2 分钟,但 BSD 传统实现采用了 30 秒,Linux 可以 cat /proc/sys/net/ipv4/tcp_fin_timeout 看到本机的这个值),然后即可回到 CLOSED 可用状态了。如果FIN_WAIT_1 状态下,收到了对方同时带 FIN 标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无须经过 FIN_WAIT_2 状态。 |
CLOSING | 表示双方都正在关闭 SOCKET 连接。当双方几乎在同时关闭一个 SOCKET 的时候,发出了自己的 FIN 报文,然后又收到了对方的FIN 报文,双方就会同时进入 CLOSING 状态。 |
CLOSE_WAIT | 表示正在等待关闭。收到对方主动关闭连接的FIN报文后,回应一个 ACK 给对方,进入 CLOSE_WAIT 状态。如果还有未发送完的数据,继续发送,等全部发送完后,再给对方发送 FIN 报文,表示:“我也要关闭连接了”。 |
LAST_ACK | 当被动关闭的一方在发送FIN报文后,等待对方的 ACK 报文的时候,就处于 LAST_ACK 状态。当收到对方的 ACK 报文后,也就可以进入到 CLOSED 可用状态了。 |
众所周知,TCP连接的建立,要经过三次握手,而关闭则需要经过四次握手,如图所示:
建立TCP连接
关闭 TCP 连接
思考
UDP 是一种无连接面向报文的传输协议,相较于 TCP 而言,简单同时更快速。 UDP 与 TCP 相比,优势在于:
但这个地球上的互联网使用的协议比例上, UDP 还是不及 TCP ,原因就是因为 UDP 的简单,而导致在可靠性上输给了 TCP 。这个世界上,目前还是需求精确的场景多一些。比如 WWW 就是建立在 TCP 协议之上,而不是 UDP 。
IM 通信必须支持双方同时发送和同时接收,那么全双工的 TCP/IP 协议无疑是最适合 IM 的。
TCP 规范中,并没有设计保活机制,原因有三个,
1)在出现短暂差错的情况下,这可能会使一个非常好的连接释放掉;
2)它们耗费不必要的带宽
3)在按分组计费的情况下会在互联网上花掉更多的钱。
而多年来的经验也告诉我们,TCP 双方一旦建立起了连接,只要双方主机不重启,十天八天后,双方依旧可以通信。但在这十天八天中,双方并没有任何保活消息的往来。
既然 TCP 长连接保活机制不是必要的,那为什么 IM 必须得设计 TCP 长连接的保活呢。这得从移动网络说起。
传统的网络环境是固定的,设备是固定设备,IP 一般也不会变化,尤其是政企局域网。一旦 TCP 连接建立,只要通信双方设备不重启, IP 不变化,那么这条连接是一直可用的。
随着移动互联时代的到来,智能手机、 PAD 占领了全世界,它们在移动的过程中,不断切换着 4G / 3G / 2G 与 WIFI 信号,每一次信号的切换,都意味着 IP 的变化。 IP 的不断变化,将会导致原有的 TCP 长连接失效。所以, IM 必须依靠一个保活的TCP 长连接来适应这个世界。
另外, NAT 超时也是不得不考虑到一个因素。运营商在长连接一段时间没有数据时,会淘汰 NAT 表中的对应项,造成链路中断。
解决 TCP 长连接保活问题,归纳起来,就是心跳的设计和长连接的重建。
TCP 长连接的保活还涉及到许多方面,尤其是安卓系统厂商各自为政, GCM 在国内又不可用的背景下,更是需要注重许多细节。比如,电量低或者内存满的时候,内核会杀掉一些进程,用户也会手动终止一些进程。如果恰巧杀掉的是你的 APP,如果针对这些情况不做特殊处理的话,你的 IM 就无力回天了。
IM 服务器端,维护 TCP 长连接的层叫连接层,负责业务处理的层叫业务层。连接层只负责消息的收与发,而业务处只处理消息的业务相关操作。如果你的服务器设计的目标是容纳 10 万以下的并发连接,那么完全不必要做连接层与业务层的解耦。(这儿就引入了 IM 服务器一个很重要的指标,单机并发连接数。)
单机并发连接数指的是,单台服务器同时可容纳的 TCP 连接数。
单机并发连接数是有上限的。我们知道 TCP 连接是建立在 PORT 到 PORT 之上的,端口总共有 65535 个,而 0-1024 是属于系统保留端口,留给我们的端口号只有 1024-65535 ,总共只有 6 万多端口,那又如何能支持百万连接呢。
这儿就有一个认识上的误区- TCP 连接数受限于端口号。事实上,端口号对于 TCP 连接数上限只是一个必要条件,而不是必须条件。除了端口号,还有一个条件,是 IP 地址。
TCP 连接标识符是这样描述的: { local ip, local port,remote ip,remote port } 。不光取决于本机的 PORT ,也取决于对方的 IP 和端口。那么我们可以大致算一个简单的理论上限, LIMIT = 本机端口数( 2^10 )×对方 IP2 ( 2^32 )×对方端口数( 2^10 )= 2^48 。这个数字可是亿亿级别的数字啊( PS :不考虑不可用 IP 和系统保留端口)。
另外,操作系统也会影响到最大连接数。在 linux 系统中,一个 tcp 连接占用一个文件描述符,所以你得调整 linux 进程最大打开句柄数, ulimit -n 。如果你的目标是 100 万最大连接,那么将 n 设置为 1000000 。
实际情况下,最终决定单机并发连接数的指标是服务器的可用内存。可用内存越高,连接数就越大。所以,为了保持一个可观的最大连接数,同时能让 CPU 并发处理,必须解耦连接层与业务层,解放内存与 CPU 。
另外一个原因是为了不重启实现业务处理代码发布。试想一下,如果连接层与业务层耦合在一起,需要发布最新的业务处理代码,势必要重启服务器,导致所有长连接断开。如果用户量小的话,也没多大关系,如果是 QQ 、微信这种级别,一旦重启完毕,那就是洪水猛兽了。
一个系统若是要支持大规模用户的访问和高并发处理,那么集群是必不可少的。
一旦做集群,那么长连接必不可少的分配到各台服务器上,不同服务器上不同的用户如何通信呢?
首先我们需要一台登陆服务器,每个客户端连接上来的时候,都需要这台服务器来分配一台连接服务器,同时维护用户与其所在连接服务器的关系表。用户并不连接这台登陆服务器,而是根据登陆服务器所返回的连接服务器的 IP 与 PORT ,与连接服务器建立起长连接。
这儿有个优化点, IM 系统中,对实时性要求最高的,通常是地理位置相近两个用户,而远距离通信的双方,通常对秒级的延迟并不敏感。所以,登陆服务器需要根据 IP 区分,把地理位置相近的用户分配到相同的连接服务器上。
其次,我们还需要一个消息中间件。如果通信双方不在同一台连接服务器上,那么就需要消息中间件来中转消息,目标用户所在的连接服务器接收到消息后,将消息再转发给目标用户。
连接服务器之间要做集群,登陆服务器也应当做一个集群,并且,登陆服务器还需要做负载均衡处理。
最耗费处理时间的是业务层,所以业务层一定要做分布式集群。业务层的集群是最简单的集群,可以直接借助 zookeeper 来实现。
我们先看看分布式系统理论中经典的 CAP 原则,所谓 CAP :
- Consistency ,强一致性
即在分布式系统中的同一数据多副本情形下,对于数据的更新操作体现出的效果与只有单份数据是一样的。
- Avalilability , 可用性
客户端在任何时刻对大规模数据系统的读/写操作都应该保证在限定延时内完成;
- Partition Tolerance , 分区容忍性
在大规模分布式数据系统中,网络分区现象,即分区间的机器无法进行网络通信的情况是必然发生的,所以系统应该能够在这种情况下仍然继续工作。
CAP 原则说的是,对于一个大规模分布式数据系统来说,CAP 三要素是不可兼得的,同一系统至多只能实现其中的两个,而必须放宽第 3 个要素来保证其他两个要素被满足。一般在网络环境下,运行环境出现网络分区是不可避免的,所以系统必须具备分区容忍性 (P) 特性,所以在一般在这种场景下设计大规模分布式系统时,往往在 AP 和 CP 中进行权衡和选择。
银行业务,订单业务,预约业务这些业务即便是在分布式系统中,也必须要保证数据的强一致性。而 IM 系统,消息的延迟写入,不同步写入绝对是可以接受的。所以 IM 系统的数据中心可以去中心化,实现多数据中心。
为什么 IM 要去中心化?因为 IM 中,系统的写入是非常频繁的,而且随着移动网络的切换,用户需要不停上线下线,接收离线消息,漫游消息。做去中心化的设计,便于提高读写并发能力。