2011-11-03 6 views
7

Tôi có một ứng dụng máy khách nhỏ mà tôi muốn gửi toàn bộ cấu trúc qua một ổ cắm TCP trong C không phải C++. Giả sử cấu trúc như sau:chuyển cấu trúc qua cổng TCP (SOCK_STREAM) trong C

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

Tôi đã tìm thấy nhiều bài viết nói rằng tôi cần sử dụng gói pragma hoặc để tuần tự hóa dữ liệu trước khi gửi và nhận.

Câu hỏi của tôi là, có đủ để sử dụng gói pragma JUST hay chỉ là serialzation không? Hay tôi cần sử dụng cả hai?

Cũng vì quá trình nối tiếp là quá trình xử lý chuyên sâu nên hiệu suất của bạn giảm đáng kể, vì vậy cách tốt nhất để tuần tự hóa cấu trúc KHÔNG sử dụng thư viện bên ngoài (tôi thích mã mẫu/bản đồ) là gì?

Trả lời

13

Bạn cần những điều sau đây để portably gửi struct của qua mạng:

  • Gói cấu trúc. Đối với trình biên dịch gcc và tương thích, hãy thực hiện việc này với __attribute__((packed)).

  • Không sử dụng bất kỳ thành viên nào khác với số nguyên không có kích thước cố định, các cấu trúc đóng gói khác đáp ứng các yêu cầu này hoặc các mảng của bất kỳ yếu tố nào trước đây. Các số nguyên đã ký cũng OK, trừ khi máy của bạn không sử dụng biểu diễn bổ sung của hai.

  • Quyết định xem giao thức của bạn có sử dụng mã hóa số nguyên nhỏ hoặc lớn hay không. Thực hiện chuyển đổi khi đọc và ghi các số nguyên đó.

  • Ngoài ra, không lấy con trỏ của các thành viên của cấu trúc đóng gói, ngoại trừ những người có kích thước 1 hoặc các cấu trúc đóng gói lồng nhau khác. Xem this answer.

Ví dụ đơn giản về mã hóa và giải mã sau.Nó giả định rằng các hàm chuyển đổi thứ tự byte là hton8(), ntoh8(), hton32()ntoh32() có sẵn (hai giá trị trước là không có op, nhưng ở đó có tính nhất quán).

#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; 
} 

Theo như chức năng chuyển đổi thứ tự byte được quan tâm, hệ thống của bạn htons()/ntohs()htonl()/ntohl() có thể được sử dụng, ví số nguyên 16- và 32-bit, tương ứng, chuyển đổi sang/từ lớn về cuối nhỏ. Tuy nhiên, tôi không biết về bất kỳ chức năng tiêu chuẩn nào cho các số nguyên 64 bit, hoặc để chuyển đổi thành/từ nhỏ endian. Bạn có thể sử dụng my byte order conversion functions; nếu bạn làm như vậy, bạn phải thông báo cho nó thứ tự byte của đơn đặt hàng của máy bằng cách xác định BADVPN_LITTLE_ENDIAN hoặc BADVPN_BIG_ENDIAN. Đối với các số nguyên đã ký, các hàm chuyển đổi có thể được thực hiện một cách an toàn giống như cách tôi đã viết và liên kết (hoán đổi byte trực tiếp); chỉ cần thay đổi unsigned để ký.

CẬP NHẬT: nếu bạn muốn một giao thức nhị phân hiệu quả, nhưng không thích loay hoay với các byte, bạn có thể thử một cái gì đó giống như Protocol Buffers (C implementation). Điều này cho phép bạn mô tả định dạng thư của mình trong các tệp riêng biệt và tạo mã nguồn mà bạn sử dụng để mã hóa và giải mã thư của định dạng bạn chỉ định. Tôi cũng thực hiện một cái gì đó tương tự bản thân mình, nhưng rất đơn giản; xem my BProto generatorsome examples (xem tệp .bproto và addr.h để biết ví dụ về cách sử dụng).

+1

Tôi sẽ thử phương pháp này, tôi chỉ muốn hỏi nếu tôi chỉ sử dụng sprintf và ghi tất cả dữ liệu vào chuỗi bằng dấu tách để tách các phần tử của cấu trúc và gửi qua socket và sau đó sử dụng strtok để trích xuất từng phần tử mặt khác ? Đây có phải là một giải pháp khả thi không? – user434885

+0

có, sprintf sẽ hoạt động, nhưng * chỉ * cho số nguyên; nếu bạn muốn gửi một chuỗi (tức là mảng byte nguyên), bằng cách sử dụng phương thức này, bạn sẽ phải coi chúng như là một mảng các byte và chuyển đổi từng byte thành một số nguyên, chèn khoảng trắng vào giữa. Ví dụ: "abc" sẽ được gửi dưới dạng "97 98 99". Điều này có thể thích hợp hơn vì nó dễ phân tích hơn khi gỡ lỗi, nhưng nó rất vụng về để mã hóa/giải mã, đặc biệt nếu bạn muốn kiểm tra lỗi đầy đủ khi giải mã. –

+0

Động lực đằng sau điểm đạn thứ hai của bạn - chỉ sử dụng số nguyên không dấu. Tại sao các ký tự không thể được sử dụng trong cấu trúc (hoặc mảng char) để gửi các chữ cái, byte hoặc chuỗi? – aaronsnoswell

1

Bạn có thể sử dụng một union với cấu trúc mà bạn muốn gửi và một mảng:

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

Bằng cách này bạn có thể gửi và nhận chỉ arr. Tất nhiên, bạn phải chăm sóc về các vấn đề endianess và sizeof(struct something) có thể thay đổi trên các máy (nhưng bạn có thể dễ dàng khắc phục điều này với một số #pragma pack).

2

Trước khi bạn gửi bất kỳ dữ liệu nào qua kết nối TCP, hãy làm việc ra một đặc tả giao thức. Nó không phải là một tài liệu nhiều trang đầy thuật ngữ kỹ thuật. Nhưng nó phải xác định ai truyền những gì khi nào và nó phải chỉ định tất cả các thông báo ở mức byte. Nó sẽ chỉ định cách kết thúc các thông điệp được thiết lập, cho dù có bất kỳ thời gian chờ nào và những người áp đặt chúng, v.v.

Không có thông số kỹ thuật, thật dễ dàng để đặt câu hỏi đơn giản là không thể trả lời được. Nếu có điều gì sai, kết cục nào có lỗi? Với một đặc điểm kỹ thuật, kết thúc không tuân theo đặc điểm kỹ thuật có lỗi. (Và nếu cả hai đầu đều tuân theo đặc điểm kỹ thuật và nó vẫn không hoạt động, đặc điểm kỹ thuật có lỗi.)

Khi bạn có một đặc điểm kỹ thuật, sẽ dễ dàng hơn khi trả lời các câu hỏi về cách kết thúc.

Tôi cũng khuyên bạn không nên không thiết kế giao thức mạng xung quanh các chi tiết cụ thể về phần cứng của bạn. Ít nhất, không phải không có một vấn đề hiệu suất đã được chứng minh.

1

Tại sao bạn làm điều này khi có các thư viện tuần tự hóa nhanh và tốt ngoài đó như Message Pack làm tất cả công việc khó khăn cho bạn và phần thưởng chúng cung cấp cho bạn khả năng tương thích chéo của giao thức socket?

Sử dụng Gói tin nhắn hoặc một số thư viện tuần tự hóa khác để thực hiện việc này.

+0

Tôi không được phép sử dụng bất kỳ thư viện bên ngoài nào. :/ – user434885

0

Gói Pragma được sử dụng cho khả năng tương thích nhị phân của bạn cấu trúc trên đầu kia. Vì máy chủ hoặc máy khách mà bạn gửi cấu trúc có thể được viết bằng ngôn ngữ khác hoặc được xây dựng bằng trình biên dịch c khác hoặc với các tùy chọn trình biên dịch c khác.

Nối tiếp, như tôi đã biết, đang tạo luồng byte từ cấu trúc của bạn. Khi bạn viết bạn cấu trúc trong ổ cắm bạn thực hiện nối tiếp.

2

Nó phụ thuộc vào việc bạn có thể chắc chắn rằng hệ thống của bạn ở một trong hai đầu của kết nối có đồng nhất hay không. Nếu bạn chắc chắn, cho tất cả thời gian (mà hầu hết chúng ta không thể được), sau đó bạn có thể mất một số phím tắt - nhưng bạn phải biết rằng họ là các phím tắt.

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

và tương tự read().

Tuy nhiên, nếu có khả năng các hệ thống có thể khác nhau, thì bạn cần thiết lập cách dữ liệu sẽ được chuyển chính thức. Bạn cũng có thể tuyến tính hóa (serialize) dữ liệu - có thể là fancily với một cái gì đó như ASN.1 hoặc có lẽ đơn giản hơn với một định dạng có thể được đọc lại dễ dàng. Đối với điều đó, văn bản thường có lợi - nó dễ dàng hơn để gỡ lỗi khi bạn có thể thấy những gì đang xảy ra. Nếu không, bạn cần xác định thứ tự byte trong đó int được chuyển và đảm bảo rằng việc truyền đi theo thứ tự đó và chuỗi có thể nhận được số byte theo sau là lượng dữ liệu thích hợp (xem xét chuyển giao một đầu cuối không hoặc không), và sau đó một số đại diện của phao. Điều này là khó sử dụng hơn. Nó không phải là tất cả những gì khó để viết serialization và deserialization chức năng để xử lý các định dạng. Phần khó khăn là thiết kế (quyết định) giao thức.

+0

điều này sẽ hoạt động trong một số trường hợp, nhưng có khả năng tốt là máy chủ và máy khách của tôi sẽ là 32 & 64 bit, vì vậy hàm sizeof (struct) sẽ trả về các giá trị khác nhau về kích thước của kích thước int sẽ tăng từ 4 byte đến 8 byte. – user434885

1

Thông thường, việc tuần tự hóa mang lại nhiều lợi ích hơn ví dụ: gửi các bit của cấu trúc qua dây (ví dụ: fwrite).

  1. Điều này xảy ra riêng cho từng dữ liệu nguyên tử không tổng hợp (ví dụ: int).
  2. Định nghĩa chính xác định dạng dữ liệu nối tiếp được gửi qua dây
  3. Vì vậy, nó đề cập đến kiến ​​trúc không đồng nhất: máy gửi và nhận có thể có độ dài và độ dài khác nhau.
  4. Nó có thể ít giòn hơn khi loại thay đổi một chút. Vì vậy, nếu một máy có một phiên bản cũ của mã của bạn đang chạy, nó có thể nói chuyện với một máy tính có phiên bản mới hơn, ví dụ: một số có một số char b[80]; thay vì char b[80]; thay vì char b[64];
  5. Nó có thể xử lý các cấu trúc dữ liệu phức tạp hơn-vectơ có kích thước, hoặc thậm chí bảng băm - một cách hợp lý (cho bảng băm, truyền liên kết, ..)

Rất thường xuyên, các thường trình tuần tự được tạo. Thậm chí 20 năm trước, RPCXDR đã tồn tại cho mục đích đó và các nguyên bản tuần tự hóa XDR vẫn còn trong nhiều libc.

0

Nếu bạn cần tính di động thì bạn phải tuần tự từng thành viên riêng lẻ do kết thúc và cấu trúc đệm.

Dưới đây là một ví dụ sử dụng 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); 

Nó chỉ là 2 file (binn.c và binn.h) để nó có thể được biên dịch với dự án thay vì sử dụng như một thư viện chia sẻ.

Có thể bạn cũng nên sử dụng khung tin nhắn (còn được gọi là khung tiền tố có độ dài) trong luồng ổ cắm.