Программирование для Linux. Профессиональный подход.
5.5. Сокеты.
Сокет — это устройство двунаправленного взаимодействия, которое предназначено для связи с другим процессом, выполняющимся на этом же или на другом компьютере. Сокеты используются Internet-программами, такими как telnet, rlogin, ftp, talk и Web-броузеры.
Например, с помощью программы telnet можно получить от Web-сервера HTML-страницу, поскольку обе программы общаются по сети при помощи сокетов. Чтобы установить соединение с Web-сервером www.codesourcery.com, следует ввести команду telnet www.codesourcery.com 80. Загадочная константа 80 обозначает порт, который прослушивается Web-сервером. Когда соединение будет установлено, введите команду GET /. В результате через сокет будет послан запрос Web-серверу, который в ответ вернет начальную HTML-страницу, после чего закроет соединение.
% telnet www.codesourcery.com 80.
Trying 206.168.99.1...
Connected to merlin.codesourcery.com (206.168.99.1).
Escape character is '^]'.
GET /
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset="iso-8659-1">
...
5.5.1. Концепции сокетов.
При создании сокета необходимо задать три параметра, тип взаимодействия, пространство имен и протокол.
Тип взаимодействия определяет способ интерпретации передаваемых данных и число абонентов. Данные, посылаемые через сокет, формируются в блоки, называемые пакетами. Тип взаимодействия указывает на то, как обрабатываются пакеты и как они передаются от отправителя к получателю.
■ При взаимодействии с установлением соединения гарантируется доставка пакетов в том порядке, в каком они были отправлены. Если пакеты теряются или приходят в неправильном порядке из-за проблемы в сети, принимающая сторона автоматически запрашивает у отправителя повторную отправку данных.
Сокеты, ориентированные на соединения, функционируют наподобие телефонного звонка: адреса запрашивающей и принимающей сторон фиксируются в самом начале, на этапе установки соединения.
■ При передаче дейтаграмм не гарантируется доставка и правильный порядок пакетов. Пакеты могут теряться и приходить в произвольном порядке. Операционная система лишь обещает сделать "все возможное".
Дейтаграммные сокеты функционируют подобно почтовой службе: отправитель указывает адрес получателя каждого сообщения и не контролирует доставку пакетов.
Пространство имен сокета определяет способ записи адресов. Например, в локальном пространстве имен адреса — это обычные имена файлов. В пространстве имен Internet адрес сокета состоит из IP-адреса компьютера, подключенного к сети, и номера порта. Благодаря номерам портов можно различать сокеты, созданные на одном компьютере.
Протокол определяет способ передачи данных. Основными семействами протоколов являются TCP/IP (ключевые сетевые протоколы, используемые в Internet) и AppleTalk (протоколы, используемые системами Macintosh). Сокеты могут также работать в соответствии с локальным коммуникационным протоколом UNIX. Не все комбинации типов взаимодействия, пространств имен и протоколов поддерживаются.
5.5.2. Системные вызовы.
Сокеты являются более гибкими в управлении, чем рассмотренные выше механизмы межзадачного взаимодействия. При работе с сокетами используются следующие функции:
■ socket() — создает сокет;
■ close() — уничтожает сокет;
■ connect() — устанавливает соединение между двумя сокетами;
■ bind() — назначает серверному сокету адрес;
■ listen() — переводит сокет в режим приема запросов на подключение;
■ accept() — принимает запрос на подключение и создает новый сокет, который будет обслуживать данное соединение.
Сокеты представляются в программе файловыми дескрипторами.
Создание и уничтожение сокетов.
Функции socket() и close() создают и уничтожают сокет соответственно. В первом случае необходимо задать три параметра: пространство имен, тип взаимодействия и протокол. Константы, определяющие пространство имен, начинаются с префикса PF_ (сокращение от "protocol family" — семейство протоколов). Например, константы PF_LOCAL и PF_UNIX соответствуют локальному пространству имен, а константа PF_INET — пространству имен Internet. Константы, определяющие тип взаимодействия, начинаются с префикса SOCK_. Сокетам, ориентированным на соединения, соответствует константа SOCK_STREAM, а дейтаграммным сокетам — константа SOCK_DGRAM.
Выбор протокола определяется связкой "пространство имен — тип взаимодействия". Поскольку для каждой такой пары, как правило, лучше всего подходит какой-то один протокол, в третьем параметре функции socket() обычно задается значение 0 (выбор по умолчанию). В случае успешного завершения функция socket() возвращает дескриптор сокета. Чтение и запись данных через сокеты осуществляется с помощью обычных файловых функций, таких как read(), write() и т.д. По окончании работы с сокетом его необходимо удалить с помощью функции close().
Вызов функции connect().
Чтобы установить соединение между двумя сокетами, следует на стороне клиента вызвать функцию connect(), указав адрес серверного сокета. Клиент — это процесс, инициирующий соединение, а сервер — это процесс, ожидающий поступления запросов на подключение. В первом параметре функции connect() задается дескриптор клиентского сокета, во втором— адрес серверного сокета, в третьем — длина (в байтах) адресной структуры, на которую ссылается второй параметр. Формат адреса будет разным в зависимости от пространства имен.
Передача данных.
При работе с сокетами можно применять те же самые функции, что и при работе с файлами. О низкоуровневых функциях ввода-вывода, поддерживаемых в Linux, рассказывается в приложении Б, "Низкоуровневый ввод-вывод". Имеется также специальная функция send(), являющаяся альтернативой традиционной функции write().
5.5.3. Серверы.
Жизненный цикл сервера можно представить так:
1) создание сокета, ориентированного на соединения (функция socket());
2) назначение сокету адреса привязки (функция bind());
3) перевод сокета в режим ожидания запросов (функция listen());
4) прием поступающих запросов (функция accept());
5) закрытие сокета (функция close()).
Данные не записываются и не читаются напрямую через серверный сокет. Вместо этого всякий раз, когда сервер принимает запрос на соединение, ОС Linux создает отдельный сокет, используемый для передачи данных через это соединение.
Серверному сокету необходимо с помощью функции bind() назначить адрес, чтобы клиент смог его найти. Первым аргументом функции является дескриптор сокета. Второй аргумент — это указатель на адресную структуру, формат которой будет зависеть от выбранного семейства адресов. Третий аргумент — это длина адресной структуры в байтах. После получения адреса сокет, ориентированный на соединения, должен вызвать функцию listen(), тем самым обозначив себя как сервер. Первым аргументом этой функции также является дескриптор сокета. Второй аргумент определяет, сколько запросов может находиться в очереди ожидания. Если очередь заполнена, все последующие запросы отвергаются. Этот аргумент задает не предельное число запросов, которое способен обработать сервер. а максимальное количество клиентов, которые могут находиться в режиме ожидания.
Сервер принимает от клиента запрос на подключение, вызывая функцию accept(). Первый ее аргумент — это дескриптор сокета. Второй аргумент указывает на адресную структуру, заполняемую адресом клиентского сокета. Третий аргумент содержит длину (в байтах) адресной структуры. Функция accept() создает новый сокет для обслуживания клиентского соединения и возвращает его дескриптор. Исходный серверный сокет продолжает принимать запросы от клиентов. Чтобы прочитать данные из сокета, не удалив их из входящей очереди, воспользуйтесь функцией recv(). Она принимает те же аргументы, что и функция read(), плюс дополнительный аргумент FLAGS. Флаг MSG_PEEK задает режим "неразрушающего" чтения, при котором прочитанные данные остаются в очереди.
5.5.4. Локальные сокеты.
Сокеты, соединяющие процессы в пределах одного компьютера, работают в локальном пространстве имен ( PF_LOCAL или PF_UNIX, это синонимы). Такие сокеты называются локальными или UNIX-сокетами. Их адресами являются имена файлов, указываемые только при создании соединения.
Имя сокета задается в структуре типа sockaddr_un. В поле sun_family необходимо записать константу AF_LOCAL, указывающую на то, что адрес находится в локальном пространстве имен. Поле sun_path содержит путевое имя файла и не может превышать 108 байтов. Длина структуры sockaddr_un вычисляется с помощью макроса SUN_LEN(). Допускается любое имя файла, но процесс должен иметь право записи в каталог, где находится файл. При подключении к сокету процесс должен иметь право чтения файла. Несмотря на то что файловая система может экспортироваться через NFS на разные компьютеры, только процессам, работающим в пределах одного компьютера, разрешается взаимодействовать друг с другом посредством локальных сокетов.
При работе в локальном пространстве имен допускается только протокол с номером 0.
Локальный сокет является частью файловой системы, поэтому он отображается командой ls (обратите внимание на букву s в строке режима):
% ls -l /tmp/socket.
Srwxrwx--x 1 user group 0 Nov 13 19:16 /tmp/socket.
Если локальный сокет больше не нужен, его файл можно удалить с помощью функции unlink().
5.5.5. Примеры программ, работающих с локальными сокетами.
Работу с локальными сокетами мы проиллюстрируем двумя программами. Первая (листинг 5.10) — это сервер. Он создает локальный сокет и переходит в режим ожидания запросов на подключение. Приняв запрос, сервер читает сообщения из сокета и отображает на на экране, пока соединение не будет закрыто. Если поступает сообщение "quit", сервер удаляет сокет и завершает свою работу. Программа socket-server ожидает путевое имя сокета в командной строке.
Листинг 5.10. (socket-server.c) Сервер локального сокета.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* Чтение сообщений из сокета и вывод их на экран. Функция.
продолжает работу до тех пор, пока сокет не будет закрыт.
Функция возвращает 0, если клиент послал сообщение "quit",
в противном случае возвращается ненулевое значение. */
Int server(int client_socket) {
while (1) {
int length;
char* text;
/* Сначала читаем строку, в которой записана длина сообщения.
Если возвращается 0, клиент закрыл соединение. */
if (read(client_socket, &length, sizeof(length)) == 0).
return 0;
/* Выделение буфера для хранения текста. */
text = (char*)malloc(length);
/* Чтение самого сообщения и вывод его на экран. */
read(client_socket, text, length);
printf("%s\n", text);
/* Очистка буфера. */
free(text);
/* Если клиент послал сообщение "quit.", работа сервера.
завершается. */
if (!strcmp(text, "quit")).
return 1;
}
}
Int main(int argc, char* const argv[]) {
const char* const socket_name = argv[1];
int socket_fd;
struct sockaddr_un name;
int client_sent_quit_message;
/* Создание локального сокета. */
socket_fd = socket(PF_LOCAL, SOCK_STREAM, 0);
/* Переход в режим сервера. */
name.sun_family = AF_LOCAL;
strcpy(name.sun_path, socket_name);
bind(socket_fd, SUN_LEN(&name));
/* Ожидание запросов. */
listen(socket_fd, 5);
/* Непрерывный прием запросов на подключение. Для каждого.
клиента вызывается функция server(). Цикл продолжается,
пока не будет получено сообщение "quit". */
do {
struct sockaddr_un client_name;
socklen_t client_name_len;
int client_socket_fd;
/* Прием запроса. */
client_socket_fd =
accept(socket_fd, &client_name, &client_name_len);
/* Обработка запроса. */
client_sent_quit_message = server(client_socket_fd);
/* Закрытие серверной стороны соединения. */
close(client_socket_fd);
} while(!client_sent_quit_message);
/* Удаление файла локального сокета. */
close(socket_fd);
unlink(socket_name);
return 0;
}
Клиентская программа, показанная в листинге 5.11, подключается к локальному сокету и посылает сообщение. Путевое имя сокета и текст сообщения задаются в командной строке.
Листинг 5.11. (socket-client.c) Клиент локального сокета.
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* Запись строки TEXT в сокет, заданный.
дескриптором SOCKET_FD. */
Void write_text(int socket_fd, const char* text) {
/* Сначала указывается число байтов в строке, включая.
завершающий символ NULL. */
int length = strlen(text) + 1;
write(socket_fd, &length, sizeof(length));
/* Запись строки. */
write(socket_fd, text, length);
}
Int main(int argc, char* const argv[]) {
const char* const socket_name = argv[1];
const char* const message = argv[2];
int socket_fd;
struct sockaddr_un name;
/* Создание сокета. */
socket_fd = socket(PF_LOCAL, SOCK_STREAM. 0);
/* Сохранение имени сервера в адресной структуре. */
name.sun_family = AF_LOCAL;
strcpy(name.sun_path, socket_name);
/* Подключение к серверному сокету. */
connect(socket_fd, &name, SUN_LEN(&name));
/* передача сообщения, заданного в командной строке. */
write_text(socket_fd, message);
close(socket_fd);
return 0;
}
Прежде чем отравить текст сообщения, клиент записывает в сокет число (хранится в переменной length), определяющее длину сообщения в байтах. На противоположной стороне сервер выясняет длину сообщения и выделяет для него буфер соответствующего размера, после чего читает само сообщение.
Чтобы проверить этот пример, запустите в одном терминальном окне серверную программу, указав путь к сокету, например:
% ./socket-server /tmp/socket.
В другом окне запустите несколько раз клиентскую программу, задав тот же путь к сокету плюс требуемое сообщение:
% ./socket-client /tmp/socket "Hello, world.".
% ./socket-client /tmp/socket "This is a test.".
Сервер получит и отобразит эти сообщения. Чтобы закрыть сервер, пошлите ему сообщение "quit":
% ./socket-client /tmp/socket "quit".
5.5.6. Internet-сокеты.
UNIX-сокеты используются для организации взаимодействия двух процессов, выполняющихся на одном компьютере. С другой стороны. Internet-сокеты позволяют соединять между собой процессы, работающие на разных компьютерах.
Пространству имен Internet соответствует константа PF_INET. Internet-сокеты чаще всего работают по протоколам TCP/IP. Протокол IP (Internet Protocol) отвечает за низкоуровневую доставку сообщений, осуществляя при необходимости их разбивку на пакеты и последующую компоновку. Доставка пакетов не гарантируется, поэтому они могут исчезать или приходить в неправильном порядке. Каждый компьютер в сети имеет свой IP-адрес. Протокол TCP (Transmission Control Protocol) функционирует поверх протокола IP и обеспечивает надежную доставку сообщений, ориентированную на установление соединений.
DNS-имена. Легче запоминать имена а не числа, поэтому служба DNS (Domain Name Service) закрепляет за IP-адресами доменные имена вида www.codesourcery.com. Служба DNS организована в виде всемирной иерархии серверов имен. Чтобы использовать доменные имена в программах, нет необходимости разбираться в протоколах DNS.Адрес Internet-сокета состоит из двух частей: адреса компьютера и номера порта. Эта информация хранится в структуре типа sockaddr_in. В поле sin_family необходимо записать константу AF_INET, указывающую на то, что адрес принадлежит пространству имен Internet. В поле sin_addr хранится IP-адрес компьютера в виде 32-разрядного целого числа. Благодаря номерам портов можно различать сокеты, создаваемые на одном компьютере. В разных системах многобайтовые значения могут храниться с разным порядком следования байтов, поэтому с помощью функции htons() необходимо преобразовать номер порта в число с сетевым порядком следования байтов.
Функция gethostbyname() преобразует адрес компьютера из текстового представления — стандартного точечного (например, 10.10.10.1) или доменного (например, www.codesourcery.com) — во внутреннее 32-разрядное. Функция возвращает указатель на структуру типа hostent. IP-адрес находится в ее поле h_addr.
Программа, представленная в листинге 5.12, иллюстрирует работу с Internet-сокетами. Программа запрашивает начальную страницу у Web-сервера, адрес которого указан в командной строке.
Листинг 5.12. (socket-inet.c) Чтение страницы с Web-сервера.
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
/* Отображение содержимого Web-страницы, полученной из.
серверного сокета. */
Void get_home_page(int socket_fd) {
char buffer[10000];
ssize_t number_characters_read;
/* Отправка HTTP-команды GET с запросом начальной страницы. */
sprintf(buffer, "GET /\n");
write(socket_fd, buffer, strlen(buffer));
/* Чтение данных из сокета. Функция read() может вернуть.
не все данные сразу, поэтому продолжаем чтение, пока.
не будут получены все данные. */
while (1) {
number_characters_read = read(socket_fd, buffer, 10000);
if (number_characters_read == 0).
return;
/* Запись данных в стандартный выходной поток. */
fwrite(buffer, sizeof(char), number_characters_read, stdout);
}
}
Int main(int argc, char* const argv[]) {
int socket_fd;
struct sockaddr_in name;
struct hostent* hostinfo;
/* Создание сокета. */
socket_fd = socket(PF_INET, SOCK_STREAM, 0);
/* Запись имени сервера в адресную структуру. */
name.sin_family = AF_INET;
/* Преобразование адреса из текстового представления во.
внутреннюю форму. */
hostinfo = gethostbyname(argv[1]);
if (hostinfo == NULL).
return 1;
else.
name sin_addr = *((struct in_addr*)hostinfo->h_addr);
/* Web-серверы используют порт 80. */
name.sin_port = htons(80);
/* Подключаемся к Web-серверу. */
if (connect(socket_fd, &name,
sizeof(struct sockaddr_in)) == -1) {
perror("connect");
return 1;
}
/* получаем содержимое начальной страницы сервера. */
get_home_page(socket_fd);
return 0;
}
Программа извлекает имя Web-сервера из командной строки (имя не является URL-адресом, т.е. в нем отсутствует префикс http://). Далее вызывается функция gethostbyname(), которая преобразует имя сервера в числовое представление. После этого программа подключает потоковый (TCP) сокет к порту 80 сервера. Web-серверы общаются по протоколу HTTP (Hypertext Transfer Protocol), поэтому программа посылает HTTP-команду GET, в ответ на которую сервер возвращает текст начальной страницы.
Стандартные номера портов. По существующему соглашению Web-серверы ожидают поступления запросов на порт 80. За большинством lntemet-сервисов закреплены стандартные номера портов. Например, защищенные Web-серверы работающие по протоколу SSL. прослушивают порт 443 а почтовые серверы (протокол SMTP) прослушивают порт 25. В Linux связи между именами протоколов/сервисов и номерами портов устанавливаются в файле /etc/services. В первой колонке файла указано имя протокола или сервисе. Во второй колонке приведен номер порта и тип взаимодействия: tcp — для сервисов ориентированных на соединения, и udp — для дейтаграмм. При реализации собственных сетевых сервисов используйте номере портов, большие чем 1024.Например, чтобы получить начальную страницу с сервера www.codesourcery.com, введите следующую команду:
% ./socket-inet www.codesourcery.com.
<html>
<meta http-equiv="Content-Type".
content="text/html; charset=iso-8859-1">
...
5.5.7. Пары сокетов.
Как было показано выше, функция pipe() создает два дескриптора для входного и выходного концов канала. Возможности каналов ограничены, так как с файловыми дескрипторами должны работать связанные процессы и данные через канал передаются только в одном направлении. Функция socketpair() создает два дескриптора для двух связанных сокетов, находящихся на одном компьютере. С помощью этих дескрипторов можно организовать двунаправленное взаимодействие процессов.
Первые три параметра функции socketpair() такие же, как и в функции socket(): пространство имен (должно быть PF_LOCAL), тип взаимодействия и протокол. Последний параметр — это массив из двух целых чисел, куда будут записаны дескрипторы сокетов, подобно функции pipe().
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.