linux網路編程之socket(六):利用recv和readn函數實現readline函數

來源:互聯網
上載者:User

在前面的文章中,我們為了避免粘包問題,實現了一個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'就返回,一行最多為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”偷窺“ 一下現在緩衝區有多少個字元,然後查看是否存在分行符號'\n',如果存在,則使用readn連通分行符號一起讀取,如果不存在,則也先將前面的資料讀取進bufp, 且移動bufp的位置,回到while迴圈開頭,再從當前bufp位置窺看,注意,當我們調用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》

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.