2010-12-02 42 views
4

我對C相當陌生,並且正在編寫一個TCP服務器,並且想知道如何處理將發送服務器響應的命令的客戶端的recv()。爲了這個問題,我們只是說頭是第一個字節,命令標識符是第二個字節,有效載荷長度是第三個字節,後面是有效載荷(如果有的話)。處理多個recv()調用和所有可能的場景

recv()這個數據的最好方法是什麼?我正在考慮調用recv()來讀取緩衝區中的前3個字節,檢查以確保頭標和命令標識符有效,然後檢查有效長度並以有效長度作爲長度再次調用recv(),並將其添加到上述緩衝區的後面。然而,閱讀Beej的網絡文章(特別是本節的這一部分:http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html#sonofdataencap),他建議使用「一個足夠大的數組來處理兩個[最大長度]數據包」來處理諸如獲取下一個數據包的情況。

什麼是處理這些類型的recv()的最佳方式?基本的問題,但我想有效地實施它,處理所有可能出現的情況。提前致謝。

回答

5

是Beej被影射的方法,和AlastairG提到,工作是這樣的:

對於每個併發連接,你保持閱讀,但是,尚未處理的數據的緩衝區。 (這是Beej建議調整爲最大數據包長度兩倍的緩衝區)。顯然,緩衝開始了空:

unsigned char recv_buffer[BUF_SIZE]; 
size_t recv_len = 0; 

每當你的插座是可讀,讀入緩衝區中的剩餘空間,然後立即嘗試過程中,你有什麼:

result = recv(sock, recv_buffer + recv_len, BUF_SIZE - recv_len, 0); 

if (result > 0) { 
    recv_len += result; 
    process_buffer(recv_buffer, &recv_len); 
} 

process_buffer()嘗試並將緩衝區中的數據作爲數據包處理。如果緩衝區尚未包含完整的數據包,它只會返回 - 否則,它會處理數據並將其從緩衝區中刪除。因此,對於您的示例協議,它看起來是這樣的:

void process_buffer(unsigned char *buffer, size_t *len) 
{ 
    while (*len >= 3) { 
     /* We have at least 3 bytes, so we have the payload length */ 

     unsigned payload_len = buffer[2]; 

     if (*len < 3 + payload_len) { 
      /* Too short - haven't recieved whole payload yet */ 
      break; 
     } 

     /* OK - execute command */ 
     do_command(buffer[0], buffer[1], payload_len, &buffer[3]); 

     /* Now shuffle the remaining data in the buffer back to the start */ 
     *len -= 3 + payload_len; 
     if (*len > 0) 
      memmove(buffer, buffer + 3 + payload_len, *len); 
    } 
} 

(該do_command()功能會檢查是否有一個有效的標題和指令字節)。

這種技術最終被必要的,因爲任何recv()可以返回一個短 - 與你的建議的方法,如果你的有效載荷長度爲500會發生什麼,但接下來的recv()只返回你400個字節?無論如何,你必須保存這400個字節,直到下一次套接字變得可讀。當你處理多個併發客戶端時,每個客戶端只需要一個recv_bufferrecv_len,並將它們填充到每個客戶端結構中(這可能也包含其他內容 - 如客戶端套接字,可能是它們的源地址,當前狀態等等。)。

5

不錯的問題。你想走多遠?對於全部演唱的所有舞蹈解決方案,請使用異步套接字,只要可以,就可以讀取所有可用的數據,並且每當您獲取新數據時,都會調用緩衝區上的某些數據處理功能。

這可以讓你做大讀。如果你獲得了大量的流水線命令,你可以處理它們而無需再次等待套接字,從而提高性能和響應時間。

在寫上做類似的事情。這是命令處理函數寫入緩衝區。如果緩衝區中有數據,那麼當檢查套接字(選擇或輪詢)時檢查可寫性並儘可能多地寫入,記住只刪除實際從緩衝區寫入的字節。

在這種情況下,循環緩衝區運行良好。

有更輕鬆簡單的解決方案。但是,這是一個很好的。請記住,服務器可能會獲得多個連接,並且可能會拆分數據包。如果你從一個套接字讀入一個緩衝區而只發現你沒有完整命令的數據,那麼你對你已經讀過的數據做了什麼?你在哪裏存儲它?如果您將它存儲在與該連接相關的緩衝區中,那麼您可能需要全部訪問,並且首先按照上面所述讀入緩衝區。

此解決方案還避免了必須爲每個連接產生一個單獨的線程 - 您可以處理任何數量的連接,而不會有任何實際問題。每個連接產生一個線程是系統資源的不必要的浪費 - 除非在某些情況下,無論如何推薦使用多個線程,並且您可以簡單地讓工作線程執行這些阻塞任務,同時保持套接字處理單線程。

基本上我同意你說的Beej說的,但不要一次只讀一點點。一次讀大塊。編寫一個這樣的套接字服務器,就像我一起學習和設計一樣,它基於一小部分的套接字體驗和手冊頁,是我從事過的最有趣的項目之一,也是非常有教育意義的項目。

+0

謝謝AlastairG。我基本上試圖保持它儘可能簡單,只處理最常見的情況,如讀入下一個數據包,或讀取部分數據包。我實際上使用select來監視傳入數據的客戶端套接字(在這種情況下是命令),所以我不認爲這是異步套接字的問題。我的方法是讀取3個字節,然後有效載荷不起作用?我不想實現Beej所說的原因是因爲如果沒有實際的代碼/僞代碼就很難理解。感謝您的建議:) – Jack 2010-12-02 16:14:26

+0

對於處理傳入數據後的寫入/發送,我只是簡單地使用Beej的sendall方法的變體,因爲我知道數據包的長度..顯示在這裏:http:// beej頁面沒有自動跳轉/導向/ bgnet /輸出/ HTML /多頁/ advanced.html#sendall。我更關心閱讀。 – Jack 2010-12-02 16:15:52

+0

我對寫作的評論只是爲了完整 - 以防其他人想知道如何編寫一個非常好的套接字服務器。我要說的一件事是,我已經嘗試編寫小而簡單的套接字服務器,但你很快發現你只需要按照我描述的方式來完成它。但並非總是如此。讀取3個字節可以工作,但爲什麼要這樣做? – AlastairG 2010-12-02 16:33:14

2

Alastair所描述的解決方案在性能方面是最好的。僅供參考 - 異步編程也稱爲事件驅動編程。換句話說,您需要等待數據進入套接字,將其讀入緩衝區,處理什麼/什麼時候可以,然後重複。您的應用程序可以在讀取數據和處理數據之間做其他事情。

一對夫婦更多的鏈接,我發現有幫助做一些非常相似:

第二個是一個偉大的圖書館,以幫助實現這一切。

至於使用緩衝區和儘可能多的閱讀,這是另一個表現的事情。批量讀取更好,更少的系統調用(讀取)。您處理的,當你決定你有足夠的處理,但要確保只處理您的「包」中的一個緩衝的時間,而不是(你用3個字節的報頭中所描述的)破壞其他數據緩衝區中的數據。

1

基本上有兩個假設,如果你正在使用多個連接,然後同時處理多個連接的最佳方式(無論是監聽套接字,readfd或writefd)是選擇/投票/ epoll的。您可以根據您的要求使用其中任何一種。

關於你的第二個查詢如何處理多個recv()這個練習可以使用: 每當數據到達時只是查看頭部(它應該是固定的長度和格式,如你所描述的)。

buff_header = (char*) malloc(HEADER_LENGTH); 
    count = recv(sock_fd, buff_header, HEADER_LENGTH, MSG_PEEK); 
    /*MSG_PEEK if you want to use the header later other wise you can set it to zero 
     and read the buffer from queue and the logic for the code written below would 
     be changed accordingly*/ 

通過這個,你得到了你的頭,你可以驗證參數,也提取完整的味精長度。 得到充分信息長度後只收到完整的味精

msg_length=payload_length+HEADER_LENGTH; 
    buffer =(char*) malloc(msg_length); 
    while(msg_length) 
    { 
     count = recv(sock_fd, buffer, msg_length, 0); 
     buffer+=count; 
     msg_length-=count; 
    } 

所以在這種方式,你不必採取有一些固定長度的任何數組,可以輕鬆實現你的邏輯。