Skip to content

Latest commit

 

History

History
193 lines (138 loc) · 7.16 KB

File metadata and controls

193 lines (138 loc) · 7.16 KB

Обработка частичной отправки

Функция send(), по независящим от нас, внутреядровым причинам ОС, может не отослать все байты, которые вы хотите.

В нашей ответственности - проконтролировать, чтобы все байты были отправлены.

Например, вы хотите отправить 512 байт, но send() вернул вам 412 байт, итого - 100 байт не отправились.

Логика достаточно проста: вы продолжаете вызывать send() до тех пор, пока не осталось байтов на отправку.

Примерная функция, для обработки частичной отправки:

#include <sys/types.h>
#include <sys/socket.h>

int sendall(int s, char* buf, int* len)
{
    /* сколько байт отправлено */
    int total = 0;

    /* сколько осталось отправить */
    int bytesleft = *len;

    int n;

    while(total < *len) {
        n = send(s, buf + total, bytesleft, 0);
        if(n == -1) {
            break;
        }

        total += n;
        bytesleft -= n;
    }

    *len = total;

    return n == -1 ? -1 : 0;
}

Но что произойдет у получателя, когда он получит вместо одного пакета несколько? Как ему понять, где заканчивается один пакет и начинается другой? Это действительно сложно, и требует дополнительной работы.

Сериализация - упаковка данных

Гонять по сети строки, вроде "Hey baby!" - легко, мы это уже делали, переправляя сообщения. А если мы хотим отправить бинарыне данные? Например, int, float и даже структуры?

Для этого нужно кодировать данные в перемещаемую бинарную форму. Получатель декодирует их.

Кодирование данных - это упаковка данных в какой-либо формат. Обычно используется слово "сериализация" или "маршализация".

Реализовывать свою сериализацию и десериализацию может быть крайне сложно, нужно учитывать - портируемость, формат данных. Лучше использовать готовые решения для упаковки данных:

  • json
  • protocol buffers
  • bson
  • и другие.

Инкапсуляция данных

Главные вопросы, которые решает инкапсуляция данных - это синхронизация пакета между отправителем и получателем и превращение произвольных байтов в сообщение.

Что такое сообщение? Где заканчивается сообщение? Все ли данные переданы?

Так как функции send() и recv() могут вернуть не все байты, которые ожидаются, требуется сделать механизм валидации сообщений на прикладном уровне.

В учебных целях реализация может быть не идеальной, однако в производственном коде конечно лучше использовать готовые и надежные решения, которые позволят работать не с байтиками, а с сообщениями , например - ZeroMQ.

Сперва нужно определить что такое сообщение, например мы делаем чатик, с такими сообщениями:

struct AppMsg {
    char* author;
    char* msg;
};

Чтобы передать это через сеть, нужно преобразовать эту структуру в сырые байты - массив uint8_t:

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

uint8_t* serialize_appmsg(
    const struct AppMsg *m,
    uint32_t *out_size
) {
    uint32_t author_len = strlen(m->author);
    uint32_t msg_len    = strlen(m->msg);

    uint32_t payload_len =
        4 + author_len +
        4 + msg_len;

    uint32_t total_len = payload_len;

    uint32_t net_total_len  = htonl(total_len);
    uint32_t net_author_len = htonl(author_len);
    uint32_t net_msg_len    = htonl(msg_len);

    uint8_t *buf = malloc(4 + payload_len);
    uint8_t *p = buf;

    memcpy(p, &net_total_len, 4);  p += 4;
    memcpy(p, &net_author_len, 4); p += 4;
    memcpy(p, m->author, author_len); p += author_len;
    memcpy(p, &net_msg_len, 4);    p += 4;
    memcpy(p, m->msg, msg_len);

    *out_size = 4 + payload_len;
    return buf;
}

Затем отправить их все, с учетом, возможной, неполной отправки:

ssize_t send_all(int sock, const uint8_t *buf, size_t len) {
    size_t sent = 0;

    while (sent < len) {
        ssize_t n = send(sock, buf + sent, len - sent, 0);
        if (n <= 0)
            return -1;
        sent += n;
    }
    return sent;
}

Получатель десериализует сообщение:

struct AppMsg deserialize_appmsg(int sock) {
    uint32_t net_total_len;
    recv_all(sock, (uint8_t*)&net_total_len, 4);

    uint32_t total_len = ntohl(net_total_len);

    uint8_t *payload = malloc(total_len);
    recv_all(sock, payload, total_len);

    uint8_t *p = payload;

    uint32_t author_len;
    memcpy(&author_len, p, 4);
    author_len = ntohl(author_len);
    p += 4;

    char *author = malloc(author_len + 1);
    memcpy(author, p, author_len);
    author[author_len] = '\0';
    p += author_len;

    uint32_t msg_len;
    memcpy(&msg_len, p, 4);
    msg_len = ntohl(msg_len);
    p += 4;

    char *msg = malloc(msg_len + 1);
    memcpy(msg, p, msg_len);
    msg[msg_len] = '\0';

    free(payload);

    struct AppMsg m = {
        .author = author,
        .msg = msg
    };
    return m;
}

Итого, получатель понимает что сообщение передано полностью по следующим причинам:

  • он сначала читает 4 байта длины
  • он считыает ровно столько сколько нужно (из длины)
  • пока не дочитал сообщение НЕ считается полученным

Все прикладные протоколы реализуют примерно тоже-самое под капотом , инкапсулируя сырые байты в сообщения:

  • HTTP/2
  • gRPC
  • WebSocket
  • ZeroMQ
  • ... и так далее.