Сетевое программирование c windows

Начинающему сетевому программисту

Время на прочтение17 мин

Количество просмотров144K

Тема сетевого программирования является для разработчиков одной из важнейших в современном цифровом мире. Правда, надо признать, что большая часть сетевого программирования сосредоточена в области написания скриптов исполнения для 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

Socket programming with winsock

This is a quick guide/tutorial to learning socket programming in C language on Windows. «Windows» because the code snippets shown over here will work only on Windows. The windows api to socket programming is called winsock.

Sockets are the fundamental «things» behind any kind of network communications done by your computer. For example when you type www.google.com in your web browser, it opens a socket and connects to google.com to fetch the page and show it to you.

Same with any chat client like gtalk or skype. Any network communication goes through a socket.

Before you begin

This tutorial assumes that you have basic knowledge of C and pointers. Also download Visual C++ 2010 Express Edition.

Initialising Winsock

Winsock first needs to be initialiased like this :

/*
	Initialise Winsock
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	
	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.");

	return 0;
}

winsock2.h is the header file to be included for winsock functions. ws2_32.lib is the library file to be linked with the program to be able to use winsock functions.

The WSAStartup function is used to start or initialise winsock library. It takes 2 parameters ; the first one is the version we want to load and second one is a WSADATA structure which will hold additional information after winsock has been loaded.

If any error occurs then the WSAStartup function would return a non zero value and WSAGetLastError can be used to get more information about what error happened.

OK , so next step is to create a socket.

Creating a socket

The socket() function is used to create a socket.
Here is a code sample :

/*
	Create a TCP socket
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");

	return 0;
}

Function socket() creates a socket and returns a socket descriptor which can be used in other network commands. The above code will create a socket of :

Address Family : AF_INET (this is IP version 4)
Type : SOCK_STREAM (this means connection oriented TCP protocol)
Protocol : 0 [ or IPPROTO_TCP , IPPROTO_UDP ]

It would be a good idea to read some documentation here

Ok , so you have created a socket successfully. But what next ? Next we shall try to connect to some server using this socket. We can connect to www.google.com

Note

Apart from SOCK_STREAM type of sockets there is another type called SOCK_DGRAM which indicates the UDP protocol. This type of socket is non-connection socket. In this tutorial we shall stick to SOCK_STREAM or TCP sockets.

Connect to a Server

We connect to a remote server on a certain port number. So we need 2 things , IP address and port number to connect to.

To connect to a remote server we need to do a couple of things. First is create a sockaddr_in structure with proper values filled in. Lets create one for ourselves :

struct sockaddr_in server;

Have a look at the structures

// IPv4 AF_INET sockets:
struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // zero this if you want to
};


typedef struct in_addr {
  union {
    struct {
      u_char s_b1,s_b2,s_b3,s_b4;
    } S_un_b;
    struct {
      u_short s_w1,s_w2;
    } S_un_w;
    u_long S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;


struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

The sockaddr_in has a member called sin_addr of type in_addr which has a s_addr which is nothing but a long. It contains the IP address in long format.

Function inet_addr is a very handy function to convert an IP address to a long format. This is how you do it :

server.sin_addr.s_addr = inet_addr("74.125.235.20");

So you need to know the IP address of the remote server you are connecting to. Here we used the ip address of google.com as a sample. A little later on we shall see how to find out the ip address of a given domain name.

The last thing needed is the connect function. It needs a socket and a sockaddr structure to connect to. Here is a code sample.

/*
	Create a TCP socket
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;
	struct sockaddr_in server;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	
	server.sin_addr.s_addr = inet_addr("74.125.235.20");
	server.sin_family = AF_INET;
	server.sin_port = htons( 80 );

	//Connect to remote server
	if (connect(s , (struct sockaddr *)&server , sizeof(server)) < 0)
	{
		puts("connect error");
		return 1;
	}
	
	puts("Connected");

	return 0;
}

It cannot be any simpler. It creates a socket and then connects. If you run the program it should show Connected.
Try connecting to a port different from port 80 and you should not be able to connect which indicates that the port is not open for connection.

OK , so we are now connected. Lets do the next thing , sending some data to the remote server.

Quick Note

The concept of «connections» apply to SOCK_STREAM/TCP type of sockets. Connection means a reliable «stream» of data such that there can be multiple such streams each having communication of its own. Think of this as a pipe which is not interfered by other data.

Other sockets like UDP , ICMP , ARP dont have a concept of «connection». These are non-connection based communication. Which means you keep sending or receiving packets from anybody and everybody.

Sending Data

Function send will simply send data. It needs the socket descriptor , the data to send and its size.
Here is a very simple example of sending some data to google.com ip :

/*
	Create a TCP socket
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;
	struct sockaddr_in server;
	char *message;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	
	server.sin_addr.s_addr = inet_addr("74.125.235.20");
	server.sin_family = AF_INET;
	server.sin_port = htons( 80 );

	//Connect to remote server
	if (connect(s , (struct sockaddr *)&server , sizeof(server)) < 0)
	{
		puts("connect error");
		return 1;
	}
	
	puts("Connected");
	
	//Send some data
	message = "GET / HTTP/1.1\r\n\r\n";
	if( send(s , message , strlen(message) , 0) < 0)
	{
		puts("Send failed");
		return 1;
	}
	puts("Data Send\n");

	return 0;
}

In the above example , we first connect to an ip address and then send the string message «GET / HTTP/1.1\r\n\r\n» to it.
The message is actually a http command to fetch the mainpage of a website.

Now that we have send some data , its time to receive a reply from the server. So lets do it.

Receiving Data

Function recv is used to receive data on a socket. In the following example we shall send the same message as the last example and receive a reply from the server.

/*
	Create a TCP socket
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;
	struct sockaddr_in server;
	char *message , server_reply[2000];
	int recv_size;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	
	server.sin_addr.s_addr = inet_addr("74.125.235.20");
	server.sin_family = AF_INET;
	server.sin_port = htons( 80 );

	//Connect to remote server
	if (connect(s , (struct sockaddr *)&server , sizeof(server)) < 0)
	{
		puts("connect error");
		return 1;
	}
	
	puts("Connected");
	
	//Send some data
	message = "GET / HTTP/1.1\r\n\r\n";
	if( send(s , message , strlen(message) , 0) < 0)
	{
		puts("Send failed");
		return 1;
	}
	puts("Data Send\n");
	
	//Receive a reply from the server
	if((recv_size = recv(s , server_reply , 2000 , 0)) == SOCKET_ERROR)
	{
		puts("recv failed");
	}
	
	puts("Reply received\n");

	//Add a NULL terminating character to make it a proper string before printing
	server_reply[recv_size] = '
/*
	Create a TCP socket
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;
	struct sockaddr_in server;
	char *message , server_reply[2000];
	int recv_size;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	
	server.sin_addr.s_addr = inet_addr("74.125.235.20");
	server.sin_family = AF_INET;
	server.sin_port = htons( 80 );

	//Connect to remote server
	if (connect(s , (struct sockaddr *)&server , sizeof(server)) < 0)
	{
		puts("connect error");
		return 1;
	}
	
	puts("Connected");
	
	//Send some data
	message = "GET / HTTP/1.1\r\n\r\n";
	if( send(s , message , strlen(message) , 0) < 0)
	{
		puts("Send failed");
		return 1;
	}
	puts("Data Send\n");
	
	//Receive a reply from the server
	if((recv_size = recv(s , server_reply , 2000 , 0)) == SOCKET_ERROR)
	{
		puts("recv failed");
	}
	
	puts("Reply received\n");

	//Add a NULL terminating character to make it a proper string before printing
	server_reply[recv_size] = '\0';
	puts(server_reply);

	return 0;
}

';
puts(server_reply);

return 0;
}

Here is the output of the above code :

Initialising Winsock...Initialised.
Socket created.
Connected
Data Send

Reply received

HTTP/1.1 302 Found
Location: http://www.google.co.in/
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Set-Cookie: PREF=ID=7da819edfd7af808:FF=0:TM=1324882923:LM=1324882923:S=PdlMu0TE
E3QKrmdB; expires=Wed, 25-Dec-2013 07:02:03 GMT; path=/; domain=.google.com
Date: Mon, 26 Dec 2011 07:02:03 GMT
Server: gws
Content-Length: 221
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.in/">here</A>.
</BODY></HTML>

Press any key to continue

We can see what reply was send by the server. It looks something like Html, well IT IS html. Google.com replied with the content of the page we requested. Quite simple!

Now that we have received our reply, its time to close the socket.

Close socket

Function closesocket is used to close the socket. Also WSACleanup must be called to unload the winsock library (ws2_32.dll).

closesocket(s);
WSACleanup();

Thats it.

Lets Revise

So in the above example we learned how to :
1. Create a socket
2. Connect to remote server
3. Send some data
4. Receive a reply

Its useful to know that your web browser also does the same thing when you open www.google.com
This kind of socket activity represents a CLIENT. A client is a system that connects to a remote system to fetch or retrieve data.

The other kind of socket activity is called a SERVER. A server is a system that uses sockets to receive incoming connections and provide them with data. It is just the opposite of Client. So www.google.com is a server and your web browser is a client. Or more technically www.google.com is a HTTP Server and your web browser is an HTTP client.

Now its time to do some server tasks using sockets. But before we move ahead there are a few side topics that should be covered just incase you need them.

Get IP address of a hostname/domain

When connecting to a remote host , it is necessary to have its IP address. Function gethostbyname is used for this purpose. It takes the domain name as the parameter and returns a structure of type hostent. This structure has the ip information. It is present in netdb.h. Lets have a look at this structure

/* Description of data base entry for a single host.  */
struct hostent
{
  char *h_name;			/* Official name of host.  */
  char **h_aliases;		/* Alias list.  */
  int h_addrtype;		/* Host address type.  */
  int h_length;			/* Length of address.  */
  char **h_addr_list;		/* List of addresses from name server.  */
};

The h_addr_list has the IP addresses. So now lets have some code to use them.

/*
	Get IP address from domain name
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	char *hostname = "www.google.com";
	char ip[100];
	struct hostent *he;
	struct in_addr **addr_list;
	int i;
	
	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
		
	if ( (he = gethostbyname( hostname ) ) == NULL) 
	{
		//gethostbyname failed
		printf("gethostbyname failed : %d" , WSAGetLastError());
		return 1;
	}
	
	//Cast the h_addr_list to in_addr , since h_addr_list also has the ip address in long format only
	addr_list = (struct in_addr **) he->h_addr_list;
	
	for(i = 0; addr_list[i] != NULL; i++) 
	{
		//Return the first one;
		strcpy(ip , inet_ntoa(*addr_list[i]) );
	}
	
	printf("%s resolved to : %s\n" , hostname , ip);
	return 0;
	return 0;
}

Output of the code would look like :

www.google.com resolved to : 74.125.235.20

So the above code can be used to find the ip address of any domain name. Then the ip address can be used to make a connection using a socket.

Function inet_ntoa will convert an IP address in long format to dotted format. This is just the opposite of inet_addr.

So far we have see some important structures that are used. Lets revise them :

1. sockaddr_in — Connection information. Used by connect , send , recv etc.
2. in_addr — Ip address in long format
3. sockaddr
4. hostent — The ip addresses of a hostname. Used by gethostbyname

Server Concepts

OK now onto server things. Servers basically do the following :

1. Open a socket
2. Bind to a address(and port).
3. Listen for incoming connections.
4. Accept connections
5. Read/Send

We have already learnt how to open a socket. So the next thing would be to bind it.

Bind a socket

Function bind can be used to bind a socket to a particular address and port. It needs a sockaddr_in structure similar to connect function.

Lets see a code example :

/*
	Bind socket to port 8888 on localhost
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s;
	struct sockaddr_in server;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	//Prepare the sockaddr_in structure
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons( 8888 );
	
	//Bind
	if( bind(s ,(struct sockaddr *)&server , sizeof(server)) == SOCKET_ERROR)
	{
		printf("Bind failed with error code : %d" , WSAGetLastError());
	}
	
	puts("Bind done");
	
	closesocket(s);

	return 0;
}

Now that bind is done, its time to make the socket listen to connections. We bind a socket to a particular IP address and a certain port number. By doing this we ensure that all incoming data which is directed towards this port number is received by this application.

This makes it obvious that you cannot have 2 sockets bound to the same port.

Listen for connections

After binding a socket to a port the next thing we need to do is listen for connections. For this we need to put the socket in listening mode. Function listen is used to put the socket in listening mode. Just add the following line after bind.

//Listen
listen(s , 3);

Thats all. Now comes the main part of accepting new connections.

Accept connection

Function accept is used for this. Here is the code

/*
	Bind socket to port 8888 on localhost
*/

#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s , new_socket;
	struct sockaddr_in server , client;
	int c;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	//Prepare the sockaddr_in structure
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons( 8888 );
	
	//Bind
	if( bind(s ,(struct sockaddr *)&server , sizeof(server)) == SOCKET_ERROR)
	{
		printf("Bind failed with error code : %d" , WSAGetLastError());
	}
	
	puts("Bind done");
	

	//Listen to incoming connections
	listen(s , 3);
	
	//Accept and incoming connection
	puts("Waiting for incoming connections...");
	
	c = sizeof(struct sockaddr_in);
	new_socket = accept(s , (struct sockaddr *)&client, &c);
	if (new_socket == INVALID_SOCKET)
	{
		printf("accept failed with error code : %d" , WSAGetLastError());
	}
	
	puts("Connection accepted");

	closesocket(s);
	WSACleanup();

	return 0;
}

Output

Run the program. It should show

Initialising Winsock...Initialised.
Socket created.
Bind done
Waiting for incoming connections...

So now this program is waiting for incoming connections on port 8888. Dont close this program , keep it running.
Now a client can connect to it on this port. We shall use the telnet client for testing this. Open a terminal and type

telnet localhost 8888

And the server output will show

Initialising Winsock...Initialised.
Socket created.
Bind done
Waiting for incoming connections...
Connection accepted
Press any key to continue

So we can see that the client connected to the server. Try the above process till you get it perfect.

Note

You can get the ip address of client and the port of connection by using the sockaddr_in structure passed to accept function. It is very simple :

char *client_ip = inet_ntoa(client.sin_addr);
int client_port = ntohs(client.sin_port);

We accepted an incoming connection but closed it immediately. This was not very productive. There are lots of things that can be done after an incoming connection is established. Afterall the connection was established for the purpose of communication. So lets reply to the client.

Here is an example :

/*
	Bind socket to port 8888 on localhost
*/
#include<io.h>
#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s , new_socket;
	struct sockaddr_in server , client;
	int c;
	char *message;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	//Prepare the sockaddr_in structure
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons( 8888 );
	
	//Bind
	if( bind(s ,(struct sockaddr *)&server , sizeof(server)) == SOCKET_ERROR)
	{
		printf("Bind failed with error code : %d" , WSAGetLastError());
	}
	
	puts("Bind done");

	//Listen to incoming connections
	listen(s , 3);
	
	//Accept and incoming connection
	puts("Waiting for incoming connections...");
	
	c = sizeof(struct sockaddr_in);
	new_socket = accept(s , (struct sockaddr *)&client, &c);
	if (new_socket == INVALID_SOCKET)
	{
		printf("accept failed with error code : %d" , WSAGetLastError());
	}
	
	puts("Connection accepted");

	//Reply to client
	message = "Hello Client , I have received your connection. But I have to go now, bye\n";
	send(new_socket , message , strlen(message) , 0);
	
	getchar();

	closesocket(s);
	WSACleanup();
	
	return 0;
}

Run the above code in 1 terminal. And connect to this server using telnet from another terminal and you should see this :

Hello Client , I have received your connection. But I have to go now, bye

So the client(telnet) received a reply from server. We had to use a getchar because otherwise the output would scroll out of the client terminal without waiting

We can see that the connection is closed immediately after that simply because the server program ends after accepting and sending reply. A server like www.google.com is always up to accept incoming connections.

It means that a server is supposed to be running all the time. Afterall its a server meant to serve. So we need to keep our server RUNNING non-stop. The simplest way to do this is to put the accept in a loop so that it can receive incoming connections all the time.

Live Server

So a live server will be alive for all time. Lets code this up :

/*
	Live Server on port 8888
*/
#include<io.h>
#include<stdio.h>
#include<winsock2.h>

#pragma comment(lib,"ws2_32.lib") //Winsock Library

int main(int argc , char *argv[])
{
	WSADATA wsa;
	SOCKET s , new_socket;
	struct sockaddr_in server , client;
	int c;
	char *message;

	printf("\nInitialising Winsock...");
	if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
	{
		printf("Failed. Error Code : %d",WSAGetLastError());
		return 1;
	}
	
	printf("Initialised.\n");
	
	//Create a socket
	if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
	{
		printf("Could not create socket : %d" , WSAGetLastError());
	}

	printf("Socket created.\n");
	
	//Prepare the sockaddr_in structure
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons( 8888 );
	
	//Bind
	if( bind(s ,(struct sockaddr *)&server , sizeof(server)) == SOCKET_ERROR)
	{
		printf("Bind failed with error code : %d" , WSAGetLastError());
		exit(EXIT_FAILURE);
	}
	
	puts("Bind done");

	//Listen to incoming connections
	listen(s , 3);
	
	//Accept and incoming connection
	puts("Waiting for incoming connections...");
	
	c = sizeof(struct sockaddr_in);
	
	while( (new_socket = accept(s , (struct sockaddr *)&client, &c)) != INVALID_SOCKET )
	{
		puts("Connection accepted");
		
		//Reply to the client
		message = "Hello Client , I have received your connection. But I have to go now, bye\n";
		send(new_socket , message , strlen(message) , 0);
	}
	
	if (new_socket == INVALID_SOCKET)
	{
		printf("accept failed with error code : %d" , WSAGetLastError());
		return 1;
	}

	closesocket(s);
	WSACleanup();
	
	return 0;
}

We havent done a lot there. Just the accept was put in a loop.

Now run the program in 1 terminal , and open 3 other terminals. From each of the 3 terminal do a telnet to the server port.

Run telnet like this. It will launch the interactive prompt.

C:\>telnet

At the telnet shell, run the command «open localhost 8888». This command will try to connect to localhost on port number 8888.

Welcome to Microsoft Telnet Client
Escape Character is 'CTRL+]'
Microsoft Telnet> open localhost 8888

Next you should see the following message at the telnet prompt. This message is received from the socket server running on port 8888.

Hello Client , I have received your connection. But I have to go now, bye

On the other hand, the server terminal would show the following messages, indicating that a client connected to it.

Initialising Winsock...Initialised.
Socket created.
Bind done
Waiting for incoming connections...
Connection accepted
Connection accepted

So now the server is running nonstop and the telnet terminals are also connected nonstop. Now close the server program.

All telnet terminals would show «Connection to host lost.»
Good so far. But still there is not effective communication between the server and the client.

The server program accepts connections in a loop and just send them a reply, after that it does nothing with them. Also it is not able to handle more than 1 connection at a time. So now its time to handle the connections , and handle multiple connections together.

Handling Connections

To handle every connection we need a separate handling code to run along with the main server accepting connections.
One way to achieve this is using threads. The main server program accepts a connection and creates a new thread to handle communication for the connection, and then the server goes back to accept more connections.

We shall now use threads to create handlers for each connection the server accepts. Lets do it pal.


Run the above server and open 3 terminals like before. Now the server will create a thread for each client connecting to it.

The telnet terminals would show :


This one looks good , but the communication handler is also quite dumb. After the greeting it terminates. It should stay alive and keep communicating with the client.

One way to do this is by making the connection handler wait for some message from a client as long as the client is connected. If the client disconnects , the connection handler ends.

So the connection handler can be rewritten like this :


The above connection handler takes some input from the client and replies back with the same. Simple! Here is how the telnet output might look


So now we have a server thats communicative. Thats useful now.

Conclusion

The winsock api is quite similar to Linux sockets in terms of function name and structures. Few differences exist like :

1. Winsock needs to be initialised with the WSAStartup function. No such thing in linux.

2. Header file names are different. Winsock needs winsock2.h , whereas Linux needs socket.h , apra/inet.h , unistd.h and many others.

3. Winsock function to close a socket is closesocket , whereas on Linux it is close.
On Winsock WSACleanup must also be called to unload the winsock dll.

4. On winsock the error number is fetched by the function WSAGetLastError(). On Linux the errno variable from errno.h file is filled with the error number.

And there are many more differences as we go deep.

By now you must have learned the basics of socket programming in C. You can try out some experiments like writing a chat client or something similar.

If you think that the tutorial needs some addons or improvements or any of the code snippets above dont work then feel free to make a comment below so that it gets fixed.

Пишем консольный сервер и клиент на C++ в Windows 32/64

Как обычно, создаём новый пустой проект C++, но для решения указываем имя, например, ClientServer, а для проекта — другое имя, пусть будет Server.

Потом ещё раз создаём пустой проект с именем Client, но добавляем его в то то же самое решение.

Жмём правую кнопку мыши на «исходных файлах» приложения-сервера, выбираем команды Добавить, Создать элемент, Visual C++, файл C++, имя файла с исходным текстом можно оставить Source.cpp (по умолчанию).

Вот полный исходник этого файла, откомпилированный в Visual Studio 2019 под актуальной сборкой Windows 10.

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream> 
#include <cstdio> 
#include <cstring> 
#include <winsock2.h> 
#pragma comment(lib, "WS2_32.lib")
using namespace std;

DWORD WINAPI serverReceive(LPVOID lpParam) { //Получение данных от клиента
 char buffer[1024] = { 0 }; //Буфер для данных
 SOCKET client = *(SOCKET*)lpParam; //Сокет для клиента
 while (true) { //Цикл работы сервера
  if (recv(client, buffer, sizeof(buffer), 0) == SOCKET_ERROR) {
   //Если не удалось получить данные буфера, сообщить об ошибке и выйти
   cout << "recv function failed with error " << WSAGetLastError() << endl;
   return -1;
  }
  if (strcmp(buffer, "exit\n") == 0) { //Если клиент отсоединился
   cout << "Client Disconnected." << endl;
   break;
  }
  cout << "Client: " << buffer << endl; //Иначе вывести сообщение от клиента из буфера
  memset(buffer, 0, sizeof(buffer)); //Очистить буфер
 }
 return 1;
}

DWORD WINAPI serverSend(LPVOID lpParam) { //Отправка данных клиенту
 char buffer[1024] = { 0 };
 SOCKET client = *(SOCKET*)lpParam;
 while (true) {
  fgets(buffer, 1024, stdin);
  if (send(client, buffer, sizeof(buffer), 0) == SOCKET_ERROR) {
   cout << "send failed with error " << WSAGetLastError() << endl;
   return -1;
  }
  if (strcmp(buffer, "exit\n") == 0) {
   cout << "Thank you for using the application" << endl;
   break;
  }
 }
 return 1;
}

int main() {
 WSADATA WSAData; //Данные 
 SOCKET server, client; //Сокеты сервера и клиента
 SOCKADDR_IN serverAddr, clientAddr; //Адреса сокетов
 WSAStartup(MAKEWORD(2, 0), &WSAData);
 server = socket(AF_INET, SOCK_STREAM, 0); //Создали сервер
 if (server == INVALID_SOCKET) {
  cout << "Socket creation failed with error:" << WSAGetLastError() << endl;
  return -1;
 }
 serverAddr.sin_addr.s_addr = INADDR_ANY;
 serverAddr.sin_family = AF_INET;
 serverAddr.sin_port = htons(5555);
 if (bind(server, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
  cout << "Bind function failed with error: " << WSAGetLastError() << endl;
  return -1;
 }

 if (listen(server, 0) == SOCKET_ERROR) { //Если не удалось получить запрос
  cout << "Listen function failed with error:" << WSAGetLastError() << endl;
  return -1;
 }
 cout << "Listening for incoming connections...." << endl; 

 char buffer[1024]; //Создать буфер для данных
 int clientAddrSize = sizeof(clientAddr); //Инициализировать адерс клиента
 if ((client = accept(server, (SOCKADDR*)&clientAddr, &clientAddrSize)) != INVALID_SOCKET) {
  //Если соединение установлено
  cout << "Client connected!" << endl;
  cout << "Now you can use our live chat application. " << "Enter \"exit\" to disconnect" << endl;

  DWORD tid; //Идентификатор
  HANDLE t1 = CreateThread(NULL, 0, serverReceive, &client, 0, &tid); //Создание потока для получения данных
  if (t1 == NULL) { //Ошибка создания потока
   cout << "Thread Creation Error: " << WSAGetLastError() << endl;
  }
  HANDLE t2 = CreateThread(NULL, 0, serverSend, &client, 0, &tid); //Создание потока для отправки данных
  if (t2 == NULL) {
   cout << "Thread Creation Error: " << WSAGetLastError() << endl;
  }

  WaitForSingleObject(t1, INFINITE);
  WaitForSingleObject(t2, INFINITE);

  closesocket(client); //Закрыть сокет
  if (closesocket(server) == SOCKET_ERROR) { //Ошибка закрытия сокета
   cout << "Close socket failed with error: " << WSAGetLastError() << endl;
   return -1;
  }
  WSACleanup();
 }
}

Я выбрал в стандартной панели инструментов конфигурацию Debug — X64. Если нужно, разрешить приложению работать с сетью, подтверждаем это.

Разрешить консольному приложению сервера доступ к сети

Разрешить консольному приложению сервера доступ к сети

Аналогично добавляем файл Source.cpp в приложение Client, вот его полный исходный код, так как он, в сущности, очень похож, можно без столь подробных комментариев:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream> 
#include <cstdio> 
#include <cstring> 
#include <winsock2.h> 
#pragma comment(lib, "WS2_32.lib")
using namespace std;

DWORD WINAPI clientReceive(LPVOID lpParam) { //Получение данных от сервера
 char buffer[1024] = { 0 };
 SOCKET server = *(SOCKET*)lpParam;
 while (true) {
  if (recv(server, buffer, sizeof(buffer), 0)  == SOCKET_ERROR) {
   cout << "recv function failed with error: " << WSAGetLastError() << endl;
   return -1;
  }
  if (strcmp(buffer, "exit\n") == 0) {
   cout << "Server disconnected." << endl;
   return 1;
  }
  cout << "Server: " << buffer << endl;
  memset(buffer, 0, sizeof(buffer));
 }
 return 1;
}

DWORD WINAPI clientSend(LPVOID lpParam) { //Отправка данных на сервер
 char buffer[1024] = { 0 };
 SOCKET server = *(SOCKET*)lpParam;
 while (true) {
  fgets(buffer, 1024, stdin);
  if (send(server, buffer, sizeof(buffer), 0) == SOCKET_ERROR) {
   cout << "send failed with error: " << WSAGetLastError() << endl;
   return -1;
  }
  if (strcmp(buffer, "exit") == 0) {
   cout << "Thank you for using the application" << endl;
   break;
  }
 }
 return 1;
}

int main() {
 WSADATA WSAData;
 SOCKET server;
 SOCKADDR_IN addr;
 WSAStartup(MAKEWORD(2, 0), &WSAData);
 if ((server = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
  cout << "Socket creation failed with error: " << WSAGetLastError() << endl;
  return -1;
 }
 
 addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //коннект к серверу
 addr.sin_family = AF_INET;
 addr.sin_port = htons(5555); //порт
 if (connect(server, (SOCKADDR*)&addr, sizeof(addr)) == SOCKET_ERROR) {
  cout << "Server connection failed with error: " << WSAGetLastError() << endl;
  return -1;
 }
 
 cout << "Connected to server!" << endl;
 cout << "Now you can use our live chat application. "  << " Enter \"exit\" to disconnect" << endl;
 
 DWORD tid;
 HANDLE t1 = CreateThread(NULL, 0, clientReceive, &server, 0, &tid);
 if (t1 == NULL) cout << "Thread creation error: " << GetLastError();
 HANDLE t2 = CreateThread(NULL, 0, clientSend, &server, 0, &tid);
 if (t2 == NULL) cout << "Thread creation error: " << GetLastError();

 WaitForSingleObject(t1, INFINITE);
 WaitForSingleObject(t2, INFINITE);
 closesocket(server);
 WSACleanup();
}

Обратите внимание на директивы покдлючения библиотек в начале обоих листингов.

В Обозревателе Решений жмём правую кнопку на заголовке решения (самого верхнего уровня), выбираем «Назначить запускаемые проекты», подтверждаем запуск нескольких проектов.

Запуск нескольких проектов из решения

Запуск нескольких проектов из решения

Наши клиент и сервер могут обмениваться сообщениями в режиме чата, конечно, функционал нетрудно расширить. Работа приложения предполагает, что у вас есть нужные компоненты в системе и настройки стандартны, в частности, доступен стандартный «локалхост» с IP-адресом 127.0.0.1

Проект в работе

Проект в работе

 Скачать это решение в архиве .zip, главная папка решения уже создана в архиве (6 Кб)

Так как опции решения хранятся в файле .suo вместе с асболютными путями к файлам проекта (а у вас эти пути точно не совпадут с моими), то действие со второго рисунка («Запуск нескольких проектов из решения») в скачанном проекте будет не настроено.

28.07.2020, 01:02 [29290 просмотров]


How to create a client-server network for multiplayer game programming using C++ and Windows Winsock

  • Download simple_network.zip

Introduction

I recently finished a video game design and implementation course at UC San Diego and was in charge of the networking side of the video game along with another teammate. I want to write a guide to illustrate how to set up a simple client-server network using C++, the Windows Winsock 2 library, and TCP. By the end of this guide, you will have simple packets sent and received between the server and the client.

Background

Microsoft provides very useful guides on how to go about creating a client and a server using their Winsock library but I just wanted to add clarification and guide those who are using it for game programming and explain how I used it. I connected the dots so that you don’t have to

I highly recommend that you follow this tutorial using Microsoft Visual Studio. I am using version 2010 Ultimate and my project is a Visual C++ Win32 Console Application.

Client connecting to Server

The server and the client will each have their own sockets, which they will use to send and receive data through a TCP connection. First we will create a class which will wrap the send and receive functions of the Winsock 2 library into a more simpler form for better readability. We will use this class inside our server and client network classes to send and receive data. This class is not completely necessary but will make our code more simpler to understand in the future.

Lets create a class called «NetworkServices». In its header file («NetworkService.h») include the following libraries:

#pragma once
#include <winsock2.h>
#include <Windows.h>

The above libraries contain the functions required for the receive and send functions of the Winsock library.

This class will not need a constructor/deconstructor. Also, declare the following static wrapper functions which will be used by both the server and the client:

class NetworkServices
{

public:

static int sendMessage(SOCKET curSocket, char * message, int messageSize);
static int receiveMessage(SOCKET curSocket, char * buffer, int bufSize);

};

Now lets write the definitions to our wrapper functions and use the Winsock library functions to send and receive data inside the class (NetworkServices.cpp):

**Note: I have #included «stdafx.h», a file that is automatically generated by Visual Studio, in my cpp files. You may not need this, in which case you may remove the line. However, users have reported errors when not including this file when working in VS.

#include "stdafx.h" 
#include "NetworkServices.h"

int NetworkServices::sendMessage(SOCKET curSocket, char * message, int messageSize)
{
    return send(curSocket, message, messageSize, 0);
}

sendMessage() takes a socket type object to send the message through, a pointer to the buffer where the message we want to send is located, and the size of the message. We then call send() of the Winsock library and provide it with the necessary information. The «0» provided to send() is usually used if you’d want to set flags for send() to work in a different way than its default but in our case, we do not need to set any flags. The send function will return an int value representing the number of bytes it successfully sent or an error value if there was a problem sending through the socket. Make sure to error check this value in your real application. If you’d like to see how the send function works then go here.

int NetworkServices::receiveMessage(SOCKET curSocket, char * buffer, int bufSize)
{
    return recv(curSocket, buffer, bufSize, 0);
}

receiveMessage() takes a socket type object to check for any data that is available for reading at that socket on the network. It will place any data read into our «buffer», and will require a buffer size to indicate how much is the maximum it can read each time receiveMessage() is called. We then provide this information to recv() which is the Winsock library function for receiving data from a socket. It will also return an int value representing the number of bytes it read into our buffer or an error if there was a problem receiving from the socket. Make sure to error check this value in your real application. The «0» provided to recv() works the same way as the one in send, here is more information on recv().

We now create the client side of the network. Declare a ClientNetwork header file called «ClientNetwork.h» and include the following libraries:

// Networking libraries
#include <winsock2.h>
#include <Windows.h>
#include "NetworkServices.h"
#include <ws2tcpip.h>
#include <stdio.h> 

and define the following constants and library links.

// size of our buffer
#define DEFAULT_BUFLEN 512
// port to connect sockets through 
#define DEFAULT_PORT "6881"
// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

Declare the following variables/functions in the header file:

class ClientNetwork
{

public:

    // for error checking function calls in Winsock library
    int iResult;

    // socket for client to connect to server
    SOCKET ConnectSocket;

    // ctor/dtor
    ClientNetwork(void);
    ~ClientNetwork(void);
};


We are ready to implement client connection to server. In the constructor for ClientNetwork inside ClientNetwork.cpp, initialize the Winsock. I will not go in depth with the explanation for how this works. You may check the Microsoft library if you wish to learn more. However, you will not most likely need to change these values.

#include "stdafx.h"
#include "ClientNetwork.h"

ClientNetwork::ClientNetwork(void) {

    // create WSADATA object
    WSADATA wsaData;

    // socket
    ConnectSocket = INVALID_SOCKET;

    // holds address info for socket to connect to
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);

    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        exit(1);
    }



    // set address info
    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;  //TCP connection!!!

Currently, I have set the address for the server to be localhost, you may change this if you’d like.

    //resolve server address and port 
    iResult = getaddrinfo("127.0.0.1", DEFAULT_PORT, &hints, &result);

    if( iResult != 0 ) 
    {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        exit(1);
    }

    // Attempt to connect to an address until one succeeds
    for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
            ptr->ai_protocol);

        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            exit(1);
        }

        // Connect to server.
        iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);

        if (iResult == SOCKET_ERROR)
        {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            printf ("The server is down... did not connect");
        }
    }



    // no longer need address info for server
    freeaddrinfo(result);



    // if connection failed
    if (ConnectSocket == INVALID_SOCKET) 
    {
        printf("Unable to connect to server!\n");
        WSACleanup();
        exit(1);
    }


Here is an important piece of information. We are going to set our socket to be non-blocking so that it will not wait on send() and receive() functions when there is no data to send/receive. This is necessary for our multiplayer game since we’d like the game to keep going if there isn’t anything to send or receive to or from a client.

    // Set the mode of the socket to be nonblocking
    u_long iMode = 1;

    iResult = ioctlsocket(ConnectSocket, FIONBIO, &iMode);
    if (iResult == SOCKET_ERROR)
    {
        printf("ioctlsocket failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        exit(1);        
    }


we will also disable nagle. (you don’t have to)

    //disable nagle
    char value = 1;
    setsockopt( ConnectSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof( value ) );

}


This is the end of our constructor for our ClientNetwork.

Now lets switch and go to setting up our ServerNetwork class.

In the header file, include the following libraries:

#include <winsock2.h>
#include <Windows.h>
#include "NetworkServices.h"
#include <ws2tcpip.h>
#include <map>
using namespace std; 
#pragma comment (lib, "Ws2_32.lib")


and define the following constants:

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "6881" 

Here is also what we need to declare in the header file:

class ServerNetwork
{

public:

    ServerNetwork(void);
    ~ServerNetwork(void);

    // Socket to listen for new connections
    SOCKET ListenSocket;

    // Socket to give to the clients
    SOCKET ClientSocket;

    // for error checking return values
    int iResult;

    // table to keep track of each client's socket
    std::map<unsigned int, SOCKET> sessions; 
};



Let’s define the ServerNetwork constructor inside the cpp file:

#include "stdafx.h" 
#include "ServerNetwork.h"

ServerNetwork::ServerNetwork(void)
{

    // create WSADATA object
    WSADATA wsaData;

    // our sockets for the server
    ListenSocket = INVALID_SOCKET;
    ClientSocket = INVALID_SOCKET;



    // address info for the server to listen to
    struct addrinfo *result = NULL;
    struct addrinfo hints;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        exit(1);
    }

    // set address information
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;    // TCP connection!!!
    hints.ai_flags = AI_PASSIVE;


The server will not need an address since it will be on the local machine.

    // Resolve the server address and port
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);

    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        exit(1);
    }

    // Create a SOCKET for connecting to server
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);

    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        exit(1);
    }

    // Set the mode of the socket to be nonblocking
    u_long iMode = 1;
    iResult = ioctlsocket(ListenSocket, FIONBIO, &iMode);

    if (iResult == SOCKET_ERROR) {
        printf("ioctlsocket failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        exit(1);
    }

    // Setup the TCP listening socket
    iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);

    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        exit(1);
    }

    // no longer need address information
    freeaddrinfo(result);

    // start listening for new clients attempting to connect
    iResult = listen(ListenSocket, SOMAXCONN);

    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        exit(1);
    }
}


This completes our server initialization. Now we will need to accept clients that attempt to connect.

Add the following declaration to the ServerNetwork header file:

    // accept new connections
    bool acceptNewClient(unsigned int & id);


And here is the definition. This function will take an ID (a way for us to track this specific client) and once a client is connected, will add their assigned socket and ID to our table. It will then return true if there was a new client added.

// accept new connections
bool ServerNetwork::acceptNewClient(unsigned int & id)
{
    // if client waiting, accept the connection and save the socket
    ClientSocket = accept(ListenSocket,NULL,NULL);

    if (ClientSocket != INVALID_SOCKET) 
    {
        //disable nagle on the client's socket
        char value = 1;
        setsockopt( ClientSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof( value ) );

        // insert new client into session id table
        sessions.insert( pair<unsigned int, SOCKET>(id, ClientSocket) );

        return true;
    }

    return false;
}

Now we will create a ServerGame class. In there we will declare and initialize a ServerNetwork object which will be used as our network. In the larger scope of the game, ServerGame should hold every information about the game on the server, including the information about the clients on the network. You will see as we go along. Create a ServerGame.h file:

#include "ServerNetwork.h"

class ServerGame
{

public:

    ServerGame(void);
    ~ServerGame(void);

    void update();

private:

   // IDs for the clients connecting for table in ServerNetwork 
    static unsigned int client_id;

   // The ServerNetwork object 
    ServerNetwork* network;
};


In its cpp file:

#include "stdafx.h" 
#include "ServerGame.h"

unsigned int ServerGame::client_id; 

ServerGame::ServerGame(void)
{
    // id's to assign clients for our table
    client_id = 0;

    // set up the server network to listen 
    network = new ServerNetwork(); 
}

void ServerGame::update() 
{

    // get new clients
    if(network->acceptNewClient(client_id))
    {
        printf("client %d has been connected to the server\n",client_id); 

        client_id++;
    }
}


The code inside the constructor of ServerGame will set up all of the networking we did earlier inside of ServerNetwork and will have its socket listen for incoming connections. In ServerGame::update() we will accept new clients that are trying to connect and assign them the current client_id and increment if the ID is assigned to them to prepare for the next client.

Lets create a ClientGame which should hold all of the client side information, including the network.

Create a header file named «ClientGame.h»:

#include <winsock2.h>
#include <Windows.h>
#include "ClientNetwork.h"

and declare these variables/functions:

class ClientGame
{

public:

    ClientGame();
    ~ClientGame(void);

    ClientNetwork* network; 
};

Inside the cpp file:

#include "stdafx.h"
#include "ClientGame.h" 

ClientGame::ClientGame(void)
{
    network = new ClientNetwork();
}

We are finally ready to check if the client will actually connect.

Create a main.cpp and include these libraries at the top:

// may need #include "stdafx.h" in visual studio
#include "stdafx.h"
#include "ServerGame.h"
#include "ClientGame.h"
// used for multi-threading
#include <process.h>

and make these global variables/functions in main.cpp:

void serverLoop(void *);
void clientLoop(void);

ServerGame * server;
ClientGame * client;


Here is the main function:

int main()
{

   // initialize the server
    server = new ServerGame();


We will run the server on a separate thread, so that it will always be checking for new clients. It will also be sending and receiving data in the future.

    // create thread with arbitrary argument for the run function
    _beginthread( serverLoop, 0, (void*)12);

    // initialize the client 
    client = new ClientGame();

This is our game loop. It will hold the console so that we can see the output from ServerNetwork and it can be extended to do other things.

    clientLoop();
}



Add these functions inside main.cpp, serverLoop will be ran by the server thread and clientLoop by the main thread :

void serverLoop(void * arg) 
{ 
    while(true) 
    {
        server->update();
    }
}

void clientLoop()
{
    while(true)
    {
        //do game stuff
       //will later run client->update();
    }
}


Congratulations! you now have a client connected to the server. If you run the program, you should see a message in console stating that client 0 has been connected to the server.

Sending and Receiving Data

Now we need to set up some data to send. Lets create a header file called «NetworkData.h». We do not need a cpp file. This file will just define some data types for us.

In this header file:

#include <string.h>

#define MAX_PACKET_SIZE 1000000

enum PacketTypes {

    INIT_CONNECTION = 0,

    ACTION_EVENT = 1,

}; 



and now we will define a packet struct to serve as a container to our data to send:

struct Packet {

    unsigned int packet_type;

    void serialize(char * data) {
        memcpy(data, this, sizeof(Packet));
    }

    void deserialize(char * data) {
        memcpy(this, data, sizeof(Packet));
    }
};


The packet_type field will be filled using the values in the enumerator we just created.

The serialize function is used to convert the packet_type data into bytes that we can send over the network.

The deserialize function is used to convert bytes received over the network back into packet_type data that we can interpret.

You will need to include NetworkData.h inside your ServerGame, ServerNetwork, ClientGame, and ClientNetwork classes!

Lets have the client send an INIT_CONNECTION packet when it is first connected to the server inside its constructor:

ClientGame::ClientGame(void)
{

    network = new ClientNetwork();

    // send init packet
    const unsigned int packet_size = sizeof(Packet);
    char packet_data[packet_size];

    Packet packet;
    packet.packet_type = INIT_CONNECTION;

    packet.serialize(packet_data);

    NetworkServices::sendMessage(network->ConnectSocket, packet_data, packet_size);
}

We just created a packet and gave it an INIT_CONNECTION type and sent it over the network using the sendMessage() function which we wrote earlier. We used the socket that the client had used to connect to the server.

Now lets read that message on the server side.

Add the following function to ServerNetwork’s header file:

under public:

    // receive incoming data
    int receiveData(unsigned int client_id, char * recvbuf);

Then this for the cpp definition:

// receive incoming data
int ServerNetwork::receiveData(unsigned int client_id, char * recvbuf)
{
    if( sessions.find(client_id) != sessions.end() )
    {
        SOCKET currentSocket = sessions[client_id];
        iResult = NetworkServices::receiveMessage(currentSocket, recvbuf, MAX_PACKET_SIZE);
        if (iResult == 0)
        {
            printf("Connection closed\n");
            closesocket(currentSocket);
        }
        return iResult;
    }
    return 0;
} 

The above function will receive data waiting on the socket for a given client ID and fill the passed buffer (recvbuf) with the data read from the network.

Lets call it from the ServerGame to read data sent from the client.

Add the following to the header file of ServerGame:

under public:

    void receiveFromClients();

under private:

   // data buffer
   char network_data[MAX_PACKET_SIZE];

Define receiveFromClients() in the cpp file. In this function we go through all the clients that we saved in the sessions table earlier as each were connected. Then we call receiveData() on all of them and get the data insideour network_data buffer. We then deserialize the packet and switch between whether it is an INIT or ACTION packet. In this same way, you may extend this program for different packets and struct containers of different sizes to do different things.

void ServerGame::receiveFromClients()
{
    Packet packet;

    // go through all clients
    std::map<unsigned int, SOCKET>::iterator iter;

    for(iter = network->sessions.begin(); iter != network->sessions.end(); iter++)
    {
        // get data for that client
        int data_length = network->receiveData(iter->first, network_data);

        if (data_length <= 0) 
        {
            //no data recieved
            continue;
        }

        int i = 0;
        while (i < (unsigned int)data_length) 
        {
            packet.deserialize(&(network_data[i]));
            i += sizeof(Packet);

            switch (packet.packet_type) {

                case INIT_CONNECTION:

                    printf("server received init packet from client\n");

                    break;

                case ACTION_EVENT:

                    printf("server received action event packet from client\n");

                    break;

                default:

                    printf("error in packet types\n");

                    break;
            }
        }
    }
}

Now call receiveFromClients() inside of ServerGame::update():

void ServerGame::update()
{
    // get new clients
   if(network->acceptNewClient(client_id))
   {
        printf("client %d has been connected to the server\n",client_id);

        client_id++;
   }

   receiveFromClients();
}


If you run the program now, you should see a message for client connecting to the server and then another message stating that the server has received an INIT packet.

Lets extend the program for the clients to send ACTION packets to the server and the server to send ACTION packets to the clients.

The client will send an INIT packet, then the server will send an ACTION packet and upon receipt, the client will send an ACTION packet, and then the server will receive the ACTION packet, and send another, and etc.

In ServerNetwork, add a function which will send messages to all connected clients.

In the header file:

    // send data to all clients
    void sendToAll(char * packets, int totalSize);

In the cpp file:

// send data to all clients
void ServerNetwork::sendToAll(char * packets, int totalSize)
{
    SOCKET currentSocket;
    std::map<unsigned int, SOCKET>::iterator iter;
    int iSendResult;

    for (iter = sessions.begin(); iter != sessions.end(); iter++)
    {
        currentSocket = iter->second;
        iSendResult = NetworkServices::sendMessage(currentSocket, packets, totalSize);

        if (iSendResult == SOCKET_ERROR) 
        {
            printf("send failed with error: %d\n", WSAGetLastError());
            closesocket(currentSocket);
        }
    }
}

Inside ServerGame.h, add the following declaration under public fields:

    void sendActionPackets();


Inside the ServerGame.cpp, add the following function which will send action packets to all connected clients:

void ServerGame::sendActionPackets()
{
    // send action packet
    const unsigned int packet_size = sizeof(Packet);
    char packet_data[packet_size];

    Packet packet;
    packet.packet_type = ACTION_EVENT;

    packet.serialize(packet_data);

    network->sendToAll(packet_data,packet_size);
}

Modify the ServerGame::receiveFromClients() to the following:

void ServerGame::receiveFromClients()
{

    Packet packet;

    // go through all clients
    std::map<unsigned int, SOCKET>::iterator iter;

    for(iter = network->sessions.begin(); iter != network->sessions.end(); iter++)
    {
        int data_length = network->receiveData(iter->first, network_data);

        if (data_length <= 0) 
        {
            //no data recieved
            continue;
        }

        int i = 0;
        while (i < (unsigned int)data_length) 
        {
            packet.deserialize(&(network_data[i]));
            i += sizeof(Packet);

            switch (packet.packet_type) {

                case INIT_CONNECTION:

                    printf("server received init packet from client\n");

                    sendActionPackets();

                    break;

                case ACTION_EVENT:

                    printf("server received action event packet from client\n");

                    sendActionPackets();

                    break;

                default:

                    printf("error in packet types\n");

                    break;
            }
        }
    }
}


Lets add functionality for clients to receive packets from the server.

In ClientNetwork.h add the following under public fields:

    int receivePackets(char *);

and in its cpp file:

int ClientNetwork::receivePackets(char * recvbuf) 
{
    iResult = NetworkServices::receiveMessage(ConnectSocket, recvbuf, MAX_PACKET_SIZE);

    if ( iResult == 0 )
    {
        printf("Connection closed\n");
        closesocket(ConnectSocket);
        WSACleanup();
        exit(1);
    }

    return iResult;
}


In ClientGame’s header file, add the following under public fields:

    void sendActionPackets();

    char network_data[MAX_PACKET_SIZE];

    void update();


In the cpp file, add the sendActionPackets() function similar to the one in ServerGame:

void ClientGame::sendActionPackets()
{
    // send action packet
    const unsigned int packet_size = sizeof(Packet);
    char packet_data[packet_size];

    Packet packet;
    packet.packet_type = ACTION_EVENT;

    packet.serialize(packet_data);

    NetworkServices::sendMessage(network->ConnectSocket, packet_data, packet_size);
}

Then write the update function which will continuously receive action packets from the server and send action packets in repsonse:

void ClientGame::update()
{
    Packet packet;
    int data_length = network->receivePackets(network_data);

    if (data_length <= 0) 
    {
        //no data recieved
        return;
    }

    int i = 0;
    while (i < (unsigned int)data_length) 
    {
        packet.deserialize(&(network_data[i]));
        i += sizeof(Packet);

        switch (packet.packet_type) {

            case ACTION_EVENT:

                printf("client received action event packet from server\n");

                sendActionPackets();

                break;

            default:

                printf("error in packet types\n");

                break;
        }
    }
}

And finally, call client’s update function inside the clientLoop in main.cpp so that the network will send and receive:

void clientLoop()
{
    while(true)
    {
        //do game stuff
        client->update();
    }
}


Congratulations!! We are finally done. If you run the program, you should be able to see messages being sent and received from the client and the server!!

Just a few last minute notes. I have not covered closing the connection but it should be pretty straight forward. You will need to remove clients from our sessions table and then close the socket using the closesocket(). This I will leave to you as an exercise.

You may extend the program by adding more data types other than the Packet type that we already defined. Just add it to the NetworkData.h file and write the corresponding serialize/deserialize functions. Make sure to account for their sizes when receiving them over the network.

Also, I had an error when building the program with Visual Studio, you may need «#pragma once» in your header files.

Good luck in your endeavors!!

Когда речь идет о сетевом программировании, чаще всего имеют в виду взаимодействие между клиентом и сервером. В этом разделе мы рассмотрим, как создать базовый TCP-сервер и клиент на языке программирования C.

Простой TCP-сервер

  1. Инициализация сокета
  2. Привязка к адресу и порту
  3. Прослушивание входящих соединений
  4. Принятие входящего соединения
  5. Отправка и прием данных
  6. Закрытие сокета
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addr_len = sizeof(address);
    char buffer[1024] = {0};
    char *message = "Hello from server";

    // Создание сокета
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // Привязка
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // Прослушивание
    listen(server_fd, 3);

    printf("Waiting for connections...\n");
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addr_len);
    printf("Connected\n");

    send(new_socket, message, strlen(message), 0);
    printf("Message sent\n");
    close(new_socket);
    close(server_fd);
    return 0;
}

Простой TCP-клиент

  1. Инициализация сокета
  2. Подключение к серверу
  3. Получение данных от сервера
  4. Закрытие сокета
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[1024] = {0};

    // Создание сокета
    sock = socket(AF_INET, SOCK_STREAM, 0);

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8080);
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // Подключение к серверу
    connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    printf("Connected to server\n");

    read(sock, buffer, 1024);
    printf("Message from server: %s\n", buffer);
    close(sock);
    return 0;
}

Заключение

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

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Windows features windows server 2012
  • Windows server 2003 simulator online
  • Почему монитор не выводит изображение при включении компьютера windows 10
  • Как переустановить windows если ноутбук не включается
  • Почему ноутбук не делает скриншот экрана windows 10