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" #includeint 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#include 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" #includeint 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" #includeint 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#include 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. Заключение
Сокеты -- простой и эффкетивный путь пересылки данных между процессами. В этой статье мы рассмотрели сокет-коммуникации и разработали пример сервера и клиента. Теперь у вас должна быть возможность добавить организацию связи с помощью сокетов в ваши собственные приложения!