IM 和 Socket 的关系及 Heart 的必要性

很多互联网服务都会开通 IM 功能,如好友通讯,用户反馈,在线推送等。在微信未盛行,QQ 大行其道的年代,IM 还属于高深技术,目前已经进入寻常百姓家了。
其实 IM 本身整个技术流程从终端角度来看不是一个难以理解的问题。但因为诸多人对网络协议不了解,所以认为 IM 难以入门。

首先要说 IM 是一个什么样的形态,它和 HTTP/S 有什么不同。
HTTP 解决了端到端通讯的问题,在互联网大放异彩的原因是浏览器的盛行。冲浪需要网络获取服务端资源,HTTP 可以使得终端发起网络请求服务端返回资源结果实现资源互通。
我们的感知里面,终端发起网络请求,服务端回复网络请求,HTTP 使命就完成了。所以很多人对于 HTTP 的认知是:一次性通讯,每次需要获取信息都要重新发起请求。
其实有这样的认知是完全正确的,因为 HTTP 的确就是这样设计的。这里要着重说到设计这个词,因为 HTTP 就是为了完成一次性通讯这么一个任务。而像 HTTP 这样的应用层协议还有非常多,比如 FTP、RTMP 等等。他们都是为了完成特定任务而实现的协议。所有应用层协议都是被设计过的,为了完成特定的使命。

但是随着社交关系的复杂,如何实现即时通讯又是摆在眼前的问题。HTTP 是一次性通讯,请求返回后,终端和服务端两不相欠,也感知不到对方的存在。
如果我们想通过 HTTP 实现即时通讯,那就需要客户端每隔 N 秒 (比如 2s) 向服务器发送一个请求,然后服务器返回新消息。这虽然很简陋,但是我就经历过这样的方案设计,我认为,在特定场景、功能、需求的前提下,这样的技术方案设计是完全合理的。
HTTP 显然不适合大量用户下的即时通讯场景。在社交网络中,服务端需要在线数据统计,需要消息及时在线推送到终端,需要即使感知到终端连接是否中断好清理服务器资源,而这些,显然 HTTP 都无能为力。
那如何实现真正的即时通讯呢?真正的即时通讯和 HTTP 通讯在技术原理层面有哪些区别呢?

Socket 是什么

很多人都认为 Socket 是一套协议。其实根本就没有 Socket 协议存在。但是话又说回来,Socket 本身的确又有其他两个身份。如果有人非要说 Socket 是协议,那 Socket 就有三个身份了。

Socket 库

互联网本来就是数据的传输。传输层是有专门的协议负责数据传输,就是 TCP 它们。
可是应用层 HTTP 怎么和传输层对接呢?就需要很多个接口。而这些接口处于一个库中,没错,这个库就叫做 Socket 库
Socket 库提供了 write、read 接口用来从缓冲区读取数据,提供了 connect 接口用来建立连接,此外,Scoket 还有很多其他接口如 close、gethostbyname 等。

1
2
3
这里要特别说明一下gethostbyname接口,因为它太重要了。gethostbyname是用来做域名解析用的,没错,域名转成IP是Scoket库提供的接口完成的。
所有网络请求,不管是局域网还是广域网,都需要知道对方的IP才能够发送消息,这是七层网络模型决定的。但是我们一般都只知道域名,在网络请求的时候,就需要做一次域名和IP的转换。
这个域名解析是操作系统来实现的,操作系统发现请求是域名,就调用gethostbyname接口进行系统中断,然后Socket库经过一系列的递归发送UDP数据报来查询域名对应的IP,最后操作系统返回该IP,应用层恢复系统中断,开始继续执行。

所以 Socket 库,就是一个 Api 接口的实现合集,它连接了应用层和传输层。应用层调用 Socket 库接口,把数据给到传输层,后面数据才能够发出去。

Socket 套接字

互联网上两个终端 (比如客户端和服务器) 如果需要通讯,就需要知道对方的 IP 和 Port (端口)。这是由网络七层模型决定的。
IP 通过 gethostbyname 已经拿到了,Port 应用层在请求的时候已经知道,比如 HTTP 默认 80,HTTPS 默认 443,还有自定义 8889 等。
那知道对方的 IP 和 Port 后存储在哪里呢?就是一个文件里面,更接地气的说,叫套接字,Socket 翻译过来也就是套接字。
终端要发送请求到服务端,首先需要在终端建立自己的套接字,存储服务端的 IP 和 Port。服务端要接收终端的消息,也要在服务端建立一个套接字,存储终端的 IP 和 Port。
终端发送 HTTP 消息给服务端后,服务端经过数据库查询等处理,要把数据返回给终端,就需要读取服务端的套接字拿到终端的 IP 和 Port,数据通过七层模型,才能把消息回复给终端。

套接字除了找到对方的 IP 和 Port,还有其他用途吗?很多用途。套接字主要做信息存储,这也合符文件的本质。所以套接字里面存储了很多东西,通讯过程中需要的信息都会存储在套接字中。下面举两个例子:
一个例子是 TCP 三次握手。TCP 本来就是一个传输通道层协议,在调用 Socket 库里面的 connect 接口的时候,开始进行三次握手协商。但是 TCP 仅仅是一个协议,握手握了几次肯定是需要内存或者磁盘存储的,这里就是把三次握手状态 (如连接中,连接完成) 存储到了套接字中。
更确切的说,是 connect 接口把三次握手状态存储到了套接字中。
因为 TCP 仅仅是一个传输层协议,用来传输数据,它的工作仅仅是更高效率的传输数据而已。而数据所表达的含义,TCP 是不管的。握手状态明显是数据含义,相对 TCP 而言处理数据是业务层的工作,而 connect 这时就是 TCP 的业务方。
三次握手的状态写到套接字后,所有的数据传输,都会先从套接字中看一下当前握手状态,发现已经连接完成,那么就可以放心的发送数据了。
还有一个例子是 TCP 的窗口信息。TCP 在稳定连接、顺序传输、丢包控制、流量控制、拥塞控制方面是碾压 UDP 的。上面的三次握手就是在做稳定连接的工作,而其他四个方面就是通过滑动窗口拥塞窗口实现的。在这两个窗口的实现里面,需要记录已经发送包的序号、对方已接收的包序号、当前还能发送包的量等等各种信息,通过这些信息,TCP 实时更新自己的发送策略,做到了稳定性数据传输。
显然,套接字里面能存储很多东西,而且,套接字主要的用途,是为数据传输提供信息判断的能力。

Socket 到底是什么?

其实上面 Socket 库、Socket 套接字这两个身份,都不常被人提起,因为这两个身份比较基础,很多人不愿意去理解他们。
但是千万不要理解 Socket 是一个协议,这是无稽之谈。因为这两个身份很多人不愿意去理解,Socket 协议又不存在,所以如果要向别人介绍什么是 Socket,我更愿意叫它 “Socket 中间件”。这是我自己瞎起的名字,但是不会像 Socket 协议那样让人误解。

Socket 库身份的时候,它是连接应用层和传输层的纽带,是一个中间件。
Socket 套接字的时候,它保存了数据传输过程中的各种有效参数,也是一个中间件。

HTTP 和 Socket 的关系

说 HTTP 和 Socket 的关系,其实说的就是 HTTP 和 Socket 库的关系。
和 HTTP 直接关联的是 Socket 库,HTTP 要进行域名解析,数据传输,数据接收,断开连接的时候,都需要通过调用 Socket 库的相关接口才能操作下去。
这样说来,其实 HTTP 和 TCP 关联就不大了。为什么这么说呢?如果哪一天,HTTP 说要用 UDP 协议,那么在调用 Socket 库相关接口的时候,传递一个参数告知使用 UDP 就可以了。
实际上,HTTP 使用 TCP 协议的确是这么实现的,HTTP 调用 Socket 库的时候,传递了一个参数告知使用 TCP,然后才会建立 TCP 稳定连接

TCP、UDP 和 Socket 的关系

说 TCP、UDP 和 Socket 的关系,其实说的就是它们和 Socket 套接字的关系。
因为 Socket 库把数据传递到传输层后,就不管了,后面都是传输层的事情了。传输层在进行三次握手、窗口策略等过程中,需要不断和 Socket 套接字进行存储和读取。
TCP 和 UDP 都和 Socket 有一个共同的关系,那就是连接套接字的建立 (UDP 可以不建立,下面细说)。
当 TCP 需要传输数据的时候,就需要通过 connect 建立一个套接字,套接字里面有 IP 和 Port,然后进行三次握手在套接字里面记录状态。
总体流程为:建立套接字 -> 三次握手记录状态 -> 发送数据流 A-> 接收数据流 A’-> 发送数据流 B-> 接收数据流 B’-> 销毁套接字,释放资源。
当 UDP 需要传输数据的时候,可以通过 connect 建立一个套接字,套接字里面有 IP 和 Port,然后直接发送数据。
流程为:Socket ()->connect->send (A)->recv (A’)->send (B)->recv (B’)->close。
但是 UDP 也可以不 connect,下面是区别:
流程为:Socket ()->sendto (A)->recvfrom (A’)->close->Socket ()->sendto (B)->recvfrom (B’)->close。
UDP 在 connect 后,可以建立套接字,这样不用每次都建立连接,发起请求的时候直接去套接字里面拿端口和 IP。如果不 connect,只能发送消息的时候,自行附带 IP,使用 sendto () 接口。

即时通讯 IM 到底是啥?

所以即时通讯,就是编写业务层代码 (自己写或者使用 WebSocket 等应用层协议),使用 UDP 或者 TCP 这样的传输层协议,然后通过 Socket 套接字确定对方的 IP 和 Port,最后通过 Socket 库接口双向发送消息
这也就是 IM 的本质。
HTTP 一直都是单向发送消息,比如客户端登陆或者调服务端 Api。这都是单向的。
其实这个单向是宏观上看的,微观上,客户端请求,服务端返回,这其实也是双向的。
但是 HTTP 因为本身的业务关系,在收到请求后,发起请求的一方就主动关闭了通道,即销毁了双方的 Socket 套接字。所以服务端就没法再发消息给终端了,只能等着下次终端再次发起强求,才能再次返回数据给终端,如此反复。
所以 HTTP 不能做到双向通讯,是由 HTTP 本身协议特性决定的。如果 HTTP 不主动断开,那 HTTP 也能实现即时通讯实现双向发送消息

1
2
3
4
!!!HTTP1.0,消息往返后request主动关闭并销毁双方套接字,通道关闭。
!!!HTTP1.1,可以通过开启connection:keepalive,用于保持双方套接字。这个时候,因为通道还在,是可以做即时通讯用途的。
!!!HTTP2.0,在1.1的基础上,在传输层做更大程度优化,如多路复用、数据压缩等。这个时候,还是没有关闭销毁套接字,所以也可以做即时通讯使用。
!!!一般没有人使用HTTP来做即时通讯。但是实际上是可以通过HTTP来实现即时通讯的。

实际上,WebSocket 就是这样实现的。WebSocket 和 HTTP 一样属于应用层协议,WebSocket 通过 HTTP 建立连接,然后不断开通道,继续使用 TCP 进行双向发送消息。

话再说回头,WebSocket 能作为 IM 协议使用,其实是 WebSocket 协议帮我们做了业务层处理操作,如封包、粘包、调用 Socket 库 Api (send ()/recv ()/connect () 等)。
如果我们不使用 WebSocket,我们就需要自行处理 Socket 库调用,数据收发,和封包粘包的处理等工作

好在各个平台开发方,都有比较出色等 Socket 三方库使用,它们就是封装了和 Socket 库交互的部分,但是封包粘包还是需要我们处理,因为这个时候没有 HTTP 或者 WebSocket 这样的业务方了,我们自己就是业务方,所以需要我们自己处理这些业务。

Heart 要解决什么问题

说到 IM,就不能不提心跳了。不管使用 TCP、UDP 还是 WebSocket,心跳都是必不可少的。
那么心跳是要解决什么问题呢?
其实心跳总结来看,就是要解决 IM 的一个本质问题:维系长链接,保持高可用

要分析这个本质,就要先说明 IM 目前遇到了什么问题。总结来看,如下 3 点:

  1. 终端网络波动太大。尤其移动端特别明显,进出电梯时候的网络屏蔽、网络供应商切换时候的 IP 更换、高速移动过程中基站的变更等等。这些都会导致套接字失效。前面说过套接字是即时通讯通道能够稳定连接的核心。网络波动会导致套接字中 IP 更换,服务端存储的套接字就失效了,需要销毁老的套接字,并创建新的套接字。
  2. NAT 超时后映射会销毁,中断通路。官方已经发布公告,IPv4 已经用完。本身 IPv4 在大陆就很紧缺。所以终端用户上网都会分配局域网 IP,通过路由器 NAT 中转到公网去。此时路由器内部会记录一个 IP 和 Port 的映射表。但是运营商为了节省资源和降低网关压力,对一段时间没有数据收发的连接,会清理掉。这时候终端和服务端都是完好的,但是中间的通路却断了。
  3. 企业等防火墙会在一段时间后关闭网络包传输。这个和 NAT 超时类似,防火墙会定时对一段时间没有数据收发对连接进行关闭,如 5 分钟。

以上 3 个问题,会导致终端和服务端无法正常通讯,导致连接不可维系。

所以心跳,就为了处理上面情况带来的影响,使得终端和服务端可以及时做如下处理:

  1. 让客户端尽快重连,保持通路。这个特别重要,如果发现中断,一定要及时重连,不然就收不到消息了。
  2. 可以让服务端尽快感知到连接的变化,清理 Socket 资源。这个是保持服务端高可用的有效途径。
  3. 告知中间设备,目前连接活跃。因为有心跳在,就有数据包通过,那么 NAT 就不会被清理,防火墙也不会屏蔽。

具体心跳的实现方式也是多样的,有很多种,比如下面这些:

  1. 应用层发送定时心跳。定时 30s、1 分钟、2 分钟、4 分钟这样的固定心率或者动态心率都有。服务器判断长时间没有心跳则销毁 Socket 等资源
  2. 开启传输层 TCP Keepalive。TCP 本身自带 keep alive,默认关闭状态。keep alive 默认每隔 2 小时发送一次传输层心跳,失败重试 9 次,超时时间为 75s,当然 TCP 的心跳可以调整。但是 TCP keep alive 有一个致命点,因为不需要应用层参与,数据包到达传输层即返回,和 ping 有些类似 (ping 都不过传输层),所以只能判断服务端内核层是否正常响应,但业务层是否崩溃不可知。如果服务器因为大批量任务处理导致拒绝服务了,TCP keep alive 是检测不到的。但是 TCP keep alive 对于感知网络是否正常非常有用,如上面的 NAT 和防火墙等。
  3. TCP Keepalive 和应用层心跳配合使用,一个用来判断网络,一个用来判断业务服务器。这样可以对 Socket 中断进行 debug,以判断是否是中间通道被关闭导致的连接中断。
  4. 智能心跳。频繁的发起心跳会导致电量、流量等浪费,智能心跳可以平衡 NAT 超时和资源节约。智能心跳根据网络环境自动调整心跳方案如动态心率、通过使用二分法等算法方案逐步逼近 NAT 超时临界点等。

所以心跳,就是为了解决网络波动、中间设备关闭通道、网线插拔、机器断电等等意外导致的链路中断。


性话题,是对人性非常有考验的话题。
产品依靠性话题可以快速提升流量,朋友因为性话题可以快速拉近距离。
经常谈性的人大部分不色,闭口不言的人却又心怀鬼胎。