December 17, 2019

Разработка сетевых приложений

Модель OSI

Чтобы понять все принципы взаимодействия компьютеров на расстоянии, надо знать так называемую модель OSI (ISO OSI == International Organization for Standardization Open System Interconnection - Взаимодействие Открытых Систем по Стандарту Международной Организации по Стандартизации).

Чтобы понять все принципы взаимодействия компьютеров на расстоянии, надо знать так называемую модель OSI (ISO OSI == International Organization for Standardization Open System Interconnection - Взаимодействие Открытых Систем по Стандарту Международной Организации по Стандартизации).

Вот эти уровни:

7. Прикладной

Это уровень, максимально приближенный к пользовательскому интерфейсу. Пользователи конечного программного продукта не волнует, как передаются данные, зачем и через какое место... Они сказали "ХОЧУ!" - а мы, программисты, должны им это обеспечить. В качестве примера можно взять на рассмотрение любую сетевую игру: для игрока она работает на этом уровне. Пользователь куда то ткнул, в интерфейсной части программы зафиксирована его команда. Что надо передать? Что то приняли, что произошло в мире игры?

6. Представительский

Здесь программист имеет дело с данными, полученными от низших уровней. В основном, это конвертирование и представление данных в удобоваримом для пользователя виде.

5. Сеансовый

Этот уровень позволяет пользователям осуществлять "сеансы связи". То есть именно на этом уровне передача пакетов становится для программиста прозрачной, и он может, не задумываясь о реализации, непосредственно передавать данные, как цельный поток. Здесь на сцену вступают протоколы HTTP, FTP, Telnet, SMTP и т.д.

4. Транспортный

Осуществляет контроль над передачей данных (сетевых пакетов). То есть, проверяет их целостность при передаче, распределяет нагрузку и т.д. Этот уровень реализует такие протоколы, как TCP, UDP и т.д. Для нас представляет наибольший интерес.

3. Сетевой

Логически контролирует адресацию в сети, маршрутизацию и т.д. Должен быть интересен разработчикам новых протоколов и стандартов. На этом уровне реализованы протоколы IP, IPX, IGMP, ICMP, ARP. В основном, управляется драйверами и операционными системами. Сюда влезать, конечно, стоит, но только когда ты знаешь, что делаешь, и полностью в себе уверен.

2. Канальный

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

1. Аппаратный (Физический)

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


Итак, подведем небольшой итог к тому, что было представлено... Мы видим, что, чем выше уровень - тем выше степень абстракции от передачи данных, к работе с самими данными. Это и есть смысл всей модели OSI: поднимаясь все выше и выше по ступенькам ее лестницы, мы все меньше и меньше заботимся о том, как данные передаются, мы все больше и больше становимся заинтересованными в самих данных, нежели в средствах для их передачи. Каждый следующий уровень скрывает в себе предыдущий, облегчая жизнь пользователю этого уровня, будь он программист, радиоинженер или твоя подруга, которая не знает, как настроить MS Outlook Express...

Нас, как программистов, интересуют уровни 3, 4 и 5. Мы должны использовать средства, которые они предоставляют, для того чтобы построить 6 и 7 уровни, с которыми смогут работать конечные пользователи.

Сокеты

У каждой современной операционной системы есть средства для взаимодействия с другими компьютерами. Самым распространенным среди программистов средством для упомянутых целей являются сокеты. Сокеты - это API (Application Programming Interface - Интерфейс Программирования Приложений) для работы с уровнями OSI. Сокеты настолько гибки, что позволяют работать почти с любым из уровней модели OSI. Хочешь - формируй IP-пакеты руками и займись хакингом, отправляя "неправильные" пакеты, которые будут вводить сервера в ступор, хочешь - займись более благоразумным делом и создай новый удобный голосовой чат, хочешь - игрульку по сети гоняй, не хочешь - твое право, но этот случай мы в данном руководстве не рассматриваем... :)

Когда мы создаем сокет (socket - гнездо), мы получаем возможность доступа к нужному нам уровню OSI. Ну а дальше мы можем использовать соответствующие вызовы для взаимодействия с ним. Когда мы создаем сокет, мы также заставляем систему организовать два канала: входящий (это как громкоговоритель у телефона) и исходящий (микрофон). Осуществляя чтение и запись в эти каналы, мы приказываем системе взять на себя дальнейшую судьбу данных, т.е. передать и проследить, чтоб данные дошли вовремя, в нужной последовательности, не искаженные и т.п. Система должна давать (и дает) максимум гарантий (для каждого уровня OSI - гарантии свои), что данные будут переданы правильно. Наша задача - поместить их в очередь, а на другом конце - прочитать из входящей очереди и обработать должным образом. В каждой ОС степень поддержки сокетов разная, но точно могу сказать: в современных операционных системах MS и *nix - сокеты поддерживаются настолько, насколько нам, они могут понадобиться.

Введение в Windows Sockets API C++

Выделяют клиентские и слушающие (серверные) сокеты. Различия между ними очевидны: клиентские подключаются к процессу (к слушающему сокету), слушающие сокеты, соответственно, обрабатывают эти подключения. Передача данных между процессами происходит через клиентские сокеты.

Общая схема работы с сокетами в Windows

Клиент

• Инициализация WSA
• Создание сокета
• Присоединение к серверу
• Прием/передача данных
• Разрыв соединения
Сервер
• Инициализация WSA
• Создание слушающего сокета и привязка к порту
• Прослушивание порта
• Обработка входящих подключений
• Прием/передача данных
• Разрыв соединения

Пример. Клиентское и серверное приложение

КЛИЕНТ:

#include < winsock2.h > 
#include < ws2tcpip.h >
#include < iostream >

#define ip "127.0.0.1"
#define port 27015
#define bufsize 256
#pragma comment(lib, "Ws2_32.lib")

int main()
{
	setlocale(LC_ALL, "Russian");

	//Инициализация WSA
	WSADATA wsaData;

	int result;
	result = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (result != 0)
	{
		std::cout << "Ошибка WSAStartup: " << result << std::endl;
		return 1;
	}

	//Создание сокета
	SOCKET clientSocket = INVALID_SOCKET;
	clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (clientSocket == INVALID_SOCKET)
	{
		std::cout << "Ошибка socket(): " << WSAGetLastError() << std::endl;
		WSACleanup();
		return 1;
	}

	//Присоединение к серверу
	sockaddr_in clientService;
	clientService.sin_family = AF_INET;
	clientService.sin_addr.s_addr = inet_addr(ip);
	clientService.sin_port = htons(port);
	result = connect(
		clientSocket,
		reinterpret_cast< SOCKADDR* >(&clientService),
		sizeof(clientService)
		);
	if (result != 0)
	{
		std::cout << "Ошибка в connect(): " << WSAGetLastError() << std::endl;
		WSACleanup();
		return 1;
	}

	//Передача данных серверу
	char data[] = "Test";
	result = send(clientSocket, data, static_cast< int >(strlen(data)), 0);
	if (result < 0)
	{
		std::cout << "Ошибка в send(): " << WSAGetLastError() << std::endl;
		return 1;
	}

	//Прием данных от сервера
	char buf[bufsize];
	int r;
	do
	{
		r = recv(clientSocket, buf, bufsize, 0);
		if (r > 0)
			std::cout << "Приянтно " << r << " байт" << std::endl;
		else if (r == 0)
			std::cout << "Соединение разорвано" << std::endl;
		else
			std::cout << "Ошибка в recv(): " << WSAGetLastError() << std::endl;
	} while (r > 0);
	
    //Разрыв соединения
	closesocket(clientSocket);
	// Если работа с сокетами больше не предполагается вызываем WSACleanup()
	WSACleanup();
	return 0;
}

СЕРВЕР:

#include < winsock2.h > 
#include < ws2tcpip.h >
#include < iostream >

#define DEFAULT_PORT "27015"
#define DEFAULT_BUFLEN 512
#pragma comment(lib, "Ws2_32.lib")

int main()
{
	setlocale(LC_ALL, "Russian");

	//Инициализация WSA
	int iResult;
	WSAData d;
	iResult = WSAStartup(MAKEWORD(2, 2), &d);
	
    if (iResult != 0)
	{
		std::cout << "Error at WSAStartup: " << iResult;
		return 1;
	}
	
    //Подготовка данных для создания сокета
	struct addrinfo *result = NULL, *ptr = NULL, hints;
	ZeroMemory(&hints, sizeof (hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	hints.ai_flags = AI_PASSIVE;
	iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
	
    if (iResult != 0)
	{
		std::cout << "Ошибка getaddrinfo: " << iResult;
		WSACleanup();
		return 1;
	}
	
    //Создание сокета
	SOCKET listenSocket = INVALID_SOCKET;
	listenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
	
    if (listenSocket == INVALID_SOCKET)
	{
		std::cout << "Error at socket(): " << WSAGetLastError();
		freeaddrinfo(result);
		WSACleanup();
		return 1;
	}
	
    //Связывание сокета с сетвевым адресом
	iResult = bind(listenSocket, result->ai_addr, result->ai_addrlen);
	if (iResult == SOCKET_ERROR)
	{
		std::cout << "Bind failed with error: " << WSAGetLastError();
		freeaddrinfo(result);
		closesocket(listenSocket);
		WSACleanup();
		return 1;
	}
	
    freeaddrinfo(result);
	
    //Прослушивание подключений
	if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		std::cout << "Listen failed with error: " << WSAGetLastError();
		closesocket(listenSocket);
		WSACleanup();
		return 1;
	}
	
    //Обработка запроса на подключение
	SOCKET ClientSocket;
	ClientSocket = INVALID_SOCKET;
	ClientSocket = accept(listenSocket, NULL, NULL);
	if (ClientSocket == INVALID_SOCKET)
	{
		std::cout << "Accept failed with error: " << WSAGetLastError();
		closesocket(listenSocket);
		WSACleanup();
		return 1;
	}
	
    //Обмен данными
	char recvbuf[DEFAULT_BUFLEN];
	int iSendResult;
	int recvbuflen = DEFAULT_BUFLEN;
	do
	{
		// Принимаем данные от клиента
		iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0)
		{
			std::cout << "Принято " << iResult << " байт" << std::endl;
			// Отправляем данные, принтые от клиента, обратно
			iSendResult = send(ClientSocket, recvbuf, iResult, 0);
			if (iSendResult == SOCKET_ERROR)
			{
				std::cout << "Send failed with error: " << WSAGetLastError();
				closesocket(ClientSocket);
				WSACleanup();
				return 1;
			}
			std::cout << "Отправлено " << iSendResult << " байт";
		}
		else if (iResult == 0)
			std::cout << "Соединение закрыто..." << std::endl;
		else
		{
			std::cout << "Ошибка recv " << WSAGetLastError();
			closesocket(ClientSocket);
			WSACleanup();
			return 1;
		}
	} while (iResult > 0);
}