Ошибка инициализации сокета прослушивания

Тема сетевого программирования является для разработчиков одной из важнейших в современном цифровом мире. Правда, надо признать, что большая часть сетевого программирования сосредоточена в области написания скриптов исполнения для web-серверов на языках PHP, Python и им подобных. Как следствие — по тематике взаимодействия клиент-сервер при работе с web-серверами написаны терабайты текстов в Интернете. Однако когда я решил посмотреть, что же имеется в Интернете по вопросу программирования сетевых приложений с использованием голых сокетов, то обнаружил интересную вещь: да, такие примеры конечно же есть, но подавляющее большинство написано под *nix-системы с использованием стандартных библиотек (что понятно – в области сетевого программирования Microsoft играет роль сильно отстающего и менее надежного «собрата» *nix-ов). Другими словами все эти примеры просто не будут работать под Windows. При определенных танцах с бубнами код сетевого приложения под Linux можно запустить и под Windows, однако это еще более запутает начинающего программиста, на которого и нацелены большинство статей в Интернете с примерами использования сокетов.

Ну а что же с документацией по работе с сетевыми сокетами в Windows от самой Microsoft? Парадоксальность ситуации заключается в том, что непосредственно в самой документации приведено очень беглое описание функций и их использования, а в примерах имеются ошибки и вызовы старых «запрещенных» современными компиляторами функций (к примеру, функция inet_addr() — https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-listen ) — такие функции конечно же можно вызывать, заглушив бдительность компилятора через #define-директивы, однако такой подход является полным зашкваром для любого даже начинающего программиста и категорически не рекомендуется к использованию. Более того, фрагмент кода в примере от Microsoft по ссылке выше:

service.sin_addr.s_addr = inet_addr("127.0.0.1");

вообще не заработает, т.к. полю Service.sin_addr.s_addr невозможно присвоить значение целого типа, которое возвращает функция inet_addr (возвращает unsigned long). То есть это ни много, ни мало — ошибка! Можно себе представить, сколько пытливых бойцов полегло на этом месте кода.

В общем, посмотрев на всё это, я решил написать базовую статью по созданию простейшего клиент-сервер приложения на С++ под Windows с детальным описанием всех используемых функций. Это приложение будет использовать Win32API и делать незамысловатую вещь, а именно: передавать сообщения от клиента к серверу и обратно, или, иначе говоря – напишем программу по реализации чата для двух пользователей.

Сразу оговорюсь, что статья рассчитана на начинающих программистов, которые только входят в сетевое программирование под Windows. Необходимые навыки – базовое знание С++, а также теоретическая подготовка по теме сетевых сокетов и стека технологии TCP/IP.

Теория сокетов за 30 секунд для «dummies»

Начну всё-таки немного с теории в стиле «for dummies». В любой современной операционной системе, все процессы инкапсулируются, т.е. скрываются друг от друга, и не имеют доступа к ресурсам друг друга. Однако существуют специальные разрешенные способы взаимодействия процессов между собой. Все эти способы взаимодействия процессов можно разделить на 3 группы: (1) сигнальные, (2) канальные и (3) разделяемая память.

Когда мы говорим про работу сетевого приложения, то всегда подразумеваем взаимодействие процессов: процесс 1 (клиент) пытается что-то послать или получить от Процесса 2 (сервер). Наиболее простым и понятным способом организации сетевого взаимодействия процессов является построение канала между этими процессами. Именно таким путём и пошли разработчики первых сетевых протоколов. Получившийся способ взаимодействия сетевых процессов в итоге оказался многоуровневым: основной программный уровень — стек сетевой технологии TCP/IP, который позволяет организовать эффективную доставку пакетов информации между различными машинами в сети, а уже на прикладном уровне тот самый «сокет» позволяет разобраться какой пакет какому процессу доставить на конкретной машине.

Иными словами «сокет» — это «розетка» конкретного процесса, в которую надо подключиться, чтобы этому процессу передать какую-либо информацию. Договорились, что эта «розетка» в Сети описывается двумя параметрами – IP-адресом (для нахождения машины в сети) и Портом подключения (для нахождения процесса-адресата на конкретной машине).

Для того, чтобы сокеты заработали под Windows, необходимо при написании программы пройти следующие Этапы:

  1. Инициализация сокетных интерфейсов Win32API.

  2. Инициализация сокета, т.е. создание специальной структуры данных и её инициализация вызовом функции.

  3. «Привязка» созданного сокета к конкретной паре IP-адрес/Порт – с этого момента данный сокет (его имя) будет ассоциироваться с конкретным процессом, который «висит» по указанному адресу и порту.

  4. Для серверной части приложения: запуск процедуры «прослушки» подключений на привязанный сокет.

    Для клиентской части приложения: запуск процедуры подключения к серверному сокету (должны знать его IP-адрес/Порт).

  5. Акцепт / Подтверждение подключения (обычно на стороне сервера).

  6. Обмен данными между процессами через установленное сокетное соединение.

  7. Закрытие сокетного соединения.

     Итак, попытаемся реализовать последовательность Этапов, указанных выше, для организации простейшего чата между клиентом и сервером. Запускаем Visual Studio, выбираем создание консольного проекта на С++ и поехали.

Этап 0: Подключение всех необходимых библиотек Win32API для работы с сокетами

Сокеты не являются «стандартными» инструментами разработки, поэтому для их активизации необходимо подключить ряд библиотек через заголовочные файлы, а именно:

  • WinSock2.h – заголовочный файл, содержащий актуальные реализации функций для работы с сокетами.

  • WS2tcpip.h – заголовочный файл, который содержит различные программные интерфейсы, связанные с работой протокола TCP/IP (переводы различных данных в формат, понимаемый протоколом и т.д.).

  • Также нам потребуется прилинковать к приложению динамическую библиотеку ядра ОС: ws2_32.dll. Делаем это через директиву компилятору: #pragma comment(lib, “ws2_32.lib”)

  • Ну и в конце Этапа 0 подключаем стандартные заголовочные файлы iostream и stdio.h   

Итого по завершению Этапа 0 в Серверной и Клиентской частях приложения имеем:

#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdio.h>
#include <vector>

#pragma comment(lib, "Ws2_32.lib")

Обратите внимание: имя системной библиотеки ws2_32.libименно такое, как это указано выше. В Сети есть различные варианты написания имени данной библиотеки, что, возможно, связано иным написанием в более ранних версиях ОС Windows. Если вы используете Windows 10, то данная библиотека называется именно ws2_32.libи находится в стандартной папке ОС: C:/Windows/System32 (проверьте наличие библиотеки у себя, заменив расширение с “lib” на “dll”).

Этап 1: Инициализация сокетных интерфейсов Win32API

Прежде чем непосредственно создать объект сокет, необходимо «запустить» программные интерфейсы для работы с ними. Под Windows это делается в два шага следующим образом:

  • Нужно определить с какой версией сокетов мы работаем (какую версию понимает наша ОС) и

  • Запустить программный интерфейс сокетов в Win32API. Ну либо расстроить пользователя тем, что ему не удастся поработать с сокетами до обновления системных библиотек

Первый шаг делается с помощью создания структуры типа WSADATA, в которую автоматически в момент создания загружаются данные о версии сокетов, используемых ОС, а также иная связанная системная информация:WSADATA wsData;

Второй шаг – непосредственный вызов функции запуска сокетов с помощью WSAStartup(). Упрощённый прототип данной функции выглядит так:

int WSAStartup (WORD <запрашиваемая версия сокетов>, WSADATA* <указатель на структуру, хранящую текущую версию реализации сокетов>)

Первый аргумент функции – указание диапазона версий реализации сокетов, которые мы хотим использовать и которые должны быть типа WORD. Этот тип данных является внутренним типом Win32API и представляет собой двухбайтовое слово (аналог в С++: unsigned short). Функция WSAStartup() просит вас передать ей именно WORD, а она уже разложит значение переменной внутри по следующему алгоритму: функция считает, что в старшем байте слова указана минимальная версия реализации сокетов, которую хочет использовать пользователь, а в младшем – максимальная. По состоянию на дату написания этой статьи (октябрь 2021 г.) актуальная версия реализации сокетов в Windows – 2. Соответственно, желательно передать и в старшем, и в младшем байте число 2. Для того, чтобы создать такую переменную типа WORD и передать в её старший и младший байты число 2, можно воспользоваться Win32API функцией MAKEWORD(2,2).

Можно немного повыёживаться и вспомнить (или полистать MSDN), что функция MAKEWORD(x,y) строит слово по правилу y << 8 | x.Нетрудно посчитать, что при x=y=2 значение функции MAKEWORD в десятичном виде будет 514. Можешь смело передать в WSAStartup() это значение, и всё будет работать.

Второй аргумент функции – просто указатель на структуру WSADATA, которую мы создали ранее и в которую подгрузилась информация о текущей версии реализации сокетов на данной машине.

WSAStartup() в случае успеха возвращает 0, а в случае каких-то проблем возвращает код ошибки, который можно расшифровать последующим вызовом функции WSAGetLastError().

Важное замечание: поскольку сетевые каналы связи и протоколы в теории считаются ненадежными (это отдельный большой разговор), то критически важно для сетевого приложения анализировать все возможные ошибки, которые возникают в процессе вызовов сокетных функций. По этой причине каждый вызов таких функций мы будем анализировать на ошибки и в случае их обнаружения завершать сетевые сеансы и закрывать открытые сокеты. Используем для этого переменную erStat типа int.

Также важно после работы приложения обязательно закрыть использовавшиеся сокеты с помощью функции closesocket(SOCKET <имя сокета>) и деинициализировать сокеты Win32API через вызов метода WSACleanup().

Итого код Этапа 1 следующий:

WSADATA wsData;
		
int erStat = WSAStartup(MAKEWORD(2,2), &wsData);
	
	if ( erStat != 0 ) {
		cout << "Error WinSock version initializaion #";
		cout << WSAGetLastError();
		return 1;
	}
	else
		cout << "WinSock initialization is OK" << endl;

Да, кода мало, а описания много. Так обычно и бывает, когда хочешь глубоко в чем-то разобраться. Так что на лабе будешь в первых рядах.

Этап 2: Создание сокета и его инициализация

Сокет в С++ – это структура данных (не класс) типа SOCKET. Её инициализация проводится через вызов функции socket(), которая привязывает созданный сокет к заданной параметрами транспортной инфраструктуре сети. Выглядит прототип данной функции следующим образом:

SOCKET socket(int <семейство используемых адресов>, int <тип сокета>, int <тип протокола>)

  • Семейство адресов: сокеты могут работать с большим семейством адресов. Наиболее частое семейство – IPv4. Указывается как AF_INET.

  • Тип сокета: обычно задается тип транспортного протокола TCP (SOCK_STREAM) или UDP (SOCK_DGRAM). Но бывают и так называемые «сырые» сокеты, функционал которых сам программист определяет в процессе использования. Тип обозначается SOCK_RAW

  • Тип протокола: необязательный параметр, если тип сокета указан как TCP или UDP – можно передать значение 0. Тут более детально останавливаться не будем, т.к. в 95% случаев используются типы сокетов TCP/UDP.

При необходимости подробно почитать про функцию socket() можно здесь.

Функция socket() возвращает дескриптор с номером сокета, под которым он зарегистрирован в ОС. Если же инициализировать сокет по каким-то причинам не удалось – возвращается значение INVALID_SOCKET.

Код Этапа 2 будет выглядеть так:

SOCKET ServSock = socket(AF_INET, SOCK_STREAM, 0);

	if (ServSock == INVALID_SOCKET) {
		cout << "Error initialization socket # " << WSAGetLastError() << endl; 
		closesocket(ServSock);
		WSACleanup();
		return 1;
	}
	else
		cout << "Server socket initialization is OK" << endl;

Этап 3: Привязка сокета к паре IP-адрес/Порт

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

Такое назначение делается с помощью функции bind(), имеющей следующий прототип:

int bind(SOCKET <имя сокета, к которому необходимо привязать адрес и порт>, sockaddr* <указатель на структуру, содержащую детальную информацию по адресу и порту, к которому надо привязать сокет>, int <размер структуры, содержащей адрес и порт>)

Функция bind() возвращает 0, если удалось успешно привязать сокет к адресу и порту, и код ошибки в ином случае, который можно расшифровать вызовом WSAGetLastError() — см. итоговый код Этапа 3 далее.

Тут надо немножно притормозить и разобраться в том, что за такая структура типа sockaddr передается вторым аргументом в функцию bind(). Она очень важна, но достаточно запутанная.

Итак, если посмотреть в её внутренности, то выглядят они очень просто: в ней всего два поля – (1) первое поле хранит семейство адресов, с которыми мы уже встречались выше при инициализации сокета, а (2) второе поле хранит некие упакованные последовательно и упорядоченные данные в размере 14-ти байт. Бессмысленно разбираться детально как именно эти данные упакованы, достаточно лишь понимать, что в этих 14-ти байтах указан и адрес, и порт, а также дополнительная служебная информация для других системных функций Win32API.

Но как же явно указать адрес и порт для привязки сокета? Для этого нужно воспользоваться другой структурой, родственной sockaddr, которая легко приводится к этому типу — структурой типа sockaddr_in.

В ней уже более понятные пользователю поля, а именно:

  • Семейство адресов — опять оно (sin_family)

  • Порт (sin_port)

  • Вложенная структура типа in_addr, в которой будет храниться сам сетевой адрес (sin_addr)

  • Технический массив на 8 байт (sin_zero[8])

При приведении типа sockaddr_in к нужному нам типу sockaddr для использования в функции bind() поля Порт (2 байта), Сетевой адрес (4 байта) и Технический массив (8 байт) как раз в сумме дают нам 14 байт, помещающихся в 14 байт, находящихся во втором поле структуры sockaddr. Первые поля у указанных типов совпадают – это семейство адресов сокетов (указываем AF_INET). Из этого видно, что структуры данных типа sockaddr и sockaddr_in тождественны, содержат одну и ту же информацию, но в разной форме для разных целей.

Соответственно, ввод данных для структуры типа sockaddr_in выглядит следующим образом:

  1. Создание структуры типа sockaddr_in : sockaddr_in servInfo;

  2. Заполнение полей созданной структуры servInfo

  • servInfo.sin_family = AF_INET;

  • servInfo.sin_port = htons(<указать номер порта как unsigned short>); порт всегда указывается через вызов функции htons(), которая переупаковывает привычное цифровое значение порта типа unsigned short в побайтовый порядок понятный для протокола TCP/IP (протоколом установлен порядок указания портов от старшего к младшему байту или «big-endian»).

  • Далее нам надо указать сетевой адрес для сокета. Тип этого поля – структура типа in_addr, которая по своей сути представляет просто особый «удобный» системным функциям вид обычного строчного IPv4 адреса. Таким образом, чтобы указать этому полю обычный IPv4 адрес, его нужно сначала преобразовать в особый числовой вид и поместить в структуру типа in_addr .

    Благо существует функция, которая переводит обычную строку типа char[], содержащую IPv4 адрес в привычном виде с точками-разделителями в структуру типа in_addr – функция inet_pton(). Прототип функции следующий:

    int inet_pton(int <семейство адресов>, char[] <строка, содержащая IP-адрес в обычном виде с точкой-разделителем>, in_addr* <указатель на структуру типа in_addr, в которую нужно поместить результат приведения строчного адреса в численный>).

    В случае ошибки функция возвращает значение меньше 0.

    Соответственно, если мы хотим привязать сокет к локальному серверу, то наш код по преобразованию IPv4 адреса будет выглядеть так:

    in_addr ip_to_num;

    erStat = inet_pton(AF_INET, “127.0.0.1”, &ip_to_num);

    if (erStat <= 0) {

                 cout << "Error in IP translation to special numeric format" << endl;

                 return 1;

           }

    Результат перевода IP-адреса содержится в структуре ip_to_num. И далее мы передаем уже в нашу переменную типа sockaddr_in значение преобразованного адреса:

    servInfo.sin_addr = ip_to_num;

Вся нужная информация для привязки сокета теперь у нас есть, и она хранится в структуре servInfo. Можно смело вызывать функцию bind(), не забыв при этом привести servInfo из типа sockaddr_in в требуемый функцииsockaddr*. Тогда итоговый код Этапа 3 (слава богу закончили) выглядит так:

in_addr ip_to_num;
erStat = inet_pton(AF_INET, “127.0.0.1”, &ip_to_num);
if (erStat <= 0) {
		cout << "Error in IP translation to special numeric format" << endl;
		return 1;
	}

sockaddr_in servInfo;
ZeroMemory(&servInfo, sizeof(servInfo));	
				
servInfo.sin_family = AF_INET;
servInfo.sin_addr = ip_to_num;	
servInfo.sin_port = htons(1234);

erStat = bind(ServSock, (sockaddr*)&servInfo, sizeof(servInfo));
if ( erStat != 0 ) {
		cout << "Error Socket binding to server info. Error # " << WSAGetLastError() << endl;
		closesocket(ServSock);
		WSACleanup();
		return 1;
	}
	else 
		cout << "Binding socket to Server info is OK" << endl;

Этап 4 (для сервера): «Прослушивание» привязанного порта для идентификации подключений

Серверная часть готова к прослушке подключающихся «Клиентов». Для того, чтобы реализовать данный этап, нужно вызвать функцию listen(), прототип которой:

int listen(SOCKET <«слушающий» сокет, который мы создавали на предыдущих этапах>, int <максимальное количество процессов, разрешенных к подключению>)

Второй аргумент: максимально возможное число подключений устанавливается через передачу параметр SOMAXCONN(рекомендуется). Если нужно установить ограничения на количество подключений – нужно указать SOMAXCONN_HINT(N), где N – кол-во подключений. Если будет подключаться больше пользователей, то они будут сброшены.

После вызова данной функции исполнение программы приостанавливается до тех пор, пока не будет соединения с Клиентом, либо пока не будет возвращена ошибка прослушивания порта. Код Этапа 4 для Сервера:

erStat = listen(ServSock, SOMAXCONN);

	if ( erStat != 0 ) {
		cout << "Can't start to listen to. Error # " << WSAGetLastError() << endl;
		closesocket(ServSock);
		WSACleanup();
		return 1;
	}
	else {
		cout << "Listening..." << endl;
	}

Этап 4 (для Клиента). Организация подключения к серверу

Код для Клиента до текущего этапа выглядит даже проще: необходимо исполнение Этапов 0, 1 и 2. Привязка сокета к конкретному процессу (bind()) не требуется, т.к. сокет будет привязан к серверному Адресу и Порту через вызов функции connect()(по сути аналог bind() для Клиента). Собственно, после создания и инициализации сокета на клиентской стороне, нужно вызвать указанную функциюconnect(). Её прототип:

int connect(SOCKET <инициализированный сокет>, sockaddr* <указатель на структуру, содержащую IP-адрес и Порт сервера>, int <размер структуры sockaddr>)

Функция возвращает 0 в случае успешного подключения и код ошибки в ином случае.

Процедура по добавлению данных в структуру sockaddr аналогична тому, как это делалось на Этапе 3 для Сервера при вызове функции bind(). Принципиально важный момент – в эту структуру для клиента должна заноситься информация о сервере, т.е. IPv4-адрес сервера и номер «слушающего» порта на сервере.

sockaddr_in servInfo;

ZeroMemory(&servInfo, sizeof(servInfo));

servInfo.sin_family = AF_INET;
servInfo.sin_addr = ip_to_num;	  // Server's IPv4 after inet_pton() function
servInfo.sin_port = htons(1234);

erStat = connect(ClientSock, (sockaddr*)&servInfo, sizeof(servInfo));
	
	if (erStat != 0) {
		cout << "Connection to Server is FAILED. Error # " << WSAGetLastError() << endl;
		closesocket(ClientSock);
		WSACleanup();
		return 1;
	}
	else 
		cout << "Connection established SUCCESSFULLY. Ready to send a message to Server" 
    << endl;

Этап 5 (только для Сервера). Подтверждение подключения

После начала прослушивания (вызов функции listen()) следующей функцией должна идти функция accept(), которую будет искать программа после того, как установится соединение с Клиентом. Прототип функции accept():

SOCKET accept(SOCKET <"слушающий" сокет на стороне Сервера>, sockaddr* <указатель на пустую структуру sockaddr, в которую будет записана информация по подключившемуся Клиенту>, int* <указатель на размер структуры типа sockaddr>)

 Функция accept() возвращает номер дескриптора, под которым зарегистрирован сокет в ОС. Если произошла ошибка, то возвращается значение INVALID_SOCKET.

Если подключение подтверждено, то вся информация по текущему соединению передаётся на новый сокет, который будет отвечать со стороны Сервера за конкретное соединение с конкретным Клиентом. Перед вызовом accept() нам надо создать пустую структуру типа sockaddr_in, куда запишутся данные подключившегося Клиента после вызова accept(). Пример кода:

sockaddr_in clientInfo; 

ZeroMemory(&clientInfo, sizeof(clientInfo));	

int clientInfo_size = sizeof(clientInfo);

SOCKET ClientConn = accept(ServSock, (sockaddr*)&clientInfo, &clientInfo_size);

if (ClientConn == INVALID_SOCKET) {
		cout << "Client detected, but can't connect to a client. Error # " << WSAGetLastError() << endl;
		closesocket(ServSock);
		closesocket(ClientConn);
		WSACleanup();
		return 1;
}
else 
		cout << "Connection to a client established successfully" << endl;

Всё, соединение между Клиентом и Сервером установлено! Самое время попробовать передать информацию от Клиента к Серверу и обратно. Как мы в начале и договорились, мы будет реализовывать простейший чат между ними.

Этап 6: Передача данных между Клиентом и Сервером

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

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

Рассмотрим прототипы функций recv() и send():

int recv(SOCKET <сокет акцептованного соединения>, char[] <буфер для приёма информации с другой стороны>, int <размер буфера>, <флаги>)

int send(SOCKET <сокет акцептованного соединения>, char[] <буфер хранящий отсылаемую информацию>, int <размер буфера>, <флаги>)

Флаги в большинстве случаев игнорируются – передается значение 0.

Функции возвращают количество переданных/полученных по факту байт.

Как видно из прототипов, по своей структуре и параметрам эти функции совершенно одинаковые. Что важно знать:

  • и та, и другая функции не гарантируют целостности отправленной/полученной информации. Это значит, что при реализации прикладных задач по взаимодействию Клиента и Сервера с их использованием требуется принимать дополнительные меры для контроля того, что все посланные байты действительно посланы и, что еще более важно, получены в том же объеме на другой стороне

  • предельно внимательно надо относиться к параметру «размер буфера». Он должен в точности равняться реальному количеству передаваемых байт. Если он будет отличаться, то есть риск потери части информации или «замусориванию» отправляемой порции данных, что ведет к автоматической поломке данных в процессе отправки/приёма. И совсем замечательно будет, если размер буфера по итогу работы функции равен возвращаемому значению функции – размеру принятых/отправленных байт.

В качестве буфера рекомендую использовать не классические массивы в С-стиле, а стандартный класс С++ <vector> типа char, т.к. он показал себя как более надежный и гибкий механизм при передаче данных, в особенности при передаче текстовых строк, где важен терминальный символ и «чистота» передаваемого массива.

Сама по себе упаковка и отправка данных делается элементарным использованием функций чтения всей строки до нажатия кнопки Ввода — fgets() с последующим вызовом функции send(), а на другой стороне — приёмом информации через recv() и выводом буфера на экран через cout <<.

Процесс непрерывного перехода от send() к recv() и обратно реализуется через бесконечный цикл, из которого совершается выход по вводу особой комбинации клавиш. Пример блока кода для Серверной части:

vector <char> servBuff(BUFF_SIZE), clientBuff(BUFF_SIZE);	
short packet_size = 0;	

while (true) {
		packet_size = recv(ClientConn, servBuff.data(), servBuff.size(), 0);					
		cout << "Client's message: " << servBuff.data() << endl; 

		cout << "Your (host) message: ";
		fgets(clientBuff.data(), clientBuff.size(), stdin);

		// Check whether server would like to stop chatting 
		if (clientBuff[0] == 'x' && clientBuff[1] == 'x' && clientBuff[2] == 'x') {
			shutdown(ClientConn, SD_BOTH);
			closesocket(ServSock);
			closesocket(ClientConn);
			WSACleanup();
			return 0;
		}

		packet_size = send(ClientConn, clientBuff.data(), clientBuff.size(), 0);

		if (packet_size == SOCKET_ERROR) {
			cout << "Can't send message to Client. Error # " << WSAGetLastError() << endl;
			closesocket(ServSock);
			closesocket(ClientConn);
			WSACleanup();
			return 1;
		}

	}

Пришло время показать итоговый рабочий код для Сервера и Клиента. Чтобы не загромождать и так большой текст дополнительным кодом, даю ссылки на код на GitHub:

Исходный код для Сервера

Исходный код для Клиента

Несколько важных финальных замечаний:

  • В итоговом коде я не использую проверку на точное получение отосланной информации, т.к. при единичной (не циклической) отсылке небольшого пакета информации накладные расходы на проверку его получения и отправку ответа будут выше, чем выгоды от такой проверки. Иными словами – такие пакеты теряются редко, а проверять их целостность и факт доставки очень долго.

  • При тестировании примера также видно, что чат рабочий, но очень уж несовершенный. Наиболее проблемное место – невозможность отправить сообщение пока другая сторона не ответила на твоё предыдущее сообщение. Суть проблемы в том, что после отсылки сообщения сторона-отправитель вызывает функцию recv(), которая, как я писал выше, блокирует исполнение последующего кода, в том числе блокирует вызов прерываний для осуществления ввода. Это приводит к тому, что набирать сообщение и что-то отправлять невозможно до тех пор, пока процесс не получит ответ от другой стороны, и вызов функции recv() не будет завершен. Благо введенная информация с клавиатуры не будет потеряна, а, накапливаясь в системном буфере ввода/вывода, будет выведена на экран как только блокировка со стороны recv() будет снята. Таким образом, мы реализовали так называемый прямой полудуплексный канал связи. Сделать его полностью дуплексным в голой сокетной архитектуре достаточно нетривиальная задача, частично решаемая за счет создания нескольких параллельно работающих потоков или нитей (threads) исполнения. Один поток будет принимать информацию, а второй – отправлять.

В последующих статьях я покажу реализацию полноценного чата между двумя сторонами (поможет разобраться в понятии «нити процесса»), а также покажу полноценную реализацию прикладного протокола по копированию файлов с Сервера на Клиент.

Mr_Dezz

The root of your problem is when calling getaddrinfo(), you are filling out the hints structure incorrectly.

You are assigning SOCK_STREAM to the ai_protocol field, and leaving the ai_socktype field set to 0. SOCK_STREAM is defined as 1, which is the same value as IPPROTO_ICMP, which is commonly used with SOCK_DGRAM sockets. So, getaddrinfo() is likely returning addrinfo entries whose ai_socktype field is set to SOCK_DGRAM. You can’t use listen() on a datagram socket, thus the WSAEOPNOTSUPP error you are seeing:

If no error occurs, listen returns zero. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.

WSAEOPNOTSUPP
The referenced socket is not of a type that supports the listen operation.

You need to assign SOCK_STREAM to the hints.ai_socktype field instead, and set the hints.ai_protocol field to either 0 or IPPROTO_TCP (preferably the latter).

Also, getaddrinfo() returns an error code, just like WSAStartup() does. Don’t use WSAGetLastError() to get its error code.

Aside from that, I see a number of other issues with your code.

  • SO_REUSEADDR requires a BOOL (4 byte integer), not a char (1 byte). You are passing in a pointer to a single char, but are telling setsockopt() that you are passing in a pointer to an int. setsockopt() will end up trying to read a value from stack memory you don’t own.

  • When your loop calls closesocket(), you should reset sockfd to INVALID_SOCKET as well, and then after the loop you should check for that condition instead of checking ptr for NULL.

  • You should be calling listen() inside your loop instead of after it. Just because you bind() a socket successfully does not guarantee you can open its assigned listening port. You should keep looping until you actually open a listening port successfully. You might also consider adding an extra log message after the loop to know which local IP/Port pair is actually listening, so you know what clients are then able to connect() to.

  • When calling WSAGetLastError(), always call it immediately after a failed Winsock call. If you call anything else beforehand, you risk resetting the error code, since WSAGetLastError() is just an alias for GetLastError(), which many APIs use.

  • When calling accept(), you are using the = assignment operator instead of the == comparison operator when checking if m_exchfd is equal to INVALID_SOCKET. Even after you fix that, if accept() succeeds, you are leaking sockfd since you lose track of it and don’t call closesocket() on it. If you are expecting only 1 client to connect, close the listening socket after the client is accepted. Otherwise, store the listening socket in your class and close it after you close the accepted client socket.

With all of that said, try something more like this:

void OutputWinsockError(LPCWSTR funcName, int errCode)
{
    std::wostringstream msg;
    msg << L"> Log: " << funcName << L"()\n";
    OutputDebugStringW(msg.str().c_str());
    output_error(errCode);
}

void OutputWinsockError(LPCWSTR funcName)
{
    OutputWinsockError(funcName, WSAGetLastError());
}

int ConnectionManager::init_server() {

    // Log
    OutputDebugString(L"> Initializing server...\n");

    // Initialize winsock
    WSAData wsa;
    int err = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (err != 0) {
        // Error initializing winsock
        OutputWinsockError(L"WSAStartup", err);
        return -1;
    }

    // Get server information
    struct addrinfo hints, *serverinfo, *ptr;
    SOCKET sockfd = INVALID_SOCKET;

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    err = getaddrinfo(NULL, PORT, &hints, &serverinfo);
    if (err != 0) {
        // Error when getting server address information
        OutputWinsockError(L"getaddrinfo", err);
        // Call Cleanup?
        return -1;
    }

    for (ptr = serverinfo; ptr != NULL; ptr = ptr->ai_next) {
        // Create socket
        sockfd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (sockfd == INVALID_SOCKET) {
            // Error when creating a socket
            OutputWinsockError(L"socket");
            continue;
        }

        // Set options
        const BOOL enable = TRUE;
        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(BOOL)) == SOCKET_ERROR) {
            // Error when setting options
            OutputWinsockError(L"setsockopt");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                // Error when closing socket
                OutputWinsockError(L"closesocket");
            }
            sockfd = INVALID_SOCKET;
            continue;
        }

        // Bind socket
        if (bind(sockfd, ptr->ai_addr, ptr->ai_addrlen) == SOCKET_ERROR) {
            // Error on binding
            OutputWinsockError(L"bind");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                // Error when closing socket
                OutputWinsockError(L"closesocket");
            }
            sockfd = INVALID_SOCKET;
            continue;
        }    

        // Listen on port
        if (listen(sockfd, BACKLOG) == SOCKET_ERROR) {
            // Error on listening
            OutputWinsockError(L"listen");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                // Error when closing socket
                OutputWinsockError(L"closesocket");
            }
            sockfd = INVALID_SOCKET;
            continue;
        }

        break;
    }

    freeaddrinfo(serverinfo);

    if (sockfd == INVALID_SOCKET) {
        OutputDebugString(L"Error: Failed to launch server.\n");
        // Call Cleanup?
        return -1;
    }

    // Accept
    struct sockaddr_storage clientinfo;
    int size = sizeof(clientinfo);

    m_exchfd = accept(sockfd, (struct sockaddr *)&clientinfo, &size);
    if (m_exchfd == INVALID_SOCKET) {
        // Error when accepting
        OutputWinsockError(L"accept");
        if (closesocket(sockfd) == SOCKET_ERROR) {
            OutputWinsockError(L"closesocket");
        }
        // Call Cleanup?
        return -1;
    }

    m_isConnected = true;

    // is not storing sockfd, close it

    // m_listenfd = sockfd;
    if (closesocket(sockfd) == SOCKET_ERROR) {
        OutputWinsockError(L"closesocket");
    }

    return 0;
}

Корень вашей проблемы при звонке getaddrinfo() Вы заполняете hints структура неправильно.

Вы назначаете SOCK_STREAM к ai_protocol поле и оставив ai_socktype поле установлено в 0. SOCK_STREAM определяется как 1, что такое же значение, как IPPROTO_ICMP, который обычно используется с SOCK_DGRAM Розетки. Так, getaddrinfo() скорее всего возвращается addrinfo записи которых ai_socktype поле установлено в SOCK_DGRAM, Вы не можете использовать listen() на сокете дейтаграммы, таким образом, WSAEOPNOTSUPP ошибка, которую вы видите:

Если ошибки не возникает, listen возвращает ноль. В противном случае значение SOCKET_ERROR возвращается, и конкретный код ошибки может быть получен путем вызова WSAGetLastError,

WSAEOPNOTSUPP
Указанный сокет не относится к типу, который поддерживает listen операция.

Вам необходимо назначить SOCK_STREAM к hints.ai_socktype вместо этого, и установите hints.ai_protocol поле либо 0, либо IPPROTO_TCP (желательно последний).

Также, getaddrinfo() возвращает код ошибки, как WSAStartup() делает. Не использовать WSAGetLastError() чтобы получить код ошибки.

Помимо этого, я вижу ряд других проблем с вашим кодом.

  • SO_REUSEADDR требует BOOL (4-байтовое целое число), а не char (1 байт). Вы передаете указатель на один char, но рассказывают setsockopt() что вы передаете указатель на int, setsockopt() в итоге попытается прочитать значение из стековой памяти, которой вы не владеете.

  • Когда ваш цикл звонит closesocket(), вы должны сбросить sockfd в INVALID_SOCKET также, и затем после цикла вы должны проверить это условие вместо проверки ptr для NULL.

  • Вы должны звонить listen() внутри вашего цикла, а не после него. Только потому что ты bind() Успешно сокет не гарантирует, что вы можете открыть назначенный порт прослушивания. Вы должны продолжать цикл до тех пор, пока вы действительно не откроете прослушивающий порт успешно. Вы можете также рассмотреть возможность добавления дополнительного сообщения журнала после цикла, чтобы узнать, какая локальная пара IP/ порт на самом деле прослушивает, чтобы вы знали, какие клиенты затем могут connect() к.

  • При звонке WSAGetLastError() всегда вызывайте его сразу после неудачного вызова Winsock. Если вы позвоните заранее, вы рискуете сбросить код ошибки, так как WSAGetLastError() это просто псевдоним для GetLastError(), которые используют многие API.

  • При звонке accept() вы используете = оператор присваивания вместо == оператор сравнения при проверке, если m_exchfd равно INVALID_SOCKET, Даже если вы исправите это, если accept() успешно, вы протекаете sockfd так как вы теряете это и не звоните closesocket() в теме. Если вы ожидаете подключения только 1 клиента, закройте сокет прослушивания после того, как клиент будет принят. В противном случае сохраните прослушивающий сокет в своем классе и закройте его после закрытия принятого клиентского сокета.

С учетом всего сказанного, попробуйте что-то вроде этого:

void OutputWinsockError(LPCWSTR funcName, int errCode)
{
    std::wostringstream msg;
    msg << L"> Log: " << funcName << L"()\n";
    OutputDebugStringW(msg.str().c_str());
    output_error(errCode);
}

void OutputWinsockError(LPCWSTR funcName)
{
    OutputWinsockError(funcName, WSAGetLastError());
}

int ConnectionManager::init_server() {

    // Log
    OutputDebugString(L"> Initializing server...\n");

    // Initialize winsock
    WSAData wsa;
    int err = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (err != 0) {
        // Error initializing winsock
        OutputWinsockError(L"WSAStartup", err);
        return -1;
    }

    // Get server information
    struct addrinfo hints, *serverinfo, *ptr;
    SOCKET sockfd = INVALID_SOCKET;

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    err = getaddrinfo(NULL, PORT, &hints, &serverinfo);
    if (err != 0) {
        // Error when getting server address information
        OutputWinsockError(L"getaddrinfo", err);
        // Call Cleanup?
        return -1;
    }

    for (ptr = serverinfo; ptr != NULL; ptr = ptr->ai_next) {
        // Create socket
        sockfd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (sockfd == INVALID_SOCKET) {
            // Error when creating a socket
            OutputWinsockError(L"socket");
            continue;
        }

        // Set options
        const BOOL enable = TRUE;
        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(BOOL)) == SOCKET_ERROR) {
            // Error when setting options
            OutputWinsockError(L"setsockopt");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                // Error when closing socket
                OutputWinsockError(L"closesocket");
            }
            sockfd = INVALID_SOCKET;
            continue;
        }

        // Bind socket
        if (bind(sockfd, ptr->ai_addr, ptr->ai_addrlen) == SOCKET_ERROR) {
            // Error on binding
            OutputWinsockError(L"bind");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                // Error when closing socket
                OutputWinsockError(L"closesocket");
            }
            sockfd = INVALID_SOCKET;
            continue;
        }    

        // Listen on port
        if (listen(sockfd, BACKLOG) == SOCKET_ERROR) {
            // Error on listening
            OutputWinsockError(L"listen");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                // Error when closing socket
                OutputWinsockError(L"closesocket");
            }
            sockfd = INVALID_SOCKET;
            continue;
        }

        break;
    }

    freeaddrinfo(serverinfo);

    if (sockfd == INVALID_SOCKET) {
        OutputDebugString(L"Error: Failed to launch server.\n");
        // Call Cleanup?
        return -1;
    }

    // Accept
    struct sockaddr_storage clientinfo;
    int size = sizeof(clientinfo);

    m_exchfd = accept(sockfd, (struct sockaddr *)&clientinfo, &size);
    if (m_exchfd == INVALID_SOCKET) {
        // Error when accepting
        OutputWinsockError(L"accept");
        if (closesocket(sockfd) == SOCKET_ERROR) {
            OutputWinsockError(L"closesocket");
        }
        // Call Cleanup?
        return -1;
    }

    m_isConnected = true;

    // is not storing sockfd, close it

    // m_listenfd = sockfd;
    if (closesocket(sockfd) == SOCKET_ERROR) {
        OutputWinsockError(L"closesocket");
    }

    return 0;
}

Я следовал инструкциям https://www.digitalocean.com/community/tutorials/how-to-install-linux-nginx-mysql-php-lemp-stack-on-centos-7, шаг за шагом, но пока я перезапускал php-fpm, это не удалось.

Журнал ошибок:

Ошибка: невозможно привязать сокет прослушивания для адреса ‘var / run / php-fpm.d / www.conf’: такого файла или каталога нет
Ошибка: сбой инициализации FPM

Среда: CentOS 7 с установленными PHP, MariaDB и NginX был установлен в VirtualBox.

Вход / отчет:

Журнал показывает:

    localhost.localdomain php-fpm[2574]: Error: unable to bind listening socket for address '/var/run/php-fpm.d/www.conf' : No such file or directory
localhost.localdomain php-fpm[2574]:Error: FPM initialization failed
localhost.localdomain systemd[1]: php-fpm.service: main process exited, code=exited, status=78/n/a
localhost.localdomain systemd[1]: Failed to start the php fastCGI process manager.

Статус показывает:

    php-fpm.service - the php fastcgi process manager
Loaded: loaded (/usr/lib/systemd/system/php-fpm.service; disabled)
Active: failed (result: exit-cod )
Process: 2639 ExecStart=/usr/sbin/php-fpm --nodaemonize (code=exited,
status=78)
Main PID:2639 (code=exited, status=78)
localhost.localdomain php-fpm[2639]: Error: unable to bind listening socket for address '/var/run/php-fpm.d/www.conf' : No such file or directory
localhost.localdomain php-fpm[2639]: ERROR: FPM initialization failed
localhost.localdomain steam[1]: failed to start the php fastCGI process manager.
localhost.localdomain steam[1]: Unit php-fpm.service entered failed state.

Www.conf является

    [www]
listen = /var/run/php-fpm.d/www.conf
listen.allowed_clients = 127.0.0.1
user = apache
group = apache
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 5
slowlog = /var/log/php-fpm/www-slow.log
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session

Nginx default.conf — это

    server{
listen 80;
server_name ip address;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ = 404;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~\.php$ {
fastcgi_split_path_info ^(.+?\.php)(./*)$;
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
}

И, cgi.fix_pathinfo = 0 проверяется.

2

Решение

Создать каталог для sock-файла:

mkdir -p /var/run/

4

Другие решения

Других решений пока нет …

spaceship1226

5 / 5 / 1

Регистрация: 25.04.2019

Сообщений: 466

1

08.02.2023, 15:08. Показов 3957. Ответов 3

Метки нет (Все метки)


Студворк — интернет-сервис помощи студентам

Инициализирую сокет в кодом

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysocket = socket(AF_INET, SOCK_RAW, IPPRPTO_RAW);
     
    // SOCKET mysocket = socket(AF_INET, SOCK_STREAM, 0);
 
     if (mysocket == INVALID_SOCKET) {
         std::cout << "Error initialization socket # " << WSAGetLastError() << std::endl;
         closesocket(mysocket);
         WSACleanup();
         return 1;
     }
     else
     {
         std::cout << "Server socket initialization is OK" << std::endl;
         bindmysocket();
         return 0;
     }

И получаю ошибку инициализации сокета
Error initialization socket #10013.
Как устранить эту ошибку?



0



Programming

Эксперт

94731 / 64177 / 26122

Регистрация: 12.04.2006

Сообщений: 116,782

08.02.2023, 15:08

Ответы с готовыми решениями:

Не инициализируется hdd с ошибкой «отказано в доступе»
Купил новый hdd(wb black 2.5 на 1Тб), подключил, но инициализировать не смог. Всё остальное на…

Почему массив не инициализируется
Почему не работает пример выше (ни одно из: int N, const N, int const N, const int N).

Спасибо.

Почему не инициализируется вектор размером 10?
Хотел создать вектор размером 10, но visual studio выдаёт ошибку и при этом размер вектора равен 0,…

Почему массив инициализируется с 7-ю элементами?..
Друзья! Требуется помощь:)

Задача: вводим денежную сумму в формате $123,321,442 , выбираем из…

3

420 / 311 / 108

Регистрация: 30.08.2022

Сообщений: 1,199

08.02.2023, 15:52

2

Предположу что вы его уже открыли до этого

Добавлено через 3 минуты
А нет. Это же raw вообще



0



5 / 5 / 1

Регистрация: 25.04.2019

Сообщений: 466

11.02.2023, 11:37

 [ТС]

3

Ларчик просто открывался стоило запустить visual studio от имени администратора и ошибка исчезла



0



Неэпический

17848 / 10616 / 2049

Регистрация: 27.09.2012

Сообщений: 26,686

Записей в блоге: 1

11.02.2023, 13:23

4

Цитата
Сообщение от spaceship1226
Посмотреть сообщение

Error initialization socket #10013.

Идете и смотрите что за ошибка.
https://learn.microsoft.com/en… 000-11999-

WSAEACCES
10013 (0x271D)
An attempt was made to access a socket in a way forbidden by its access permissions.

Цитата
Сообщение от spaceship1226
Посмотреть сообщение

Ларчик просто открывался стоило запустить visual studio от имени администратора и ошибка исчезла

В документации об этом сказано.
https://learn.microsoft.com/en… ck2-socket

Note On Windows NT, raw socket support requires administrative privileges.



0



IT_Exp

Эксперт

87844 / 49110 / 22898

Регистрация: 17.06.2006

Сообщений: 92,604

11.02.2023, 13:23

4

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Ошибка инициализации системы защиты
  • Ошибка инициализации системы обновлений битрикс
  • Ошибка инициализации системы криптографии
  • Ошибка инициализации системы warhammer 40000 soulstorm
  • Ошибка инициализации симс 4 пиратка

  • Добавить комментарий

    ;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: