Нейл Мэтью - Основы программирования в Linux
Сначала серверное приложение создает сокет, который как файловый дескриптор представляет собой ресурс, присваиваемый единственному серверному процессу. Сервер создает его с помощью системного вызова socket, и этот сокет не может использоваться совместно с другими процессами.
Далее сервер присваивает сокету имя. Локальные сокеты с заданными именами файлов в файловой системе Linux часто размещаются в каталоге /tmp или /usr/tmp. У сетевых сокетов имя файла будет идентификатором сервиса (номер порта/точка доступа), относящегося к конкретной сети, к которой могут подключаться клиенты. Этот идентификатор, задавая определенный номер порта, соответствующий корректному серверному процессу, позволяет Linux направлять входящие подключения по определенному маршруту. Например, Web-сервер обычно создает сокет для порта 80, идентификатор, зарезервированный для этой цели. Web-обозреватели знают о необходимости применять порт 80 для своих HTTP-подключений к Web- сайтам, которые пользователь хочет читать. Именуется сокет с помощью системного вызова bind. Далее серверный процесс ждет подключения клиента к именованному сокету. Системный вызов listen формирует очередь входящих подключений. Сервер может принять их с помощью системного вызова accept.
Когда сервер вызывает accept, создается новый сокет, отличающийся от именованного сокета. Этот новый сокет применяется только для взаимодействия с данным конкретным клиентом. Именованный сокет сохраняется для дальнейших подключений других клиентов. Если сервер написан корректно, он может извлечь выгоду из многочисленных подключений. Web-сервер добивается этого за счет одновременного предоставления страниц многих клиентам. В случае простого сервера все последующие клиенты ждут в очереди до тех пор, пока сервер не будет готов снова.
Клиентская сторона системы с применением сокетов гораздо проще. Клиент создает неименованный сокет с помощью вызова socket. Затем он вызывает connect для подключения к серверу, используя в качестве адреса именованный сокет сервера.
Будучи установлены, сокеты могут применяться как низкоуровневые файловые дескрипторы, обеспечивая двунаправленный обмен данными.
Выполните упражнения 15.1 и 15.2.
Упражнение 15.1. Простой локальный клиентДалее приведен пример очень простой клиентской программы client1.с. В ней неименованный сокет создается и затем подключается к сокету сервера, названному server_socket. Системный вызов socket мы подробно рассмотрим чуть позже, когда будем обсуждать некоторые проблемы адресации.
1. Включите нужные заголовочные файлы и задайте переменные:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch = 'A';
2. Создайте сокет для клиента:
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
3. Назовите сокет по согласованию с сервером:
address.sun_family = AF_UNIX;
strcpy(address.sun_path, "server_socket");
len = sizeof(address);
4. Соедините ваш сокет с сокетом сервера:
result = connect(sockfd, (struct sockaddr *)&address, len);
if (result == -1) {
perror("oops : client1");
exit(1);
}
5. Теперь вы можете читать и писать через sockfd:
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf("char from server = %cn", ch);
close(sockfd);
exit(0);
}
Эта программа завершится аварийно, если вы попытаетесь выполнить ее, потому что еще не создан именованный сокет сервера, (Точное сообщение об ошибке может отличаться в разных системах.)
$ ./client1
oops: client1: No such file or directory
$
Упражнение 15.2. Простой локальный серверДалее приведена программа простого сервера server1.с, которая принимает запрос на соединение от клиента. Она создает сокет сервера, присваивает ему имя, создает очередь ожидания и принимает запросы на соединения.
1. Включите необходимые заголовочные файлы и задайте переменные:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
2. Удалите все старые сокеты и создайте неименованный сокет для сервера:
unlink("server_socket");
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
3. Присвойте имя сокету:
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, "server_socket");
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
4. Создайте очередь запросов на соединение и ждите запроса клиента:
listen(server_sockfd, 5);
while(1) {
char ch;
printf("server waitingn");
5. Примите запрос на соединение:
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
6. Читайте и записывайте данные клиента с помощью client_sockfd:
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}
Как это работает
В этом примере серверная программа в каждый момент времени может обслуживать только одного клиента. Она просто читает символ, поступивший от клиента, увеличивает его и записывает обратно. В более сложных системах, где сервер должен выполнять больше работы по поручению клиента, такой подход будет неприемлемым, потому что другие клиенты не смогут подключиться до тех пор, пока сервер не завершит работу. Позже вы увидите пару методов, позволяющих подключаться многочисленным клиентам.
Когда вы выполняете серверную программу, она создает сокет и ждет запросов на соединение. Если вы запустите ее в фоновом режиме, т.е. она будет выполняться независимо, вы сможете затем запускать клиентов как высокоприоритетные задачи.
$ ./server1 &
[1] 1094
$ server waiting
Ожидая запросы на соединения, сервер выводит сообщение. В приведенном примере сервер ждет запрос с сокета файловой системы, и вы сможете увидеть его с помощью обычной команды ls.
Хорошо взять за правило удалять сокет после окончания работы с ним, даже в случае аварийного завершения программы из-за получения сигнала. Это убережет файловую систему от загромождения неиспользуемыми файлами.
$ ls -lF server socket
srwxr-xr-x 1 neil users 0 2007-06-23 11:41 server_socket=
Здесь тип устройства — сокет, на что указывает символ s перед правами доступа и символ = в конце имени. Сокет был создан как обычный файл с правами доступа, модифицированными текущей umask. Если применить команду ps, то можно увидеть сервер, выполняющийся в фоновом режиме. Он показан спящим (параметр STAT равен s) и, следовательно, не потребляющим ресурсы ЦП.
$ ps lх
F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND
0 1000 23385 10689 17 0 1424 312 361800 S pts/1 0:00 ./server1
Теперь, когда вы запустите программу, то успешно подключитесь к серверу. Поскольку сокет сервера существует, вы можете соединиться с ним и обмениваться данными.
$ ./client1
server waiting char from server = В
$
На терминале вывод сервера и клиента перемешаны, но можно увидеть, что сервер получил символ от клиента, увеличил его и вернул. Далее сервер продолжает выполняться и ждет следующего клиента. Если вы запустите несколько клиентов вместе, они будут обслуживаться по очереди, хотя полученный вывод может оказаться еще более перемешанным.
$ ./client1 & ./client1 & ./client1 &
[2] 23412
[3] 23413
[4] 23414
server waiting
char from server = В
server waiting
char from server = В
server waiting
char from server = В
server waiting
[2] Done client1
[3]- Done client1
[4]+ Done client1
$
Атрибуты сокета
Для того чтобы до конца понять системные вызовы, применявшиеся в рассмотренном примере, необходимо узнать кое-что об организации сети в системах UNIX.
Сокеты характеризуются тремя атрибутами: доменом, типом и протоколом. У них также есть адрес, используемый как имя сокета. Форматы адресов меняются в зависимости от домена, также называемого семейством протоколов (protocol family). Каждое семейство протоколов может применять одно или несколько семейств адресов, определяющих формат адреса.