GongGong
GongGong

Reputation: 21

C socket sending multiple fields message with binary protocol

How to construct a request message with a given message specification, and then send to server thought c socket? Binary protocol is employed for Client and Server communication. Are the following approaches correct?

Given message specification:

Field         Fomat   Length    values
------------  ------  ------   --------
requesID      Uint16    2       20
requestNum    Uint16    2       100
requestTitle  String    10      data sring


    /************** approach 1 ****************/
    typedef unsigned short uint16;
    typedef struct {
        uint16 requesID [2];
        uint16 requestNum [2];
        unsigned char requestTitle [10];
    }requesMsg;
    …
    requesMsg rqMsg;

    memcpy(rqMsg.requesID, "\x0\x14", 2);   //20
    memcpy(rqMsg.requesNum, "\x0\x64", 2);  //100
    memcpy(rqMsg.requesTitle, "title01   ", 10);
    …
    send(sockfd, &rqMsg, sizeof(rqMsg), 0);

    /************** approach 2 ****************/
    unsigned char rqMsg[14];
    memset(rqMsg, 0, 14);
    memcpy(rqMsg, "\x0\x14", 2);
    memcpy(rqMsg+2, "\x0\x64", 2);
    memcpy(rqMsg+4, "title01   ", 10);
    …
    send(sock, &rqMsg, sizeof(rqMsg), 0);
    

Upvotes: 2

Views: 676

Answers (2)

mrKirushko
mrKirushko

Reputation: 80

Both of them are at least partially correct but I much prefer the first one because it allows for quick and natural data manipulations and access and leaves less space for errors compared to manual indexing. As a bonus you can even copy and assign structure values as a whole and in C it works as expected.

But for any outgoing data you should make sure to use a "packed" struct. Not only it will reduce the amount of data transmitted down to the array-based implementation figure but it will also make sure that the fields alignments are the same in all the programs involved. For most C compilers I tried (GCC included) it can be done with __attribute__((__packed__)) attribute, but there are different compilers that require different attributes or even a different keyword.

Also endianness control may be required if your application is going to run on different architectures (ARM clients vs x86_64 server is a major example). I just use some simple macros like these to preprocess each field individually before doing any calculations or data output:

#define BYTE_SWAP16(num)    ( ((num & 0xFF) << 8) | ((num >> 8) & 0xFF) )
#define BYTE_SWAP32(num)    ( ((num>>24)&0xff) | ((num<<8)&0xff0000) | ((num>>8)&0xff00) | ((num<<24)&0xff000000) )

But you can use different approaches like BCD encoding, separate decoding functions or something else.

Also notice that uint16_t is already a 2-byte value. You probably don't need two of them to store your single values.

Upvotes: 0

junix
junix

Reputation: 3211

I'm afraid you are misunderstanding something: The length column appears to tell you the length in bytes, so if you receive a uint16 you receive 2 bytes.

Your first approach could lead to serious problem through data structure alignment. If I were in your shoes I'd prefer the second approach and fill in the bytes on my own into a byte array.

A general note about filling fields here: I'ts useless to use memcpy for "native" fields like uint16, etc. It might work but is simply a waste of runtime. You can fill in fields of a struct simply assigning them a value like rqMsg.requesID = 20;

Another issue is the question of byte order or endianness of your binary protocol.

As a whole package, I'd implement a "serializeRequest" function taking fields of your struct and convert it into a byte array according to the protocol.

Upvotes: 3

Related Questions