Функция 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
- ... и так далее.