2011-11-03 94 views
7

我有一個小型的客戶端服務器應用程序,我希望通過C++而不是C++的TCP套接字發送整個結構。假設結構爲以下:傳遞一個結構通過TCP(SOCK_STREAM)套接字C

struct something{ 
int a; 
char b[64]; 
float c; 
} 

我發現有很多帖子說,我需要使用編譯包或發送和recieveing之前序列化的數據。

我的問題是,使用JUST pragma pack或只是序列化足夠了嗎?或者我需要使用兩者?

此外,由於序列化是處理器密集型過程,這使得你的性能急劇下降,所以什麼是序列化一個結構沒有使用外部庫(我喜歡示例代碼/算法)的最佳方式?

回答

13

您需要以下可移植髮送結構的通過網絡:

  • 包的結構。對於gcc和兼容的編譯器,請使用__attribute__((packed))執行此操作。

  • 請不要使用除了固定大小的無符號整數以外的其他成員,滿足這些要求的其他打包結構或任何前者的數組。有符號的整數也可以,除非你的機器不使用二進制補碼錶示。

  • 決定您的協議是否使用整數的小端或大端編碼。在讀寫這些整數時進行轉換。

  • 另外,不採取壓縮結構成員的指針,除了那些具有大小爲1或其它嵌套堆積結構。見this answer

一個簡單的編碼和解碼示例如下。它假定字節順序轉換函數hton8()ntoh8()hton32(),並ntoh32()是可用的(前兩者都是一個無操作,但也有對一致性)。

#include <stdint.h> 
#include <inttypes.h> 
#include <stdlib.h> 
#include <stdio.h> 

// get byte order conversion functions 
#include "byteorder.h" 

struct packet { 
    uint8_t x; 
    uint32_t y; 
} __attribute__((packed)); 

static void decode_packet (uint8_t *recv_data, size_t recv_len) 
{ 
    // check size 
    if (recv_len < sizeof(struct packet)) { 
     fprintf(stderr, "received too little!"); 
     return; 
    } 

    // make pointer 
    struct packet *recv_packet = (struct packet *)recv_data; 

    // fix byte order 
    uint8_t x = ntoh8(recv_packet->x); 
    uint32_t y = ntoh32(recv_packet->y); 

    printf("Decoded: x=%"PRIu8" y=%"PRIu32"\n", x, y); 
} 

int main (int argc, char *argv[]) 
{ 
    // build packet 
    struct packet p; 
    p.x = hton8(17); 
    p.y = hton32(2924); 

    // send packet over link.... 
    // on the other end, get some data (recv_data, recv_len) to decode: 
    uint8_t *recv_data = (uint8_t *)&p; 
    size_t recv_len = sizeof(p); 

    // now decode 
    decode_packet(recv_data, recv_len); 

    return 0; 
} 

至於字節順序轉換函數而言,系統的htons()/ntohs()htonl()/ntohl()可以使用,分別爲16位和32位整數,轉換到/從大端。但是,我不知道64位整數的任何標準函數,或轉換爲/從小端。您可以使用my byte order conversion functions;如果你這樣做,你必須通過定義BADVPN_LITTLE_ENDIANBADVPN_BIG_ENDIAN來告訴你的機器的字節順序

至於符號整數而言,轉換功能,可以安全地以同樣的方式實現的那些我寫和鏈接(直接交換字節);只需更改無符號簽名。

UPDATE:如果你想要一個高效的二進制協議,但不喜歡用字節擺弄,你可以嘗試像Protocol BuffersC implementation)。這使您可以在單獨的文件中描述消息的格式,並生成源代碼,用於對指定格式的消息進行編碼和解碼。我也實施了類似的東西,但大大簡化了;請參閱my BProto generatorsome examples(查看.bproto文件,addr.h查看使用示例)。

+1

我會嘗試這種方法,我只想問,如果我只是使用sprintf,並使用分隔符將所有數據寫入字符串來分離結構的元素並通過套接字發送,然後使用strtok提取每個元素另一邊 ?這也是一個可行的解決方案嗎? – user434885

+0

是的,sprintf會工作,但*只*爲整數;如果你想發送一個字符串(即原始字節數組),使用這種方法,你必須把它們當作一個字節數組,並把每個字節轉換成一個整數,在它們之間插入空格。例如,「abc」將作爲「97 98 99」發送。這可能更好,因爲它在調試時分析起來更容易些,但編碼/解碼很笨拙,尤其是在解碼時需要全面的錯誤檢查。 –

+0

第二個要點背後的動機是什麼 - 只使用無符號整數。爲什麼不能在結構(或字符數組)中使用字符來發送字母,字節或字符串? – aaronsnoswell

1

你可以使用一個union你要發送的結構和數組:

union SendSomething { 
    char arr[sizeof(struct something)]; 
    struct something smth; 
}; 

這種方式,您可以發送和接收只是編曲。當然,你必須關心endianess問題,sizeof(struct something)可能因機器而異(但你可以用#pragma pack輕鬆解決這個問題)。

2

在通過TCP連接發送任何數據之前,先制定一個協議規範。它不必是充滿技術術語的多頁文檔。但它必須指定誰在何時傳輸什麼,並且必須在字節級別指定所有消息。它應該說明消息的結尾是如何建立的,是否有超時和誰強加它們等等。

沒有規範,很容易提出根本無法回答的問題。如果出現問題,哪一端出錯?對於規範,不符合規範的結果是錯誤的。 (如果兩端都符合規範並且仍然不起作用,則說明存在錯誤。)

一旦您有了規範,回答關於應如何設計一端或另一端的問題就容易多了。

我也強烈建議不是圍繞您的硬件的具體細節設計網絡協議。至少,不是沒有經過驗證的性能問題。

1

你爲什麼要這麼做的時候有很好的和快速的序列化圖書館有像Message Pack該做的所有爲你努力工作,並作爲獎勵,他們爲你提供你的套接字協議的跨語言兼容?

使用消息包或其他序列化庫來執行此操作。

+0

我不允許使用任何外部庫。 :/ – user434885

0

Pragma包用於在另一端結構的二進制兼容性。 因爲您發送結構的服務器或客戶端可能會使用其他語言編寫,或者使用其他c編譯器或其他c編譯器選項編譯。

序列化,據我所知,正在從你的結構的字節流。當你在socket中編寫結構體時,你會進行序列化。

2

這取決於您是否可以確定連接兩端的系統是否是同類系統。如果你確定,在任何時候(我們大多數人都不能),那麼你可以採取一些捷徑 - 但你必須知道它們是捷徑。

struct something some; 
... 
if ((nbytes = write(sockfd, &some, sizeof(some)) != sizeof(some)) 
    ...short write or erroneous write... 

和類似的read()

但是,如果系統有可能不同,那麼您需要確定數據將如何正式傳輸。你可能很好地將數據線性化(序列化) - 可能很像ASN.1,或者可能更簡單地使用可輕鬆重新讀取的格式。爲此,文本通常是有益的 - 當您可以看到發生了什麼問題時,更容易進行調試。否則,您需要定義傳輸int的字節順序,並確保傳輸遵循該順序,並且該字符串可能會得到一個字節計數,然後是適當的數據量(考慮是否將終端傳輸爲空或不),然後浮點數的一些表示。這很麻煩。編寫序列化和反序列化函數來處理格式化並不難。棘手的部分是設計(決定)協議。

+0

這在某些情況下會起作用,但是我的服務器和客戶端很可能是32位和64位的機器,所以sizeof(struct)函數將返回不同的值,因爲int的大小將從4字節到8個字節。 – user434885

1

通常,序列化帶來了諸多優點。通過導線發送結構的位(例如使用fwrite)。

  1. 它單獨會爲每個非集合原子數據(例如INT)。
  2. 它精確定義了通過線路發送的串行數據格式。因此,它處理異構架構:發送和接收機器可以具有不同的字長和字節序。
  3. 當類型改變一點時,它可能不那麼脆弱。因此,如果一臺機器運行的是舊版本的代碼,則它可能能夠與具有更新版本的機器通信,例如,一個具有char b[80];而不是char b[64];
  4. 它可以用邏輯方式處理更復雜的數據結構 - 可變大小的向量或甚至散列表(對於散列表,傳輸關聯,..)

很多時候,會生成序列化例程。即使在20年前,RPCXDR已經爲此而存在,並且XDR序列化原語仍然處於很多libc中。

0

如果您需要可移植性,那麼您必須單獨序列化每個成員,由於字節數和結構填充。

下面是使用Binn一個例子:

binn *obj; 

    // create a new object 
    obj = binn_object(); 

    // add values to it 
    binn_object_set_int32(obj, "id", 123); 
    binn_object_set_str(obj, "name", "Samsung Galaxy Charger"); 
    binn_object_set_double(obj, "price", 12.50); 
    binn_object_set_blob(obj, "picture", picptr, piclen); 

    // send over the network 
    send(sock, binn_ptr(obj), binn_size(obj)); 

    // release the buffer 
    binn_free(obj); 

它僅僅是2個文件(binn.c和binn.h),所以它可以與項目被編譯而不是用作一個共享庫。

也許你也應該在插座流使用消息幀(也稱爲長度前綴取景)。