2
votes

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);
    
2
Only the second approach is correct, and you can think about the utility of the memset call yourself.Kerrek SB
@KerrekSB That's not entirely true. If the endianness of his machine matches the endianness of the protocol and if he uses pragma pack(1) for the structure he could get the correct result. (Ok, as soon as he removes the array size from the fields) But unfortunately the code is not really portable then...junix

2 Answers

3
votes

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.

0
votes

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.