在前面的文章中,我們爲了避免粘包問題,實現了一個readn函數讀取固定字節的數據。如果應用層協議的各字段長度固定,用readn來讀是非常方便
的。例如設計一種客戶端上傳文件的協議,規定前12字節表示文件名,超過12字節的文件名截斷,不足12字節的文件名用'\0'補齊,從第13字節開始是
文件內容,上傳完所有文件內容後關閉連接,服務器可以先調用readn讀12個字節,根據文件名創建文件,然後在一個循環中調用read讀文件內容並存
盤,循環結束的條件是read返回0。
字段長度固定的協議往往不夠靈活,難以適應新的變化。前面講過的TFTP協議的各字段是可變長的,以'\0'爲分隔符,文件名可以任意長,再看blksize
等幾個選項字段,TFTP協議並沒有規定從第m字節到第n字節是blksize的值,而是把選項的描述信息“blksize”與它的值“512”一起做成一個可變長的字
段。
因此,常見的應用層協議都是帶有可變長字段的,字段之間的分隔符用換行'\n'的比用'\0'的更常見,如HTTP協議。可變長字段的協議用readn來讀就很
不方便了,爲此我們實現一個類似於fgets的readline函數。
首先來看一個跟read 相似的系統函數recv。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv函數與read函數類似,但只能讀取套接字描述符,而不能是一般的文件描述符,且多了一個標誌參數。
flags參數比較重要的有兩個,一個是MSG_OOB,即讀取帶外數據時候的選項,tcp頭部有一個緊急指針16位的值。另一個是MSG_PEEK,即從緩衝區
返回數據但不清空緩衝區,這點與read是不同的。
下面使用封裝後的recv函數實現readline函數:
C++ Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 /* recv()只能讀寫套接字,而不能是一般的文件描述符 */
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK); // 設置標誌位後讀取後不清除緩衝區
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/* 讀到'\n'就返回,加上'\n' 一行最多爲maxline個字符 */
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
int count = 0;
while (1)
{
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0)
return ret; // 返回小於0表示失敗
else if (ret == 0)
return ret; //返回0表示對方關閉連接了
nread = ret;
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i + 1);
if (ret != i + 1)
exit(EXIT_FAILURE);
return ret + count;
}
}
if (nread > nleft)
exit(EXIT_FAILURE);
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
exit(EXIT_FAILURE);
bufp += nread;
count += nread;
}
return -1;
在readline函數中,我們先用recv_peek”偷窺“ 一下現在緩衝區有多少個字符並讀取到bufp,然後查看是否存在換行符'\n'。如果存在,則使用readn連
通換行符一起讀取(清空緩衝區);如果不存在,也清空一下緩衝區, 且移動bufp的位置,回到while循環開頭,再次窺看。注意,當我們調用readn讀
取數據時,那部分緩衝區是會被清空的,因爲readn調用了read函數。還需注意一點是,如果第二次纔讀取到了'\n',則先用count保存了第一次讀取的
字符個數,然後返回的ret需加上原先的數據大小。
使用 readline函數也可以認爲是解決粘包問題的一個辦法,即以'\n'爲結尾當作一條消息。對於服務器端來說可以在前面的fork程序的基礎上把do_service函數更改如下:
C++ Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 void do_echoser(int conn)
{
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(conn, recvbuf, 1024);
if (ret == -1)
ERR_EXIT("readline error");
else if (ret == 0) //客戶端關閉
{
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
客戶端的更改也是類似的,不再贅述,測試輸出也是正常的。
參考:
《Linux C 編程一站式學習》
《TCP/IP詳解 卷一》
《UNP》
linux網絡編程之socket(六):利用recv和readn函數實現readline函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.