基本概念
- TCP本質上是數據流,從原理上看,沒有包的概念,TCP包對應用程序員可以是透明的。
- 粘包實際上是把底層包的實現和上層流的概念混在一起。
- 粘包問題本質上是如何確定數據流的邊界。
確定邊界的幾種典型辦法
1. 固定長度法:一般在簡單的私有協議中實現,可以簡化流程,方便實現。
通訊之前先通過第三方規定本次發送的包長
- 阻塞發送與接收:
發送:send(fd, wr_data_buf, wr_data_len, 0); /* wr_data_buf 數據緩存, wr_data_len預先設定的固定長度 */
接收:recv(fd, wr_data_buf, wr_data_len, 0);
- 如以上代碼所示,發送和接收就直接調用socketAPI接口就可以了。這樣寫簡單,但是有如下問題:容易阻塞主進程,引起多余的進程調度和不可控的系統超時;可以用獨立的進程或者線程來優化,但會引起復雜的同步邏輯;無法適應大規模的發送端和接收端同時工作的場景。
- 無阻塞的發送和接收: 這種方式編碼復雜一點,但是解決了阻塞方式引起的問題,是目前的主流解決方案。發送端的流程圖是這樣的:Alice 固定長度法發送端流程圖說明如下:流程圖中假設發送的預設固定長度是1024個字節。如果利用EPOLL的Level方式,應該在EPOLLOUT的回調函數中調用alice_send_data,隱式實現3->4->3的循環流程。如果利用EPOLL的Edge方式,應該在EPOLLOUT的回調函數中調用alice_send_epoll,顯式實現3-4-3的循環流程。偽代碼是這樣的:
static int alice_send_data(int fd, char *wr_data_buf)
{
int n;
n = send(fd, wr_data_buf + offset, 1024 - off, MSG_DONTWAIT); /* 無阻塞發送了n個字節*/
if (n < 0) {
if (errno == EAGAIN || errno == EINTR)
return 0;
else {
return -1; /* error */
}
} else if (n == 0) {
return 1; /* socket close */
}
offset += n; /*記住總共發了off個字節 */
if (off < 1024) /*如果小于預先給定的長度,返回0,繼續調用本函數發送 */
return 0;
return 1; /*發完了,返回1,繼續下面的工作 */
}
static int alice_send_epoll(int fd, char *wr_data_buf) /* edge 方式 */
{
int offset = 0;
int finish;
do {
finish = alice_send_data(fd, wr_data_buf)
} while (!finish);
}
- 接收端的流程圖是這樣的:Bob 固定長度法接收端流程圖我們可以看出,接收部分可發送部分很相似,這樣本文就不重復代碼了。
2. 變長法: 在私有和公共協議中實現,比固定長度法稍微復雜一點,但比較靈活:
通訊之前先通過第三方規定長度的位置,以便接收端獲取
- 發送端知道發送數據的實際長度,然后加上記錄長度的4個字節,算出數據總長,按照固定長度的辦法發送。
- 接收端則需要動態獲得數據的實際長度,它的流程圖是這樣的:Bob 變長法接收端流程圖
我們看出,變長法在接收端實際上是兩步固定長度法,所以它比固定長度法復雜。但是由于發送端可以靈活的指定數據的長度,也就是每次發送的數據可以不同,應用更加廣泛。
3. 特殊字符串法:在私有和公共協議中實現,比變長法更復雜,但是節省包頭長度字段,處理更加靈活。
通訊之前通過第三方規定一個特殊的字符串,比如說'rnrn',接收端才能據此確定數據流的邊界。
- 發送端可以按照固定長度的辦法發送。
- 接收端則需要不斷地查找接收緩沖里面的所有數據,看是否有特殊字符串的存在。它的流程圖是這樣的:Bob 特殊字符串接收端流程圖