Элементы API сокетов
В этом разделе кратко рассмотрены основы API сокетов и построены простейшие 11 клиентское и серверное приложения. Хотя эти приложения очень схематичны, на их примере проиллюстрированы важнейшие характеристики клиента и сервера TCP.
Начнем с вызовов API, необходимых для простого клиента. На рис. 1.2 показаны функции, применяемые в любом клиенте. Адрес удаленного хоста задается с помощью структуры sockaddr_in, которая передается функции connect.
Первое, что вы должны сделать, - это получить сокет для логического соединения. Для этого предназначен системный вызов socket.
#include <sys/socket.h> /* UNIX */
#include <winsock2.h> /* Windows */
SOCKET socket( int domain, int type, int protocol);
Возвращаемое значение: дескриптор сокета в случае успеха; —1 (UNIX) или INVALID_SOCKET (Windows) - ошибка.
API сокетов не зависит от протокола и может поддерживать разные адресные домены. Параметр domain - это константа, указывающая, какой домен нужен сокету.
Чаще используются домены AF_INET (то есть Internet) и AF_LOCAL (или AF_UNIX). В книге рассматривается только домен AF_INET. Домен AF_LOCAL применяется для межпроцессного взаимодействия (IPC) на одной и той же машине.
Примечание: Существуют разногласия по поводу того, следует ли обозначать константы доменов AF_* или PF_*. Сторонники PF_* указывают на их происхождение от уже устаревших вариантов вызова socket в системах 4.1c/2.8/2.9BSD. И, кроме того, они считают, то PF означает protocol family (семейство протоколов). Сторонники же AF_* говорят, что в коде ядра, относящемся к реализации сокетов, параметр domain сравнивается именно с константами AF_*. Но, поскольку оба набора констант определены одинаково действительности одни константы просто выражаются через другие, — на практике можно употреблять оба варианта.
С помощью параметра type задается тип создаваемого сокета. Чаще встречаются следующие значения (а в этой книге только такие) сокетов:
Рис. 1.2. Основные вызовы API сокетов для клиентов
Параметр protocol показывает, какой протокол следует использовать с данным сокетом. В контексте TCP/IP он обычно неявно определяется типом сокета, поэтому в качестве значения задают 0. Иногда, например в случае простых (raw) сокетов, имеется несколько возможных протоколов, так что нужный необходимо задавать явно. Об этом будет рассказано в совете 40.
Для самого простого TCP-клиента потребуется еще один вызов API сокетов, обеспечивающий установление соединения:
#include <sys./socket.h> /* UNIX */
#include <winsock2.h> /* Windows */
int connect(SOCKET s, const struct sockaddr *peer, int peer_len);
Возвращаемое значение: 0 - нормально, -1 (UNIX) или не 0 (Windows) - ошибка.
Параметр s — это дескриптор сокета, который вернул системный вызов socket. Параметр peer указывает на структуру, в которой хранится адрес удаленного хоста и некоторая дополнительная информация. Для домена AF_INET - это структура типа sockaddr_in. Ниже вы увидите, как она заполняется. Параметр peer_len содержит размер структуры в байтах, на которую указывает peer.
После установления соединения можно передавать данные. В ОС UNIX вы должны обратиться к системным вызовам read и write и передать им дескриптор сокета точно так же, как передали бы дескриптор открытого файла. Увы, как уже говорилось, в Windows эти системные вызовы не поддерживают семантику сокетов, поэтому приходится пользоваться вызовами recv и send. Они отличаются от read и write только наличием дополнительного параметра.
#include <sys/socket.h> /*UNIX*/
#include <winsock2.h> /*Windows*/
int recv(SOCKET s, void *buf, size_t left, int flags);
int send(SOCKET s, const void *buf, size_t len, int flags);
Возвращаемое значение: число принятых или переданных байтов в случае успеха или -1 в случае ошибки.
Параметры s, buf и len означают то же, что и для вызовов read и write. Значение параметра flags в основном зависит от системы, но и UNIX, и Windows поддерживают следующие флаги:
При работе с протоколом TCP вам ничего больше не понадобится. Но при работе с UDP нужны еще системные вызовы recvfrom и sendto. Они очень похожи на recv и send, но позволяют при отправке датаграммы задать адрес назначения, а при приеме - получить адрес источника.
#include <sys/socket.h> /*UNIX*/
#include <winsock2.h> /*Windows*/
int recvfrom(SOCKET s, void *buf, size_t len, int flags,
struct sockaddr *from, int *fromlen);
int sendto(SOCKET s, const void *buf, size_t len, int flags,
const struct sockaddr *to, int tolen);
Возвращаемое значение: число принятых или переданных байтов в случае успеха или -1 при ошибке.
Первые четыре параметра - s, buf, len к flags - такие же, как в вызовах recv и send. Параметр from в вызове recvfrom указывает на структуру, в которую ядро помещает адрес источника пришедшей датаграммы. Длина этого адреса хранится в целом числе, на которое указывает параметр fromlen. Обратите внимание, что fromlen - это указатель на целое.
Аналогично параметр to в вызове sendto указывает на адрес структуры, содержащей адреса назначения датаграммы, а параметр tolen - длина этого адреса. Заметьте, что to - это целое, а не указатель.
В листинге 1.1 приведен пример простого TCP-клиента.
Листинг 1.1. Простейший TCP-клиент
1 #include <sys/types .h>
2 #include <sys/socket .h>
3 #include <netinet/in.h>
4 #include <arpa/inet. h>
5 #include <stdio.h>
6 int main( void )
7 {
8 struct sockaddr_in peer;
9 int s ;
10 int rc;
11 char buf [ 1 ];
12 peer. sin_family = AF_INET;
13 peer.sin_port = htons( 7500 );
14 peer.sin_addr.s_addr = inet_addr( "127.0.0.1" );
15 s = socket ( AF_INET, SOCK_STREAM, 0 );
16 if (s < 0)
17 {
18 perror( "ошибка вызова socket" );
19 exit ( 1 );
20 }
21 rc = connect( s, ( struct sockaddr * )&peer, sizeof( peer ) );
22 if (rc)
23 {
24 perror( "ошибка вызова connect" );
25 exit( 1 )
26 }
27 rc = send( s, "1", 1, 0 );
28 if (rc <= 0)
29 {
30 perror( "ошибка вызова send" ) ;
31 exit ( 1 ) ;
32 }
33 rc = recv( s, buf, 1, 0 ) ;
34 if ( rc <= 0 )
35 perror ( "ошибка вызова recv"' );
36 else
37 printf( "%c\n", buf[ 0 ] );
38 exit( 0 );
39 }
Клиент в листинге 1.1 написан как UNIX-программа, чтобы не было сложностей, связанных с переносимостью и Windows-функцией WSAStartup. В совете 4 сказано, что в основном эти сложности можно скрыть в заголовочном файле, но сначала надо подготовить некоторые механизмы. Пока ограничимся более простой моделью UNIX.
Подготовка адреса сервера
12-14 Заполняем структуру sockaddr_in, заплывая в ее поля номер порта (7500) и адрес. 127.0.0.1 - это возвратный адрес, который означает, что сервер находится на той же машине, что и клиент.
Получение сокета и соединение с сервером
15-20 Получаем сокет типа SOCK_STREAM. Как было отмечено выше, протокол TCP, будучи потоковым, требует именно такого сокета.
21-26 Устанавливаем соединение с сервером, обращаясь к системному вызову connect. Этот вызов нужен, чтобы сообщить ядру адрес сервера.
Отправка и получение одного байта
27- 38 Сначала посылаем один байт серверу, затем читаем из сокета один байт и записываем полученный байт в стандартный вывод и завершаем сеанс.
Прежде чем тестировать клиента, необходим сервер. Вызовы API сокетов для сервера немного иные, чем для клиента. Они показам на рис. 1.3.
Сервер должен быть готов к установлению соединений с клиентами. Для этого он обязан прослушивать известный ему порт с помощью системного вызова listen. Но предварительно необходимо привязан адрес интерфейса и номер порта к прослушивающему сокету. Для этого предназначен вызов bind:
#include <sys/socket.h> /* UNIX */
#include <winsock2.h> /* Windows */
int bind(SOCKET s, const struct sockaddl *name, int namelen);
Возвращаемое значение: 0 - нормально, -1 (UNIX) или SOCKET_ERROR (Windows) - ошибка.
Параметр s - это дескриптор прослушивающег сокета. С помощью параметров name и namelen передаются порт и сетевой интерфейс, которые нужно прослушивать. Обычно в качестве адреса задается консанта INADDR_ANY. Это означает, что будет принято соединение, запрашиваемое по любому интерфейсу. Если хосту с несколькими сетевыми адресами нужно принимать соединения только по одному интерфейсу, то следует указать IP-адрес этого интерфейса. Как обычно, namelen - длина структуры sockaddr_in.
После привязки локального адреса к сокету нужно перевести сокет в режим прослушивания входящих соединений с помощью системного вызова listen, назначение которого часто не понимают. Его единственная задача - пометить сокет как прослушивающий. Когда хосту поступает запрос на установление соединения, ядро ищет в списке прослушивающих сокетов тот, для которого адрес назначения и номер порта соответствуют указанным в запросе.
#include <sys/socket.h> /* UNIX */
#include <winsock2.h> /* Windows */
int listen( SOCKET s, int backlog);
Возвращаемое значение: О - нормально, -1 (UNIX) или SOCKET_ERROR (Windows) - ошибка.
Параметр s - это дескриптор сокета, который нужно перевести в режим прослушивания. Параметр backlog - это максимальное число ожидающих, но еще не принятых соединений. Следует отметить, что это не максимальное число одновременных соединений с данным портом, а лишь максимальное число частично установленных соединений, ожидающих в очереди, пока приложение их примет (описание системного вызова accept дано ниже).
Рис. 1.3. Основные вызовы API сокетов для сервера
Традиционно значение параметра backlog не более пяти соединений, но в современных реализациях, которые должны поддерживать приложения с высокой нагрузкой, например, Web-сервера, оно может быть намного больше. Поэтому, чтобы выяснить его истинное значение, необходимо изучить документацию по конкретной системе. Если задать значение, большее максимально допустимого, то система уменьшит его, не сообщив об ошибке.
И последний вызов, который будет здесь рассмотрен, - это accept. Он служит для приема соединения, ожидающего во входной очереди. После того как соединение принято, его можно использовать для передачи Данных, например, с помощью вызовов recv и send. В случае успеха accept возвращает дескриптор нового сокета, по которому и будет происходить обмен данными. Номер локального порта для этого сокета такой же, как и для прослушивающего сокета. Адрес интерфейса, на который поступил запрос о соединении, называется Локальным. Адрес и номер порта клиента считаются удаленными.
Обратите внимание, что оба сокета имеют один и тот же номер локального порта. Это нормально поскольку TCP-соединение полностью определяется четырьмя параметрами - локальным адресом, локальным портом, удаленным адресом и удаленным портом. Поскольку удаленные адрес и порт для этих двух сокетов различны, то ядро может отличить их друг от друга.
#include <sys/socket.h> /* UNIX */
#include <winsock2.h> /* windows */
int accept (SOCKET s, struct sockaddr *addr, int *addrlen);
Возвращаемое значение: 0- нормально, -1 (UNIX) или INVALID_SOCKET (Windows) - ошибка
Параметр s – это дескриптор прослушивающего сокета. Как показано на рис. 1.3, accept возвращает адрес приложения на другом конце соединения в структуре sockaddr_in, на которую указывает параметр addr. Целому числу, на которое указывает параметр addrlen, ядро присваивает значение, равное длине этой структуры. Часто нет необходимости знать адрес клиентского приложения, поэтому в качестве add и addrlen будет передаваться NULL.
В листинге 1. 2 приведен простейший сервер. Эта программа также очень схематична, поскольку ее назначение - продемонстрировать структуру сервера и элементарные вызовы API сокетов, которые обязан выполнить любой сервер. Обратите внимание что как и в случае с клиентом на рис. 1.2, сервер следует потоку управления, годному на рис. 1.3.
Листинг 1.2. Простой TCP-сервер
1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <stdio.h>
5 int main (void)
6 {
7 struct sockaddr_in local;
8 int s;
9 int s1;
10 int rc;
11 char buf [ 1 ];
12 local.sin_family = AF_INET;
13 local.sin_port = htons( 7500 ) ;
14 local.sin_addr.s_ addr = htonl ( INADDR_ANY );
15 s = socket ( AF_INET, SOCK_STREAM, 0 );
16 if ( s < 0 )
17 {
18 perror("ошибка вызова socket" );
19 exit ( 1 );
20 }
21 rc = bind( s, ( struct sockaddr * )&local, sizeof ( local ) );
22 if ( rc < 0 )
23 {
24 perror ( "ошибка вызова bind" );
25 exit ( 1 );
26 }
27 rc = listen( s, 5 );
28 if ( rc )
29 {
30 perror ( "ошибка вызова listen" );
31 exit ( 1 );
32 }
33 s1 = accept( s, NULL, NULL );
34 if ( s1 < 0 )
35 {
36 perror ( "ошибка вызова accept" );
37 exit ( 1 );
38 }
39 rc = recv( s1, buf, 1, 0 );
40 if ( rc <= 0 )
41 {
42 perror( "ошибка вызова recv" );
43 exit ( 1 );
44 }
45 printf( "%c\n", buf[ 0 ] );
46 rc = send( s1, "2", 1, 0 );
47 if ( rc <= 0 )
48 perror( "ошибка вызова send" );
49 exit ( 0 )
50 }
Заполнение адресной структуры и получение сокета
12- 20 Заполняем структуру sockaddr_in, записывая в ее поля известные адресе и номер порта, получаем сокет типа SOCK_STREAM, который и будет прослушивающим.
Привязка известного порта и вызов listen
21-32 Привязываем известные порт и адрес, записанные в структуру local, к полученному сокету. Затем вызываем listen, чтобы пометить сокет как прослушивающий.
Принятие соединения
33-39 Вызываем accept для приема новых соединений. Вызов accept блокирует выполнение программы до тех пор, пока не поступит запрос на соединение, после чего возвращает новый сокет для этого соединения.
Обмен данными
39-49 Сначала читаем и печатаем байт со значением 1, полученный от клиента. Затем посылаем один байт со значением 2 назад клиенту и завершаем программу.
Теперь можно протестировать клиент и сервер, запустив сервер в одном окне, а клиент - в другом. Обратите внимание, что сервер должен быть запущен первым, иначе клиент аварийно завершится с сообщением Connection refused (В соединении отказано).
bsd: $ simplec
ошибка вызова connect: Connection refused
bsd: $
Ошибка произошла потому, что при попытке клиента установить соединение не было сервера, прослушивающего порт 7500.
Теперь следует поступить правильно, то есть запустить сервер до запуска клиента:
bsd: $ simples 1 bsd: $ |
bsd: $ simplec 2 bsd: $ |