Как добавить 2-байтовый размер строки и отправить ее по протоколу TCP

#c #sockets #tcp #endianness

Вопрос:

У меня есть приложение, которое отправляет полученные UDP-пакеты по TCP-соединению. Я храню данные пакета UDP в std::string объекте.

Для отправки/приема TCP я использую схему кодирования/декодирования данных as <2-byte data length><Data> .

Это мое требование.

Как добавить длину строки в 2 байта к значению? std::string

Кроме того, нужно ли мне также заботиться о конце (hostToNetwork) для целого числа длиной 2 байта? Кроме того, для части данных?

Комментарии:

1. Храните данные со смещением в два байта, заполните длину, как только вы узнаете, что это такое.

2. не могу. Данные udp передаются в один механизм синтаксического анализа, и он возвращает строковый объект, который мне нужно предварительно указать его длину и отправить.

3. Вы не можете эффективно дополнять (т. Е. без перемещения других данных). С другой стороны, это просто перемещение данных UDP-пакета, что довольно быстро. Действительно ли эта операция является узким местом? (Кстати, не очевидно, почему вы просто не добавляете это в синтаксический анализатор.)

4. Вы, вероятно, хотите позаботиться о точности размера. Если вы просто получаете и повторно отправляете UDP-пакеты, вам не следует каким-либо образом изменять данные пакетов.

5. ..или просто дважды вызовите функцию send (), один раз для заголовка, затем другой для дейтаграммы UDP.

Ответ №1:

В UDP одно send() из них является полным «сообщением», вы не можете разделить «сообщение» на несколько send() s.

Но TCP-это поток байтов, поэтому вы можете совершать несколько последовательных вызовов для send() каждого «сообщения». Итак, просто отправьте длину данных в одном send() , а затем отправьте данные в другом send() . TCP гарантирует, что байты будут получены в том же порядке, в котором они были отправлены. Вам вообще не нужно добавлять байты длины к std::string самому себе.

Это работает особенно хорошо, если включена функция «Отправка сращивания» (она же алгоритм Нэгла), которая обычно используется по умолчанию. Это позволяет стеку сокетов буферизировать исходящие данные, чтобы он мог более эффективно отправлять пакеты по сети. Но даже если Нэгл отключен, эта схема все равно будет работать.

На самом деле, в TCP нет никакой гарантии, что send() он примет все запрошенные байты за один раз, поэтому вы все равно должны быть готовы звонить send() несколько раз.

Попробуйте что-нибудь вроде этого:

 bool sendRaw(int sock, const void *data, size_t len)
{
    const char *pdata = static_cast<const char*>(data);
    while (len > 0)
    {
        int numSent = send(sock, pdata, len, 0);
        if (numSent < 0) return false; // or throw...
        pdata  = numSent;
        len -= numSent;
    }
    return true;
}

bool sendUint16(int sock, uint16_t value)
{
    value = htons(value);
    return sendRaw(sock, amp;value, sizeof(value));
}

bool sendString(int sock, const std::string amp;s)
{
    if (s.size() > 0xFFFF) return false; // or throw...
    uint16_t len = static_cast<uint16_t>(s.size());
    bool ok = sendUint16(sock, len);
    if (ok) ok = sendRaw(sock, s.c_str(), len);
    return ok;
}
 
 std::string udpData = ...;
bool ok = sendString(sock, udpData);
...
 

И тогда вы можете просто повернуть процесс вспять на принимающей стороне, например:

 int recvRaw(int sock, void *data, size_t len)
{
    char *pdata = static_cast<char*>(data);
    while (len > 0)
    {
        int numRecvd = recv(sock, pdata, len, 0);
        if (numRecvd <= 0) return numRecvd; // or throw...
        pdata  = numRecvd;
        len -= numRecvd;
    }
    return 1;
}

int recvUint16(int sock, uint16_t amp;value)
{
    int ret = recvRaw(sock, amp;value, sizeof(value));
    value = (ret == 1) ? ntohs(value) : 0;
    return ret;
}

int recvString(int sock, std::string amp;s)
{
    s.clear();
    uint16_t len;
    int ret = recvUint16(sock, len);
    if ((ret == 1) amp;amp; (len > 0)) {
        s.resize(len);
        ret = recvRaw(sock, s.data()/*amp;s[0]*/, len);
    }
    return ret;
}
 
 std::string udpData;
int ret = recvString(sock, udpData);
...
 

Комментарии:

1. Спасибо, но не повлияет ли это на производительность приложения, например, для 1000 пакетов udp, когда я выполняю 2000 вызовов send (), хотя при схеме с добавленной длиной вызов send() будет таким же, как количество пакетов.

2. @tingtong нет, на самом деле это не сильно повлияет на производительность, если вообще повлияет. Попытка фактически добавить байты длины к значению std::string больше повлияет на производительность из-за выделения новой памяти. Этот код использует уже выделенную память. И в любом случае сокет имеет внутренний буфер, поэтому не имеет значения, сколько send() вызовов вы совершаете, сначала байты должны быть скопированы в этот буфер, прежде чем ядро передаст их.

3.Я немного слаб в том, как данные считываются/записываются в память. Верна ли эта логика::: std::string sourceUdp = "Vivekananda"; uint16_t sLen = static_cast<uint16_t>(sourceUdp.size()); std::string destTcp=""; destTcp.append(std::to_string((0xFFFF))); destTcp.append(sourceUdp); destTcp.insert(0,std::to_string(sLen));

4. @tingtong нет, потому что ваше использование to_string() и insert() не подходит в данном случае, таким образом destTcp , не будет иметь никакого отношения к правильному формату, который вы хотите. Попробуйте это вместо этого: std::string destTcp(2 sLen, ''); *reinterpret_cast<uint16_t*>(amp;destTcp[0]) = htons(sLen); memcpy(amp;destTcp[2], sourceUdp.c_str(), sLen);

Ответ №2:

Вероятно, не конец света, чтобы просто сделать это прямо перед отправкой. Выделение временного буфера из стека для пакета UDP (который в любом случае не будет больше 64 КБ) — при условии, что синхронная отправка сокета будет быстрой.

 unsigned char buffer[LONGEST_STRING_LENGTH 2];
size_t len = str.size();
// assert(len <= LONGEST_STRING_LENGTH);
uint16_t lenNBO = (uint16_t)len;
lenNBO = htons(lenNBO);
memcpy(buffer, amp;lenNBO, 2);
memcpy(buffer 2, str.c_str(), len);
send(sock, buffer, len 2, 0);
 

Ответ №3:

Используете ли вы какую-либо оболочку (C ) для сокетов? В противном случае просто предварительно заполните строку, в которую вы получаете данные, двумя произвольными байтами, а затем добавьте данные, полученные из сокета. После этого запишите длину строки в первые два смещения (минус два).

Что-то вроде

 string x("xx");
// receive your data into x, starting at offset 2
x[0] = (x.length() - 2) amp; 0xff;
x[1] = ((x.length() - 2) amp; 0xff00) >> 8;
// send ...
 

В зависимости от того, какой конец вы хотите, переключите назначение 0 и 1. Приведенный выше код записывает его как little endian. Порядок байтов в сети считается большим конечным.

Но tbh., если вы используете API сокетов напрямую, я бы использовал простой массив символов вместо std::string .