вторник, 3 января 2012 г.

Введение в Windows Sockets API: Серверные сокеты C++

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

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

Инициализация WSA

int iResult;

WSAData d;
iResult = WSAStartup( MAKEWORD(2, 2), &d);
if(iResult != 0)
{
 std::cout << "Error at WSAStartup: " << iResult;
 return 1;
}

Как я уже писал в предыдущей статье, перед началом работы с сокетами необходимо провести инициализацию Windows Sockets API.

Подготовка данных для создания сокета

#define DEFAULT_PORT "27015"

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;
}

Функция getaddrinfo используется для определения значений в структуре sockaddr.

  • AF_INET указывает, что используется четвертая версия IP протокола.
  • SOCK_STREAM используется для определения потока сокета.
  • IPPROTO_TCP используется для определения TCP протокола.
  • 270015 это номер порта, к которому будет подключаться клиент.

Создание сокета

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;
}

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

Связывание сокета с сетвевым адресом

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);

После того, как мы создали сокет, его необходимо связать с конкретным сетевым адресом. Для этого надо вызвать функцию bind и передать ей в качестве аргументов дескриптор сокета и данные из структуры sockaddr.
Информация об адресе, хранимая в переменной result, больше не требуется, поэтому мы можем ее удалить, используя функцию freeaddrinfo.

Прослушивание подключений

if( listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
{
 std::cout << "Listen failed with error: " << WSAGetLastError();
 closesocket(listenSocket);
 WSACleanup();
 return 1;
}

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

Обработка запроса на подключение

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;
}

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

Обмен данными

#defin DEFAULT_BUFLEN 512

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);

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

Заключение

Это заключительная теоретическая часть из серии статей посвященных введению в сетевое программирование в среде Windows. В последующих, практических статьях, я покажу как можно создать простое сетевое приложение.
Дополнительную информацию о функциях, структурах WSA можно найти в MSDN: Winsock reference.

Введение в WSA

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

Отправить комментарий