在C中通过TCP(SOCK_STREAM)套接字传递结构

时间:2011-11-03 19:39:15

标签: c sockets tcp ipc

我有一个小型客户端服务器应用程序,我希望通过C而不是C ++的TCP套接字发送整个结构。假设结构如下:

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

我发现很多帖子说我需要使用pragma pack或在发送和接收之前序列化数据。

我的问题是,使用JUST pragma pack还是仅仅序列化是否足够?或者我需要同时使用它们吗?

此外,由于序列化是处理器密集型过程,这会使您的性能急剧下降,因此在没有使用外部库的情况下序列化结构的最佳方法是什么(我想要一个示例代码/算法)?

8 个答案:

答案 0 :(得分:15)

您需要以下内容通过网络轻松发送struct:

  • 打包结构。对于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;
}

就字节顺序转换函数而言,对于16位和32位整数,可以使用系统的htons() / ntohs()htonl() / ntohl()分别转换为/从big-endian转换。但是,我不知道64位整数的任何标准函数,或者转换为/从小端转换。您可以使用my byte order conversion functions;如果你这样做,你必须通过定义BADVPN_LITTLE_ENDIANBADVPN_BIG_ENDIAN告诉它你机器的字节顺序。

就有符号整数而言,转换函数可以像我编写和链接的那样安全地实现(直接交换字节);只需将unsigned更改为signed。

UPDATE :如果你想要一个有效的二进制协议,但又不喜欢摆弄字节,你可以尝试Protocol BuffersC implementation)之类的东西。这允许您在单独的文件中描述消息的格式,并生成用于编码和解码您指定格式的消息的源代码。我自己也实现了类似的东西,但大大简化了;请参阅my BProto generatorsome examples(查看.bproto文件,使用addr.h作为用法示例)。

答案 1 :(得分:3)

在通过TCP连接发送任何数据之前,请制定协议规范。它不必是一个充满技术术语的多页文档。但它必须指定谁在什么时候传输什么,它必须在字节级别指定所有消息。它应该指定如何建立消息的结尾,是否有任何超时以及谁强加它们等等。

如果没有规范,很容易提出根本无法回答的问题。如果出现问题,哪一端有问题?根据规范,不符合规范的一端是错误的。 (如果两端都遵循规范并且仍然不起作用,那么规范是错误的。)

一旦你有了规范,就可以更容易地回答有关如何设计一端或另一端的问题。

我还强烈建议围绕硬件细节设计网络协议。至少,并非没有经证实的性能问题。

答案 2 :(得分:2)

这取决于您是否可以确定连接两端的系统是否同质。如果您确定(我们大多数人都不可能),那么您可以采取一些捷径 - 但您必须意识到它们是捷径。

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

和类似的read()

但是,如果系统可能有所不同,那么您需要确定数据的正式传输方式。您可能会对数据进行线性化(序列化) - 可能与ASN.1类似,或者更简单地使用可以轻松重读的格式。为此,文本通常是有益的 - 当您可以看到出现问题时,它更容易调试。如果不这样做,您需要定义传输int的字节顺序,并确保传输遵循该顺序,字符串可能会获得一个字节数,然后是适当的数据量(考虑是否传输一个终端是否为null),然后浮点数的一些表示。这更加繁琐。编写序列化和反序列化函数来处理格式化并不是那么困难。棘手的部分是设计(决定)协议。

答案 3 :(得分:1)

您可以将union与您要发送的结构和数组一起使用:

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

这样你就可以发送和接收arr。当然,您必须关注字节序问题,并且sizeof(struct something)可能因机器而异(但您可以使用#pragma pack轻松克服此问题)。

答案 4 :(得分:1)

为什么你会在那里有好的和快速的序列化库(如Message Pack为你完成所有艰苦的工作)时这样做,作为奖励他们为你提供套接字协议的跨语言兼容性?

使用Message Pack或其他序列化库来执行此操作。

答案 5 :(得分:1)

通常,序列化带来了几个好处,例如:通过线路发送结构的比特(例如fwrite)。

  1. 对于每个非聚合原子数据(例如int)单独发生。
  2. 它精确定义了通过线路发送的串行数据格式
  3. 因此它涉及异构架构:发送和接收机器可能有不同的字长和字节序。
  4. 当类型稍微改变时,它可能不那么脆弱。因此,如果一台计算机运行旧版本的代码,它可能能够与具有更新版本的计算机通信,例如一个人使用char b[80];代替char b[64];
  5. 它可以处理更复杂的数据结构 - 变量大小的向量,甚至哈希表 - 用逻辑方式(对于哈希表,传输关联,......)
  6. 通常,会生成序列化例程。甚至在20年前,RPCXDR已经存在,并且XDR序列化原语仍然存在于许多libc中。

答案 6 :(得分:0)

Pragma pack用于另一端的struct的二进制兼容性。 因为发送结构的服务器或客户端可能使用其他语言编写,也可能与其他c编译器或其他c编译器选项一起构建。

序列化,据我所知,正在从你的struct生成字节流。当您在套接字中编写struct时,您将进行序列化。

答案 7 :(得分:0)

Google协议缓冲区为该问题提供了一个很好的解决方案。在这里{{3}}

根据有效负载的结构创建一个.proto文件,并将其另存为 payload.proto

Controller
public function lodging() {
    $url = 'https://vkysten-api.bookingstudio.dk/rest/v1Admin/lodgings.xml';
    $username = 'xxxxxxxxxxxxxxx';
    $headers = array( 'Authorization' => 'Basic ' . base64_encode( "$username" ) );
    $response = wp_remote_get( $url, array( 'headers' => $headers ) );

    $body     = wp_remote_retrieve_body($response);

    $data = simplexml_load_string($body);

    return $data;
}
}

Frontpage with the link
<div class="huse">
    <?php $i = 0; ?>
    @foreach ($lodging->Lodging as $lodge)
      @php
      $i++;
      $images =  $lodge->Images->Image['Location'];
      $address = $lodge['Address'];
      $postalcode = $lodge['PostalCode'];
      $city = $lodge['City'];

      $items = array();
      foreach($lodge->Images->Image as $item) {
          $items[] = $item;
      };

      usort ($items, function($a, $b) {
          return strcmp($a['SortOrder'], $b['SortOrder']);
      });

      @endphp
      @if (!empty($items))
      <div class="lodging-items">
        @foreach ($items as $item)
        <a href="feriehuse/?hus={{$address}}">
          @if ($item['SortOrder']==0)
          <div class="lodge-image">
            <img src="{{$item['Location']}}" Width="400" Height="300" alt="">
            <div class="lodge-address">{{$address}} | {{$postalcode}} {{$city}}</div>
          </div>

          @endif
          @endforeach

          <div class="frontlodge">{{$lodge->Localizations->Localization['Title']}}</div>
        </a>
      </div>
      @endif
      <?php if ($i === 8) break; ?>
    @endforeach
  </div>

Single page
<?php $item = $lodging->Lodging['Address']; ?>

@foreach ($lodging as $item)
{{$item['Address']}}
@endforeach

使用

编译.proto文件
syntax="proto3"

message Payload {
     int32 age = 1;
     string name = 2;
} . 

这将在您的目录中创建头文件 payload.pb-c.h 及其相应的 payload.pb-c.c

创建您的 server.c 文件,并包含protobuf-c头文件

protoc --c_out=. payload.proto

在接收方 client.c

#include<stdio.h>
#include"payload.pb.c.h"

int main()
{
   Payload pload = PLOAD__INIT;
   pload.name = "Adam";
   pload.age = 1300000;

   int len = payload__get_packed_size(&pload);

   uint8_t buffer[len];

   payload__pack(&pload, buffer);

   // Now send this buffer to the client via socket. 
}

确保使用 -lprotobuf-c 标志编译程序

....
int main()
{
   uint8_t buffer[MAX_SIZE]; // load this buffer with the socket data. 
   size_t buffer_len; // Length of the buffer obtain via read()
   Payload *pload = payload_unpack(NULL, buffer_len, buffer);

   printf("Age : %d Name : %s", pload->age, pload->name);
}