Чтение онлайн

ЖАНРЫ

UNIX: разработка сетевых приложений
Шрифт:

Пример: получение семейства адресов сокета

Функция

sockfd_to_family
, представленная в листинге 4.4, возвращает семейство адресов сокета.

Листинг 4.4. Возвращаемое семейство адресов сокета

//lib/sockfd_to_family.c

1 #include "unp.h"

2 int

3 sockfd_to_family(int sockfd)

4 {

5 union {

6 struct sockaddr sa;

7 char data[MAXSOCKADDR];

8 } un;

9 socklen_t len;

10 len = MAXSOCKADDR;

11 if (getsockname(sockfd, (SA*)un.data, &len) < 0)

12 return (-1);

13 return (un.sa.sa_family);

14 }

Выделение
пространства для наибольшей структуры адреса сокета

5-8
Поскольку мы не знаем, какой тип структуры адреса сокета нужно будет разместить в памяти, мы используем в нашем заголовочном файле
unp.h
константу
MAXSOCKADDR
, которая представляет собой размер наибольшей структуры адреса сокета в байтах. Мы определяем массив типа
char
соответствующего размера в объединении, включающем универсальную структуру адреса сокета.

Вызов функции getsockname

10-13
Мы вызываем функцию
getsockname
и возвращаем семейство адресов.

Поскольку POSIX позволяет вызывать функцию

getsockname
на неприсоединенном сокете, эта функция должна работать для любого дескриптора открытого сокета.

4.11. Резюме

Все клиенты и серверы начинают работу с вызова функции

socket
, возвращающей дескриптор сокета. Затем клиенты вызывают функцию
connect
, в то время как серверы вызывают функции
bind
,
listen
и
accept
. Сокеты обычно закрываются с помощью стандартной функции
close
, хотя в разделе 6.6 вы увидите другой способ закрытия, реализуемый с помощью функции
shutdown
. Мы также проверим влияние параметра сокета
SO_LINGER
(см. раздел 7.5).

Большинство серверов TCP являются параллельными. При этом для каждого клиентского соединения, которым управляет сервер, вызывается функция

fork
. Вы увидите, что большинство серверов UDP являются последовательными. Хотя обе эти модели успешно использовались на протяжении ряда лет, имеются и другие возможности создания серверов с использованием программных потоков и процессов, которые мы рассмотрим в главе 30.

Упражнения

1. В разделе 4.4 мы утверждали, что константы

INADDR_
, определенные в заголовочном файле
<netinet/in.h>
, расположены в порядке байтов узла. Каким образом мы можем это определить?

2. Измените листинг 1.1 так, чтобы вызвать функцию

getsockname
после успешного завершения функции
connect
. Выведите локальный IP-адрес и локальный порт, присвоенный сокету TCP, используя функцию
sock_ntop
. В каком диапазоне (см. рис. 2.10) будут находиться динамически назначаемые порты вашей системы?

3. Предположим, что на параллельном сервере после вызова функции

fork
запускается дочерний процесс, который завершает обслуживание клиента
перед тем, как результат выполнения функции
fork
возвращается родительскому процессу. Что происходит при этих двух вызовах функции
close
в листинге 4.3?

4. В листинге 4.2 сначала измените порт сервера с 13 на 9999 (так, чтобы для запуска программы вам не потребовались права привилегированного пользователя). Удалите вызов функции

listen
. Что происходит?

5. Продолжайте предыдущее упражнение. Удалите вызов функции

bind
, но оставьте вызов функции
listen
. Что происходит?

Глава 5

Пример TCP-соединения клиент-сервер

5.1. Введение

Напишем простой пример пары клиент-сервер, используя элементарные функции из предыдущей главы. Наш простой пример — это эхо-сервер, функционирующий следующим образом:

1. Клиент считывает строку текста из стандартного потока ввода и отправляет ее серверу.

2. Сервер считывает строку из сети и отсылает эту строку обратно клиенту.

3. Клиент считывает отраженную строку и помещает ее в свой стандартный поток вывода.

На рис. 5.1 изображена пара клиент-сервер вместе с функциями, используемыми для ввода и вывода.

Рис. 5.1. Простой эхо-клиент и эхо-сервер

Между клиентом и сервером мы показали две стрелки, но на самом деле это одно двустороннее соединение TCP. Функции

fgets
и
fputs
имеются в стандартной библиотеке ввода-вывода, а функции
writen
и
readline
приведены в разделе 3.9.

Мы разрабатываем нашу собственную реализацию эхо-сервера, однако большинство реализаций TCP/IP предоставляют готовый эхо-сервер, работающий как с TCP, так и с UDP (см. раздел 2.12). С нашим собственным клиентом мы также будем использовать и готовый сервер.

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

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

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

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

Поделиться с друзьями: