《程序员的IM之路(一) 基础篇》

1.聊聊通信

  什么是通信?我认为,但凡是通信双方通过直接使用器官交流的方式都不是通信,只有通信双方通过某种媒介发生交流才能算通信。按照这个定义,那么,面对面交谈,隔山喊话,打手势都不属于通信的范畴。但是如果借助某种媒介来进行交流,比如纸,电波,光纤,甚至是人,只要符合借助媒介的交流方式,那就是通信。
  通信在人类社会发展历史中扮演了极其重要的角色,是人类社会发展的一个强需求,从烽火、信使到驿站,再到邮政、无线电波、电话,以及现在的卫星与网络,可以说随着人类社会进步,人们对通信的需求越来越强,依赖也越来越深,对通信的速度、质量要求越来越高。
  21 世纪最重要的是什么?是信息。而信息的传播途径,几乎就是通信。所以,得通信者得天下。看看互联网界吧,可以说所有的互联网公司都可以归纳为围绕通信做业务的企业。

2.What’s IM?

  IM,Instant Message 的简称,网络时代,最重要的通信实现,便是 IM 。大如 QQ 、微信、陌陌,小到小公司自己做的即时通讯系统,都是 IM 。 IM 不仅能做聊天工具,作为通信基础它还深入教育、金融、电商、医疗、游戏等各行各业。
  要做一个稳定、可靠、高并发的 IM 并不是一件易事,这不光涉及到巨大的研发成本,还有不菲的硬件成本,维护成本。所以通常只有大公司才会研发搭建自己的 IM ,而中小型公司通常会采用市面上成熟的商业化 IM 产品,比如网易云信,融云,环信。

3.基础知识介绍

1 TCP 与 UDP 介绍

TCP 和 UDP 都是传输层协议,它们有不同的优缺点,也适用于不一样的场景。

TCP UDP
可靠性 TCP 有握手机制和流量控制机制、重传机制,它是可靠的, 而 UDP 不可靠
是否建立连接 TCP 在传输数据前,会建议一条虚拟连接,而 UDP 不需要。
资源占用 服务器需要维护大量 TCP 连接,且 TCP 连接状态重,所以 TCP 在资源占用上高于 UDP 。
传输速度 UDP 传输前不需要握手,所以速度比 TCP 更快。

1 TCP 的连接状态

  TCP 的连接状态共有 11 种,要理解 TCP 连接的建立关闭,将连接状态结合在一起理解,收益更大。
  
  下面是一张非常著名的 TCP 状态变化图,出自《TCP/IP详解》一书。三次握手,四次挥手,同时打开,同时关闭都可以在图中推演出来。
  
《程序员的IM之路(一) 基础篇》_第1张图片

状态 说明
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 可用状态了。

2 TCP连接的建立与关闭

  众所周知,TCP连接的建立,要经过三次握手,而关闭则需要经过四次握手,如图所示:
《程序员的IM之路(一) 基础篇》_第2张图片

建立TCP连接

  • 第一次握手:客户端(主动方)发起一个 TCP 连接请求,给服务端(被动方)发送一个 SYN 报文,此时客户端的状态变为SYN_SENT 。
  • 第二次握手:服务端收到 SYN 报文后,向客户端同时发送一个SYN 报文和 ACK 报文(通常这两个报文会合并成一个报文),此时服务端的状态变为 SYN_RCVD 。
  • 第三次握手:客户端收到服务端发出的 SYN & ACK 报文后,进入 ESTABLISHED 状态,同时发送最后一个 ACK 报文给服务端。服务端收到最后一个 ACK 报文后,也进入了 ESTABLISHED 状态。
    至此,连接双方都进入 ESTABLISHED 状态,便可以开始传输数据了。

关闭 TCP 连接

  • 第一次握手:客户端(关闭主动方)向服务端(关闭被动方)发送 FIN 报文。客户端进入 FIN_WAIT_1 状态。
  • 第二次握手:服务端收到 FIN 报文,向客户端发送 ACK 报文,自身进入 CLOSE_WAIT 状态.而客户端收到 ACK 报文后,进入 FIN_WAIT_2 状态。
  • 第三次握手:服务端向客户端发送 FIN 报文,进入 LAST_ACK 状态。
  • 第四次握手:客户端收到 FIN 报文,同时向服务端发送 ACK 报文,自身进入 TIME_WAIT 状态,在等待 2MSL 时间后CLOSED 。而服务端收到最后一个 ACK 后,也进入 CLOSED 。

思考

  • 建立连接时,如果最后一个 ACK 服务端没有收到会怎样?
  • 为什么建立连接要握手 3 次,而不是 2 次?
  • 为什么关闭连接时要多一次握手?
  • 为什么处于 TIME_WAIT 状态下,要等待 2 个 MSL ?
  • 如何解决服务器堆积的大量 timewait 和 closewait 问题?

3 UDP的介绍

  UDP 是一种无连接面向报文的传输协议,相较于 TCP 而言,简单同时更快速。 UDP 与 TCP 相比,优势在于:

  • 快速。不需要复杂的握手机制。
  • 开销小。因为无连接,不需要维护各种连接状态,所以开销小。
  • 实时性强。由于 TCP 复杂的拥塞控制算法以及重传机制,导致网络拥堵或者丢包时会停止发送或者减少发送带来更多的延迟。而 UDP 则没有这些顾虑,最多只需要实现自己的重传机制。所以特别适合视频、语音、游戏等场景。

但这个地球上的互联网使用的协议比例上, UDP 还是不及 TCP ,原因就是因为 UDP 的简单,而导致在可靠性上输给了 TCP 。这个世界上,目前还是需求精确的场景多一些。比如 WWW 就是建立在 TCP 协议之上,而不是 UDP 。

4. IM 核心技术介绍

1 TCP 长连接的保活

  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 长连接保活问题,归纳起来,就是心跳的设计和长连接的重建。

1 心跳设计

  • 前台活跃态。在 IM 应用处于活跃态时,为了保持消息的及时性,可采用固定较短间隔心跳。如果用户有发送消息,也可以动态的把心跳报文与消息报文结合起来发送。
  • 先快后慢。在连接建立的初始阶段,采取快速心跳策略,待连接稳定一段时间后,采用慢速心跳策略。一来节省流量与耗电量,二来减轻服务器压力。
  • 后台休眠。在 IM 应用切换到后台后,心跳应当逐渐减慢直到停止跳动。停止跳动后,如何接收消息呢?那就要用到推送代理机制了。一旦应用进入休眠状态,就为自己开启一个推送代理。如果有消息过来,推送通知栏会有一条通知,当用户点开通知的时候便唤醒应用。 IOS 系统可以使用 APNS 代理,而安卓系统,像华为、小米也都是自带推送系统,并且对第三方开放。

2 长连接的重建

  • 网络环境切换。一旦检测到 4G / 3G / 2G 与 WIFI 信号切换事件,那么长连接必须重建。
  • DHCP 租期耗尽。 DHCP 租期耗尽时, IP 地址可能会发生变化。一旦检测到 IP 地址发生变化,长连接需要重建。

3 小结

TCP 长连接的保活还涉及到许多方面,尤其是安卓系统厂商各自为政, GCM 在国内又不可用的背景下,更是需要注重许多细节。比如,电量低或者内存满的时候,内核会杀掉一些进程,用户也会手动终止一些进程。如果恰巧杀掉的是你的 APP,如果针对这些情况不做特殊处理的话,你的 IM 就无力回天了。

2 连接层与业务层的解耦

  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 、微信这种级别,一旦重启完毕,那就是洪水猛兽了。
  

3 集群

  一个系统若是要支持大规模用户的访问和高并发处理,那么集群是必不可少的。

1 连接层的集群

  一旦做集群,那么长连接必不可少的分配到各台服务器上,不同服务器上不同的用户如何通信呢?
  首先我们需要一台登陆服务器,每个客户端连接上来的时候,都需要这台服务器来分配一台连接服务器,同时维护用户与其所在连接服务器的关系表。用户并不连接这台登陆服务器,而是根据登陆服务器所返回的连接服务器的 IP 与 PORT ,与连接服务器建立起长连接。
  这儿有个优化点, IM 系统中,对实时性要求最高的,通常是地理位置相近两个用户,而远距离通信的双方,通常对秒级的延迟并不敏感。所以,登陆服务器需要根据 IP 区分,把地理位置相近的用户分配到相同的连接服务器上。
  其次,我们还需要一个消息中间件。如果通信双方不在同一台连接服务器上,那么就需要消息中间件来中转消息,目标用户所在的连接服务器接收到消息后,将消息再转发给目标用户。
  连接服务器之间要做集群,登陆服务器也应当做一个集群,并且,登陆服务器还需要做负载均衡处理。

2 业务层的集群 

  最耗费处理时间的是业务层,所以业务层一定要做分布式集群。业务层的集群是最简单的集群,可以直接借助 zookeeper 来实现。

4 数据去中心化      

我们先看看分布式系统理论中经典的 CAP 原则,所谓 CAP :
- Consistency ,强一致性
即在分布式系统中的同一数据多副本情形下,对于数据的更新操作体现出的效果与只有单份数据是一样的。
- Avalilability , 可用性
客户端在任何时刻对大规模数据系统的读/写操作都应该保证在限定延时内完成;
- Partition Tolerance , 分区容忍性
在大规模分布式数据系统中,网络分区现象,即分区间的机器无法进行网络通信的情况是必然发生的,所以系统应该能够在这种情况下仍然继续工作。
  
  CAP 原则说的是,对于一个大规模分布式数据系统来说,CAP 三要素是不可兼得的,同一系统至多只能实现其中的两个,而必须放宽第 3 个要素来保证其他两个要素被满足。一般在网络环境下,运行环境出现网络分区是不可避免的,所以系统必须具备分区容忍性 (P) 特性,所以在一般在这种场景下设计大规模分布式系统时,往往在 AP 和 CP 中进行权衡和选择。
  银行业务,订单业务,预约业务这些业务即便是在分布式系统中,也必须要保证数据的强一致性。而 IM 系统,消息的延迟写入,不同步写入绝对是可以接受的。所以 IM 系统的数据中心可以去中心化,实现多数据中心。
  为什么 IM 要去中心化?因为 IM 中,系统的写入是非常频繁的,而且随着移动网络的切换,用户需要不停上线下线,接收离线消息,漫游消息。做去中心化的设计,便于提高读写并发能力。
  

5.系统架构

《程序员的IM之路(一) 基础篇》_第3张图片

  • 用户在登陆服务器登陆后,获取最适配的连接服务器的地址与端口,然后与连接服务器建立长连接。
  • 连接服务器通过消息中间件将消息发送给业务服务器处理
  • 跨连接服务器通信双方通过数据中心中的路由表找到对方连接所在服务器进行通信。
  • 用户的实时消息由连接服务器投递,离线消息、漫游消息、好友列表、最近通信列表以 http 方式从登陆服务器->数据中心获取。
  • 服务器产生的日志通过消息中间件传递给日志服务器进行记录

6.技术选型

  • 服务器编程语言采用 Java 。
  • 使用 Netty 作为网络服务器框架。其异步 IO 和事件驱动模型能支持大规模并发连接处理。
  • 采用 zookeeper 管理连接服务器和业务服务器的集群部署
  • 采用 nginx 或者 HA 作为负载均衡方案
  • 使用 SpringMVC 开发登陆服务器
  • 使用 MySQL、Redis 作为数据中心
  • 使用RabbitMQ作为消息中间件。(备选方案 Redis 、 ZeroMQ )
  • 采用自定义消息数据格式,使用 protobuf 压缩数据。
  • 采用 RSA 密钥交换, AES 对消息进行加解密。每 n 毫秒进行AES 密钥交换。

你可能感兴趣的:(IM,TCP,UDP)