一、报文类型
im的客户端与服务器通过发送报文(也就是请求包)来完成消息的传递,报文分为三种,请求报文(request,后简称为为R),应答报文(acknowledge,后简称为A),通知报文(notify,后简称为N),这三种报文的解释如下:
R:客户端主动发送给服务器的报文
A:服务器被动应答客户端的报文,一个A一定对应一个R
N:服务器主动发送给客户端的报文
二、普通消息投递流程
用户A给用户B发送一个“你好”,很容易想到,流程如下:
1)client-A向im-server发送一个消息请求包,即msg:R
2)im-server在成功处理后,回复client-A一个消息响应包,即msg:A
3)如果此时client-B在线,则im-server主动向client-B发送一个消息通知包,即msg:N(当然,如果client-B不在线,则消息会存储离线)
三、上述消息投递流程出现的问题
从流程图中容易看到,发送方client-A收到msg:A后,只能说明im-server成功接收到了消息,并不能说明client-B接收到了消息。在若干场景下,可能出现msg:N包丢失,且发送方client-A完全不知道,例如:
a、服务器崩溃,msg:N包未发出
b、网络抖动,msg:N包被网络设备丢弃
c、client-B崩溃,msg:N包未接收
结论是悲观的:接收方client-B是否有收到msg:N,发送方client-A完全不可控,那怎么办呢?
四、应用层确认+im消息可靠投递的六个报文
upd是一种不可靠的传输层协议,tcp是一种可靠的传输层协议,tcp是如何做到可靠的?答案是:超时、重传、确认。
要想实现应用层的消息可靠投递,必须加入应用层的确认机制,即:要想让发送方client-A确保接收方client-B收到了消息,必须让接收方client-B给一个消息的确认,这个应用层的确认的流程,与消息的发送流程类似:
4)client-B向im-server发送一个ack请求包,即ack:R
5)im-server在成功处理后,回复client-B一个ack响应包,即ack:A
6)则im-server主动向client-A发送一个ack通知包,即ack:N
至此,发送“你好”的client-A,在收到了ack:N报文后,才能确认client-B真正接收到了“你好”。
会发现,一条消息的发送,分别包含(上)(下)两个半场,即msg的R/A/N三个报文,ack的R/A/N三个报文,一个应用层即时通讯消息的可靠投递,共涉及6个报文,这就是im系统中消息投递的最核心技术(如果某个im系统不包含这6个报文,不要谈什么消息的可靠性)。
小结
1)client-A向im-server发送一个消息请求包,即msg:R
2)im-server在成功处理后,回复client-A一个消息响应包,即msg:A
3)如果此时client-B在线,则im-server主动向client-B发送一个消息通知包,即msg:N(当然,如果client-B不在线,则消息会存储离线)
4)client-B向im-server发送一个ack请求包,即ack:R
5)im-server在成功处理后,回复client-B一个ack响应包,即ack:A
6)则im-server主动向client-A发送一个ack通知包,即ack:N
至此,发送“你好”的client-A,在收到了ack:N报文后,才能确认client-B真正接收到了“你好”。
参考文章:即时通讯软件如何保证消息的不重复和不丢失
五、TCP粘包现象
1、为什么会出现粘包现象
TCP是一个可靠的传输协议,它是面向连接(3次握手成功了才能开始传输)。
TCP 协议是流式协议。那么这句话到底是什么意思呢?所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要我们人为地去给这些协议划分边界。那什么是粘包?所谓粘包就是连续给对端发送两个或者两个以上的数据包,对端在一次收取中可能收到的数据包大于 1 个,大于 1 个,可能是几个(包括一个)包加上某个包的部分,或者干脆就是几个完整的包在一起。(简单的来说就是TCP是流协议,几个数据包是一起发送过去被接收到的。一波流嘛!!)
有的面试官可能会这么问:网络通信时,如何解决粘包、丢包或者包乱序问题?这个问题其实是面试官在考察面试者的网络基础知识,如果是 TCP 协议,在大多数场景下,是不存在丢包和包乱序问题的。TCP 通信是可靠通信方式,TCP 协议栈通过序列号和包重传确认机制保证数据包的有序和一定被正确发到目的地。那么面试官这么问你,其实就是要你回到怎么解决粘包的问题?
2、解决策略
简单来说几时接收端在接收到一波流数据之后,怎么把这波流数据中去区分出那是几个数据包,包与包之间的边界怎么去界定?解决了界定问题的过程就是所谓的拆包过程。那么如何区分界定呢?目前主要有三种方法:
- 固定包长的数据包
顾名思义,即每个协议包的长度都是固定的。举个例子,例如我们可以规定每个协议包的大小是 64字节。接收端每次收满 64 个字节,就取出来解析(如果不够,就先存起来)。这种通信协议的格式简单但灵活性差。如果包内容不足指定的字节数,剩余的空间需要填充特殊的信息。如果包内容超过指定字节数,又得分包分片,需要增加额外处理逻辑——在发送端进行分包分片,在接收端重新组装包片。(分包和分片的逻辑又是另一套了) - 以指定字符(串)为包的结束标志
这种协议包比较常见,即字节流中遇到特殊的符号值时就认为到一个包的末尾了。例如,我们熟悉的 FTP协议,发邮件的 SMTP 协议,一个命令或者一段数据后面加上"\r\n"(即所谓的 CRLF)表示一个包的结束。对端收到后,每遇到一个”\r\n“就把之前的数据当做一个数据包。其不足之处就是如果协议数据包内容部分需要使用包结束标志字符,就需要对这些字符做转码或者转义操作,以免被接收方错误地当成包结束标志而误解析。 - 包头 + 包体格式
这种格式的包一般分为两部分,即包头和包体,包头是固定大小的,且包头中必须含有一个字段来说明接下来的包体有多大。
struct msgHeader{
int32_t bodySize; // 表示消息体有多大。
int32_t cmd;
};
这就是一个典型的包头格式,bodySize 指定了这个包的包体是多大。由于包头大小是固定的(这里是 size(int32_t) + sizeof(int32_t) = 8 字节),对端先收取包头大小字节数目(当然,如果不够还是先缓存起来,直到收够为止),然后解析包头,根据包头中指定的包体大小来收取包体,等包体收够了,就组装成一个完整的包来处理。
但是假如传输的是一个文件,而不是变长的格式化数据,其实也没有所谓的分包现象,因为这一波流都是一个文件的内容,全部接收就好了。