Почему отправка дейтаграммы не работает, если я сначала не создам TCP-соединение?

#c #windows #sockets #winsock

#c #Windows #сокеты #winsock

Вопрос:

Следующая программа на c должна преобразовать каждую строку в верхний регистр, используя socket datagram для связи между двумя потоками.

 Example:
Hello World!<return>
HELLO WORLD!
123abc!<return>
123ABC!
<return>
<end program>
  

Написанная программа работает для меня, однако, если я прокомментирую bugfix() вызов функции в main, программа будет бесконечно ждать после первой строки ввода.

 Example:
Hello World!<return>
<the program wait indefinitely>
  

Это происходит в Windows 7 с последним обновлением от 04.10.2011 с использованием последнего MinGW32.

 #include <iostream>
#include <cstdlib>
#include <cctype>
#include <sys/types.h>
#include <winsock.h>
#include <windows.h>
#include <process.h>

using namespace std;

#define CHECK(exp, cond)  do { typeof(exp) _check_value_ = exp; check(_check_value_ cond, _check_value_, __LINE__, #exp #cond); } while(0)

template <class T>
void check(bool ok, T value, int line, const char* text) {
    if (!ok) {
        cerr << "ERROR(" << line << "):" << text << "nReturned: " << value << endl;
        cerr << "errno=" << errno << endl;
        cerr << "WSAGetLastError()=" << WSAGetLastError() << endl;
        exit(EXIT_FAILURE);
    }
}

#define DATA_CAPACITY   1000
#define PORT            23584
#define TEST_IP         "192.0.32.10"
#define MYSELF          "127.0.0.1"
#define DST_IP          MYSELF

sockaddr_in address(u_long ip, u_short port) {
    sockaddr_in addr = { };
    addr.sin_family = AF_INET;
    addr.sin_port = port;
    addr.sin_addr.s_addr = ip;
    return addr;
}

void __cdecl client_thread(void* args) {
    SOCKET s = *(SOCKET*)args;

    sockaddr_in addr = address(inet_addr(DST_IP), htons(PORT));

    char data[DATA_CAPACITY];
    while (1) {
        cin.getline(data, DATA_CAPACITY);
        int data_len = strlen(data);

        CHECK(sendto(s, data, data_len, 0, (sockaddr*)amp;addr, sizeof addr), >= 0);
        CHECK(recvfrom(s, data, DATA_CAPACITY, 0, NULL, NULL), >= 0);

        cout << data << endl;

        if (data_len == 0)
            break;
    }

    CHECK(closesocket(s), == 0);
}

void __cdecl server_thread(void* args) {
    SOCKET s = *(SOCKET*)args;
    sockaddr_in addr = address(INADDR_ANY, htons(PORT));
    int addr_size = sizeof addr;
    CHECK(bind(s, (sockaddr*)amp;addr, sizeof addr), != SOCKET_ERROR);

    char data[DATA_CAPACITY];
    while (1) {
        int data_len = recvfrom(s, data, DATA_CAPACITY, 0, (sockaddr*)amp;addr, amp;addr_size);
        CHECK(data_len, >= 0);

        for (int i = 0; i < data_len; i  )
            if (islower(data[i]))
                data[i] = toupper(data[i]);

        CHECK(sendto(s, data, data_len, 0, (sockaddr*)amp;addr, addr_size), >= 0);

        if (data_len == 0)
            break;
    }

    CHECK(closesocket(s), == 0);
}

// This function create a TCP connection with www.example.com and the close it
void bugfix() {
    SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in addr = address(inet_addr(TEST_IP), htons(80));
    connect(s, (sockaddr*)amp;addr, sizeof addr);
    CHECK(closesocket(s), == 0);
}

int main()
{
    cout << "Convert text to uppercase, an empty line terminate the program" << endl;


    WSADATA wsaData;
    CHECK(WSAStartup(MAKEWORD(2, 2), amp;wsaData), == 0);

    SOCKET client = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    SOCKET server = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

    CHECK(client, != INVALID_SOCKET);
    CHECK(server, != INVALID_SOCKET);

    // if this function is not called the program doesn't work
    bugfix();

    HANDLE hClient = (HANDLE)_beginthread(client_thread, 0, amp;client);
    HANDLE hServer = (HANDLE)_beginthread(server_thread, 0, amp;server);

    HANDLE h[] = { hClient, hServer };

    WaitForMultipleObjects(sizeof h / sizeof *h, h, TRUE, INFINITE);

    CHECK(WSACleanup(), == 0);

    return EXIT_SUCCESS;
}
  

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

1. Помимо других проблем, этот код отлично работает для меня в Windows 7 x64.

Ответ №1:

     int data_len = strlen(data);
  

Тони Хоар назвал свое определение нулевого указателя ошибкой на миллиард долларов. Наличие строк, заканчивающихся нулем, должно быть, ошибкой Денниса Ричи на десять миллиардов долларов. Добавьте одно.

В остальном ваша программа представляет собой сложный способ обнаружить, что UDP не является надежным протоколом. Сетевому стеку разрешено произвольно удалять UDP-пакеты или изменять их порядок. Это нормально, если поверх него есть другой протокол, который обнаруживает это, например TCP. Вы летаете без таких бандажей, исправление ошибки () на самом деле не является обходным решением.

Используйте TCP, сначала отправьте длину пакета, чтобы получатель знал, сколько байтов следует за вами, чтобы вы были защищены от поведения потока. Но более того, обмен данными между потоками через сокет — это действительно дорогой способ избежать использования массива с мьютексом. Потоки имеют неограниченный доступ к памяти в процессе, вам не нужен механизм межпроцессной связи, чтобы заставить их обмениваться данными.

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

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

Ответ №2:

Я сразу вижу несколько проблем.

Обычно я не использую флаг IPPROTO_UDP для создания сокета. Просто передайте сокету 0 для параметра протокола.

 SOCKET client = socket(PF_INET, SOCK_DGRAM, 0);
SOCKET server = socket(PF_INET, SOCK_DGRAM, 0);
  

Что еще более важно. Вам нужно вызвать «bind» в клиентском сокете тем же способом, что и в серверном сокете. Если вы хотите, чтобы ОС выбрала для вас произвольно доступный порт, вы можете использовать 0 в качестве значения порта и IPADDR_ANY в качестве IP-адреса. Если вы хотите знать, какой порт ОС выбрала для вас в качестве локального, вы можете использовать getsockname. Что-то вроде следующего:

 void __cdecl client_thread(void* args) {
    SOCKET s = *(SOCKET*)args;

    sockaddr_in addr = address(inet_addr(DST_IP), htons(PORT));

    sockaddr_in localAddrBind = address(INADDR_ANY, 0);
    sockaddr_in localAddrActual = {};
    int length = sizeof(localAddrActual);
    int bindRet = bind(s, (sockaddr*)amp;localAddrBind, sizeof(localAddrBind));

    getsockname(s, (sockaddr*)amp;localAddrActual, amp;length);
    printf("Listening on port %dn", ntohs(localAddrActual.sin_port));