一、報文類型
im的客戶端與服務(wù)器通過發(fā)送報文(也就是請求包)來完成消息的傳遞,報文分為三種,請求報文(request,后簡稱為為R),應(yīng)答報文(acknowledge,后簡稱為A),通知報文(notify,后簡稱為N),這三種報文的解釋如下:

R:客戶端主動發(fā)送給服務(wù)器的報文
A:服務(wù)器被動應(yīng)答客戶端的報文,一個A一定對應(yīng)一個R
N:服務(wù)器主動發(fā)送給客戶端的報文
二、普通消息投遞流程
用戶A給用戶B發(fā)送一個“你好”,很容易想到,流程如下:

1)client-A向im-server發(fā)送一個消息請求包,即msg:R
2)im-server在成功處理后,回復(fù)client-A一個消息響應(yīng)包,即msg:A
3)如果此時client-B在線,則im-server主動向client-B發(fā)送一個消息通知包,即msg:N(當(dāng)然,如果client-B不在線,則消息會存儲離線)
三、上述消息投遞流程出現(xiàn)的問題
從流程圖中容易看到,發(fā)送方client-A收到msg:A后,只能說明im-server成功接收到了消息,并不能說明client-B接收到了消息。在若干場景下,可能出現(xiàn)msg:N包丟失,且發(fā)送方client-A完全不知道,例如:
a、服務(wù)器崩潰,msg:N包未發(fā)出
b、網(wǎng)絡(luò)抖動,msg:N包被網(wǎng)絡(luò)設(shè)備丟棄
c、client-B崩潰,msg:N包未接收
結(jié)論是悲觀的:接收方client-B是否有收到msg:N,發(fā)送方client-A完全不可控,那怎么辦呢?
四、應(yīng)用層確認(rèn)+im消息可靠投遞的六個報文
upd是一種不可靠的傳輸層協(xié)議,tcp是一種可靠的傳輸層協(xié)議,tcp是如何做到可靠的?答案是:超時、重傳、確認(rèn)。
要想實現(xiàn)應(yīng)用層的消息可靠投遞,必須加入應(yīng)用層的確認(rèn)機制,即:要想讓發(fā)送方client-A確保接收方client-B收到了消息,必須讓接收方client-B給一個消息的確認(rèn),這個應(yīng)用層的確認(rèn)的流程,與消息的發(fā)送流程類似:

4)client-B向im-server發(fā)送一個ack請求包,即ack:R
5)im-server在成功處理后,回復(fù)client-B一個ack響應(yīng)包,即ack:A
6)則im-server主動向client-A發(fā)送一個ack通知包,即ack:N
至此,發(fā)送“你好”的client-A,在收到了ack:N報文后,才能確認(rèn)client-B真正接收到了“你好”。
會發(fā)現(xiàn),一條消息的發(fā)送,分別包含(上)(下)兩個半場,即msg的R/A/N三個報文,ack的R/A/N三個報文,一個應(yīng)用層即時通訊消息的可靠投遞,共涉及6個報文,這就是im系統(tǒng)中消息投遞的最核心技術(shù)(如果某個im系統(tǒng)不包含這6個報文,不要談什么消息的可靠性)。
小結(jié)
1)client-A向im-server發(fā)送一個消息請求包,即msg:R
2)im-server在成功處理后,回復(fù)client-A一個消息響應(yīng)包,即msg:A
3)如果此時client-B在線,則im-server主動向client-B發(fā)送一個消息通知包,即msg:N(當(dāng)然,如果client-B不在線,則消息會存儲離線)
4)client-B向im-server發(fā)送一個ack請求包,即ack:R
5)im-server在成功處理后,回復(fù)client-B一個ack響應(yīng)包,即ack:A
6)則im-server主動向client-A發(fā)送一個ack通知包,即ack:N
至此,發(fā)送“你好”的client-A,在收到了ack:N報文后,才能確認(rèn)client-B真正接收到了“你好”。

五、TCP粘包現(xiàn)象
1、為什么會出現(xiàn)粘包現(xiàn)象
TCP是一個可靠的傳輸協(xié)議,它是面向連接(3次握手成功了才能開始傳輸)。
TCP 協(xié)議是流式協(xié)議。那么這句話到底是什么意思呢?所謂流式協(xié)議,即協(xié)議的內(nèi)容是像流水一樣的字節(jié)流,內(nèi)容與內(nèi)容之間沒有明確的分界標(biāo)志,需要我們?nèi)藶榈厝ソo這些協(xié)議劃分邊界。那什么是粘包?所謂粘包就是連續(xù)給對端發(fā)送兩個或者兩個以上的數(shù)據(jù)包,對端在一次收取中可能收到的數(shù)據(jù)包大于 1 個,大于 1 個,可能是幾個(包括一個)包加上某個包的部分,或者干脆就是幾個完整的包在一起。(簡單的來說就是TCP是流協(xié)議,幾個數(shù)據(jù)包是一起發(fā)送過去被接收到的。一波流嘛?。。?br>
有的面試官可能會這么問:網(wǎng)絡(luò)通信時,如何解決粘包、丟包或者包亂序問題?這個問題其實是面試官在考察面試者的網(wǎng)絡(luò)基礎(chǔ)知識,如果是 TCP 協(xié)議,在大多數(shù)場景下,是不存在丟包和包亂序問題的。TCP 通信是可靠通信方式,TCP 協(xié)議棧通過序列號和包重傳確認(rèn)機制保證數(shù)據(jù)包的有序和一定被正確發(fā)到目的地。那么面試官這么問你,其實就是要你回到怎么解決粘包的問題?
2、解決策略
簡單來說幾時接收端在接收到一波流數(shù)據(jù)之后,怎么把這波流數(shù)據(jù)中去區(qū)分出那是幾個數(shù)據(jù)包,包與包之間的邊界怎么去界定?解決了界定問題的過程就是所謂的拆包過程。那么如何區(qū)分界定呢?目前主要有三種方法:
-
固定包長的數(shù)據(jù)包
顧名思義,即每個協(xié)議包的長度都是固定的。舉個例子,例如我們可以規(guī)定每個協(xié)議包的大小是 64字節(jié)。接收端每次收滿 64 個字節(jié),就取出來解析(如果不夠,就先存起來)。這種通信協(xié)議的格式簡單但靈活性差。如果包內(nèi)容不足指定的字節(jié)數(shù),剩余的空間需要填充特殊的信息。如果包內(nèi)容超過指定字節(jié)數(shù),又得分包分片,需要增加額外處理邏輯——在發(fā)送端進行分包分片,在接收端重新組裝包片。(分包和分片的邏輯又是另一套了) -
以指定字符(串)為包的結(jié)束標(biāo)志
這種協(xié)議包比較常見,即字節(jié)流中遇到特殊的符號值時就認(rèn)為到一個包的末尾了。例如,我們熟悉的 FTP協(xié)議,發(fā)郵件的 SMTP 協(xié)議,一個命令或者一段數(shù)據(jù)后面加上"\r\n"(即所謂的 CRLF)表示一個包的結(jié)束。對端收到后,每遇到一個”\r\n“就把之前的數(shù)據(jù)當(dāng)做一個數(shù)據(jù)包。其不足之處就是如果協(xié)議數(shù)據(jù)包內(nèi)容部分需要使用包結(jié)束標(biāo)志字符,就需要對這些字符做轉(zhuǎn)碼或者轉(zhuǎn)義操作,以免被接收方錯誤地當(dāng)成包結(jié)束標(biāo)志而誤解析。 -
包頭 + 包體格式
這種格式的包一般分為兩部分,即包頭和包體,包頭是固定大小的,且包頭中必須含有一個字段來說明接下來的包體有多大。
struct msgHeader{
int32_t bodySize; // 表示消息體有多大。
int32_t cmd;
};
這就是一個典型的包頭格式,bodySize 指定了這個包的包體是多大。由于包頭大小是固定的(這里是 size(int32_t) + sizeof(int32_t) = 8 字節(jié)),對端先收取包頭大小字節(jié)數(shù)目(當(dāng)然,如果不夠還是先緩存起來,直到收夠為止),然后解析包頭,根據(jù)包頭中指定的包體大小來收取包體,等包體收夠了,就組裝成一個完整的包來處理。

但是假如傳輸?shù)氖且粋€文件,而不是變長的格式化數(shù)據(jù),其實也沒有所謂的分包現(xiàn)象,因為這一波流都是一個文件的內(nèi)容,全部接收就好了。