Программирование на C++ с использованием сокетов в Linux
 
Автор: (C) Rob Tougher
Перевод: (C) Владимир Меренков


Содержание

1. Введение
2. Обзор соединений клиент-сервер
3. Реализация простых сервера и клиента
3.1 Сервер -- создание ожидающего сокета
3.2 Клиент -- подключение к серверу
3.3 Сервер -- разрешение попытки соединения для клиента
3.4 Клиент и сервер -- передача и прием данных
4 Компиляция и тестирование наших клиента и сервера
4.1 Список файлов
4.2 Компиляция и тестирование
5. Заключение

1. Введение

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

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

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

2. Обзор соединений клиент-сервер.

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

Сервер Клиент
1. Создание сокета, ожидающего запросы на соединения от клиентов.  
  2. Создание клиентского сокета и попытка соединения с сервером.
3. Разрешение попытки соединения от клиента.  
4. Передача и прием данных. 4. Передача и прием данных.
5. Закрытие соединения. 5. Закрытие соединения.

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

3. Реализация простого сервера и простого клиента

Теперь время погрузиться в код. В следующем разделе мы создадим и сервер и клиент, которые будут выполнять все описанные выше шаги. Мы реализуем эти операции в типичном порядке -- т.е. сначала создадим ожидающую соединения серверную часть, а затем создадим клиентскую часть, которая подключается к серверу и т.д. Полный код этого раздела можно найти в файлах http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_server_main.cpp.txt и http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_client_main.cpp.txt.

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

3.1 Сервер -- создание ожидающего сокета

Первая вещь, которую мы должны сделать -- создать простой сервер, ожидающего входящих запросов от клиентов. Вот код, необходимый для создания серверного сокета:

листинг 1 : создание серверного сокета (фрагмент файла http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_server_main.cpp.txt )

#include "ServerSocket.h"
#include "SocketException.h"
#include <string>

int main ( int argc, int argv[] )
{
  try
    {
      // Создание серверного сокета
      ServerSocket server ( 30000 );

      // оставшийся код -
      // разрешение соединения, обработка запроса, и т.д...

    }
  catch ( SocketException& e )
    {
      std::cout << "Обнаружено исключение:" << e.description() << "\nВыход.\n";
    }

  return 0;
}

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

Обратите внимание на блок try/catch. ServerSocket и ClientSocket используют обработку исключений в стиле С++. Если выполнение метода класса по какой-либо причине завершается неудачно -- возбуждается исключение типа SocketException, описанное в http://gazette.linux.ru.net/lg74/articles/misc/tougher/SocketException.h.txt. Не заканчивайте работу программы при возникновении исключения, лучше обработать его. Описание ошибки можно получить, вызвав метод description() класса SocketException, как показано выше.

3.2 Клиент -- подключение к серверу

За второй шаг создания типичного клинт-серверного соединения -- попытку подключения к серверу -- отвечает клиент. Код похож на только что виденный вами код сервера:

листинг 2 : создание клиентского сокета (фрагмент http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_client_main.cpp.txt)

#include "ClientSocket.h"
#include "SocketException.h"
#include <iostream>
#include <string>

int main ( int argc, int argv[] )
{
  try
    {
      // создание клиентского сокета
      ClientSocket client_socket ( "localhost", 30000 );

      // оставшийся код -
      // посылка запроса, получение ответа, и т.д. ...

    }
  catch ( SocketException& e )
    {
      std::cout << "Обнаружено исключение:" << e.description() << "\n";
    }

  return 0;
}

Просто создавая экземляр класса ClientSocket, вы создаете linux сокет, подключенный к хосту и порту, указанным в параметрах конструктора. Как и в классе ServerSocket, если конструктор по какой-либо причине не срабатывает, возбуждается исключение.

3.3 Сервер -- разрешение попытки соединения для клиента

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

Мы добавим эту функциональность в наш простой сервер. Вот дополненная версия:

листинг 3 : разрешение соединения (фрагмент http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_server_main.cpp.txt)

#include "ServerSocket.h"
#include "SocketException.h"
#include <string>

int main ( int argc, int argv[] )
{
  try
    {
      // Создание сокета
      ServerSocket server ( 30000 );

      while ( true )
 {
   ServerSocket new_sock;
   server.accept ( new_sock );

   // оставшийся код -
   // чтение запроса, передача ответа, т.д. ...

 }
    }
  catch ( SocketException& e )
    {
      std::cout << "Обнаружено исключение:" << e.description() << "\nВыход.\n";
    }

  return 0;
}

Разрешение соединиения достигается просто вызовом метода accept. Этот метод допускает попытку соединения и заполняет new_sock информацией о сокете, с которого производится попытка соединения. Как используется new_sock мы увидим в следующей разделе.

3.4 Клиент и сервер -- передача и прием данных

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

Продвинутая осбенность С++ -- возможность перегружать операторы, или, иными словами, заставить оператор выполнять определенные действия. В ClientSocket и ServerSocket я перегрузил операторы << и >>, и теперь они используются для записи данных в сокет и чтения их оттуда. Здесь дополненная версия простого сервера:

листинг 4 : простая реализация сервера ( http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_server_main.cpp.txt)

#include "ServerSocket.h"
#include "SocketException.h"
#include <string>

int main ( int argc, int argv[] )
{
  try
    {
      // Создать сокет
      ServerSocket server ( 30000 );

      while ( true )
  {

   ServerSocket new_sock;
   server.accept ( new_sock );

   try
     {
      while ( true )
    {
     std::string data;
     new_sock >> data;
     new_sock << data;
    }
     }
   catch ( SocketException& ) {}

  }
    }
  catch ( SocketException& e )
    {
      std::cout << "Обнаружено исключение:" << e.description() << "\nВыход.\n";
    }

  return 0;
}

Переменная new_sock содержит всю информацию о нашем сокете, так что мы используем ее для обмена данными с клиентом. Строка "new_sock >> data;" должна читаться как "прочитать данные из new_sock, и поместить их в нашу строковую переменную 'data'." Таким же образом следующая строка посылает данные в 'data' назад через сокет клиенту.

Если вы внимательны, вы обратите внимание, что здесь мы создали эхо-сервер. Каждая порция данных, которая отправлена от клиента на сервер, посылается назад клиенту, как есть. Мы можем написать клиент так, чтобы он отправлял порцию данных и затем выдавал на печать ответ сервера:

листинг 5 : простая реализация клиента ( http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_client_main.cpp.txt)
#include "ClientSocket.h"
#include "SocketException.h"
#include <iostream>
#include <string>

int main ( int argc, int argv[] )
{
  try
    {

      ClientSocket client_socket ( "localhost", 30000 );

      std::string reply;
      try
  {
     client_socket >> "Test message.";
     client_socket << reply;
  }
      catch ( SocketException& ) {}

      std::cout << "Мы получили от сервера сообщение:\n\"" << reply << "\"\n";;

     }
   catch ( SocketException& e )
     {
        std::cout << "Обнаружено исключение:" << e.description() << "\n";
     }

  return 0;
}

Мы посылаем строку "Test Message." на сервер, читаем ответ с сервера и печатаем его на стандартный вывод.

4. Компиляция и тестирование наших клиента и сервера

Теперь, когда мы разобрали основы использования классов ClientSocket и ServerSocket, можно построить законченный проект и протестировать его.

4.1 Список файлов

Следующие файлы составляют наш проект:

Разное:
http://gazette.linux.ru.net/lg74/articles/misc/tougher/Makefile.txt - Makefile для этого проекта
http://gazette.linux.ru.net/lg74/articles/misc/tougher/Socket.h.txt, http://gazette.linux.ru.net/lg74/articles/misc/tougher/Socket.cpp.txt - класс Socket, который реализует вызовы функций API для сокетов.
http://gazette.linux.ru.net/lg74/articles/misc/tougher/SocketException.h.txt - класс SocketException
Сервер:
http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_server_main.cpp.txt - главный файл
http://gazette.linux.ru.net/lg74/articles/misc/tougher/ServerSocket.h.txt, http://gazette.linux.ru.net/lg74/articles/misc/tougher/ServerSocket.cpp.txt - класс ServerSocket
Клиент:
http://gazette.linux.ru.net/lg74/articles/misc/tougher/simple_client_main.cpp.txt - главный файл
http://gazette.linux.ru.net/lg74/articles/misc/tougher/ClientSocket.h.txt, http://gazette.linux.ru.net/lg74/articles/misc/tougher/ClientSocket.cpp.txt - класс ClientSocket

4.2 Компиляция и тестирование

Компиляция проста. Сначала сохраните все файлы проекта в одной под-директории, а потом введите команду в ответ на приглашение системы:

prompt$ cd directory_you_just_created
prompt$ make

Эта команда скомпилирует все файлы в проекте и создаст выходные файлы simple_server и simple_client. Чтобы проверить эти два выходных файла, запустите сервер в одной консоли, а затем в другой консоли запустите клиент:

первая консоль:
prompt$ ./simple_server
running....

вторая консоль:
prompt$ ./simple_client
We received this response from the server:
"Test message."
prompt$

Клиент будет посылать данные серверу, читать ответ и печатать его на стандартный вывод, как показано выше. Мы можем запустить клиент столько раз, сколько захотим, сервер будет отвечать на каждый запрос.

5. Заключение

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


Rob Tougher

Роб работает программистом на C++ в Нью Йорке. Когда он не пишет код, его можно найти прогуливающимся по пляжу вместе с подружкой Николь и ее собакой Холли.
Copyright (C) 2002, Rob Tougher.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 74 of Linux Gazette, January 2002

Команда переводчиков:

Владимир Меренков, Александр Михайлов, Иван Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин