Как я и обещал в предыдущей статье, сегодня мы рассмотрим, как Winsock-приложение может принимать входящие соединения. Архитектура "клиент-сервер" ныне у всех на устах, поэтому, дабы не отставать от моды, мы займемся организацией простейшего сервера, принимающего входящие соединения.
Приложения-клиенты, обращающиеся к нашему серверу, мы напишем в следующем выпуске. Самые нетерпеливые могут сделать это сами.
Сделаем небольшое теоретическое отступление.
Для того, что бы сервер мог принимать входящие соединения от клиентов, клиенты должны знать адрес сервера. Кроме адреса, клиент должен знать порт, на котором сервер ожидает появления запроса на соединение. То есть, полный адрес сервера состоит из его IP адреса (имени) и порта. Приложение-клиент, должно знать адрес сервера и порт перед установкой соединения.
Например, 216.127.88.3: 80, это адрес и порт HTTP сервера, так как HTTP сервис находиться именно на этом порте. По аналогии FTP сервис находится на 21 порту, SOCKS на 1080 и так далее.
Что же нам нужно сделать для того, что бы наше приложение-сервер, работало в режиме приёма входящих соединений? Вот список необходимых нам действий:
1) Создаём сокет
2) Привязываем сокет к определенному IP-адресу и порту.
3) Устанавливаем сокет в "слушающий" режим.
4) Ждём запроса на соединение.
5) Обрабатываем входящий запрос (если он поступил в п. 4) и производим все необходимые нам действия (регистрацию, обмен данными и т.п.)
6) Goto пункт 4.
Как видите, список необходимых действий не сложный. Однако с некоторыми пунктами у нас возникнут проблемы из-за нехватки знаний материала. Например, пункты 2-4. В этой статье мы исправим ситуацию!
Давайте рассмотрим практическую реализацию каждого пункта средствами Winsock. Итак:
Каким образом, и при каких обстоятельствах сокет привязывается к определенному IP-адресу/порту? Когда сокет создаётся, он изначально не имеет привязки к определенному адресу и порту. Привязка происходит в случае выполнения функции connect (автоматически), либо по требованию программиста (функция bind), до начала операций ввода/вывода
Рассмотрим функцию bind.
int bind(SOCKET s, // (in) сокет
const struct sockaddr FAR *name, // (in) структура, содержащая нужный адрес и порт
int namelen // (in) длинна параметра name
);
Параметр
name заполняется следующим образом:
sockaddr_in name;
ZeroMemory (&name, sizeof (name));
name.sin_family = AF_INET;
name.sin_addr.S_un.S_addr = htonl (INADDR_ANY);
name.sin_port = htons (0);
В данном случае, система сама вбирает IP адрес и номер порта, которые закрепятся за сокетом. Для того, что бы предоставить системе право выбора IP и порта адреса мы должны указать INADDR_ANY в качестве IP-адреса, и 0 для порта. Можно также, задать IP-адрес самостоятельно, а выбор свободного порта предоставить системе (можно наоборот), или задать самим и адрес и порт.
Для приложений, которым не все равно, какой из локальных адресов/портов система присвоит данному сокету, используем такой вариант:
sockaddr_in name;
ZeroMemory (&name, sizeof (name));
name.sin_family = AF_INET;
name.sin_addr.S_un.S_addr = inet_addr ("193.108.128.229");
name.sin_port = htons (5678);
В этом случае, сокет привязывается к интерфейсу 193.108.128.229, порт 5678. Следует принимать во внимание, что на машине, на которой выполняется программа, должен присутствовать адрес 193.108.128.229. Также, значение порта не должно превышать 0xffff (65535) и запрашиваемый порт должен быть не занят другим приложением.
Такая форма заполнения (с явным указанием IP адреса) нужна, например, если машина имеет более чем один сетевой интерфейс (multihomed). Тогда, мы указываем нужный нам сетевой адаптер. На моей машине одна сетевая карта имеет внутренний IP-адрес, а вторая реальный интернет адрес. Если я хочу создать сервер видимый извне, то мне нужно привязать сокет ко второй сетевой карте.
sockaddr_in name;
ZeroMemory (&name, sizeof (name));
name.sin_family = AF_INET;
name.sin_addr.S_un.S_addr = inet_addr ("193.108.128.229");
name.sin_port = htons (0);
В данном случае, мы явно указываем IP адрес, но позволяем системе самой выбрать свободный порт.
Пример вызова:
sockaddr_in name;
ZeroMemory (&name, sizeof (name));
name.sin_family = AF_INET;
name.sin_addr.S_un.S_addr = inet_addr ("193.108.128.229");
name.sin_port = htons (5678);
if (SOCKET_ERROR == bind (test_sock, (sockaddr* ) &name, sizeof (name) ) )
{
return E_FAIL;
}
A если нет ошибки, функция возвращает ноль, в противном случае - SOCKET_ERROR.
В случае, если при вызове bind IP-адрес/порт не были указанны явно, система, как мы знаем, выбирает адрес/порт самостоятельно. Для того, что бы узнать, какой адрес или порт был выбран для конкретного сокета в результате вызова bind, мы можем использовать функцию
getsockname.
int getsockname(SOCKET s, // (in) сокет
struct sockaddr FAR *name, // (out) структура, в которую система поместит данные
int FAR *namelen // (in, out) длинна данных
);
Пример:
sockaddr_in name;
int n_l = sizeof (name);
ZeroMemory (&name, sizeof (name));
If (SOCKET_ERROR == getsockname (test_sock, (sockaddr* ) &name, &n_l))
{
// Error
}
Если функция выполнилась успешно, то структура name должна содержать информацию о типе адреса, IP-адрес и порт данного сокета.
Если у вас возникнет необходимость вывести содержимое структуры name (тип sockaddr_in) на экран, нам пригодятся следующие функции:
inet_ntoa конвертирует интернет-адрес в строку формата "xxx.xxx.xxx.xxx"
char FAR * inet_ntoa(struct in_addr in // адрес в сетевом формате
);
Пример использования:
printf ("Server IP: %s", inet_ntoa ((in_addr) name.sin_addr));
2.
ntohs конвертирует 16-битное значение из TCP/IP формата в нужный для данной машины формат 16-битного числа (для процессоров семейства Intel - little-endian)
u_short ntohs(u_short netshort
);
Пример использования:
printf ("Server port: %d", ntohs (name.sin_port));
Таким образом, с помощью функций
getsockname, inet_ntoa, ntohs мы можем узнать все необходимые нам данные относительно привязки сокета.
Для того, что бы сокет мог в дальнейшем принимать входящие соединения, мы должны использовать функцию, которая переводит сокет в "слушающий режим".
int listen(SOCKET s, // Сокет
int backlog // Количество входящих соединений для данного сокета
);
Пример использования:
if (FAILED (listen (s, SOMAXCONN) ) ) // Максимально возможное кол-во
{
// входящих соединений
return E_FAIL;
}
или
if (FAILED (listen (s, 10) ) ) // Максимум 10 входящих соединений
{
return E_FAIL;
}
SOMAXCONN максимально возможное количество соединений для данной системы.
Если функция выполнилась успешно, он возвращает нулевое значение, в противном случае
SOCKET_ERROR.
После того, как сокет был помешен в слушающее состояние, он может принимать входящие соединения. Для приёма входящих соединений используется функция accept.
SOCKET accept(SOCKET s, // сокет
struct sockaddr FAR *addr, // Адрес присоединившейся машины
int FAR *addrlen // Длинна адреса
);
При вызове accept, сокет блокируется вплоть до появления сигнала о входящем соединении (по аналогии к функциям из предыдущей статьи). Функция возвращает новый сокет, который будет использоваться для связи с присоединившейся машиной (система создаёт его сама, при успешном соединении). Более подробные данные о присоединившейся машине accept возвращает в параметре addr (тип адреса, IP-адрес, порт).
Пример вызова:
SOCKET new_sock;
Sockaddr_in new_ca;
int new_len = sizeof (new_ca);
ZeroMemory (&new_ca, sizeof (new_ca));
if (FAILED (new_sock = accept (s, (sockaddr* ) &new_ca, &new_len) ) )
{
// Error
}
//Теперь new_sock можно использовать для передачи данных.
Если функция выполнилась успешно, он возвращает новый сокет для установленного соединения, в противном случае
INVALID_SOCKET.
Теперь мы можем приступить к написанию простейшего приложения-сервера. Давайте еще раз опишем протокол работы клиентской и серверной части.
Сервер:
1) Создаём сокет
2) Привязываем сокет к определенному IP-адресу и порту.
3) Устанавливаем сокет в "слушающий" режим.
4) Ждём запроса на соединение.
5) Обрабатываем входящий запрос (если он поступил в п. 4), а именно принимаем от клиента одно сообщение и выводим его на экран.
6) Закрываем соединение с данным клиентом
7) Если максимальный лимит входящих соединений для данной программы не достигнут (устанавливается при вызове listen), то идём в пункт 4 (и ждём следующего соединения), иначе завершаем работу.
Как вы видите из схемы, сервер принимает соединения до тех пор, пока не будет достигнут лимит, установленный при вызове listen, или вы не закрыли приложение-сервер. При открытии очередного соединения, сервер получает сообщение от клиента, выводит его на экран и закрывает соединение.
Клиент:
1) Создаём сокет
2) Выполняем соединение с сервером
3) Посылаем сообщение
4) Закрываем соединение с сервером
5) Завершаем работу.
Принцип работы клиента еще более прост. Я думаю, вы без труда сможете реализовать простейшее приложение-клиент, работающее с данным сервером, просмотрев исходный код сервера. Скачать пример можно отсюда
ws4.zipКонечно, данную программу можно назвать громким словом "сервер" с большой натяжкой. Она даже не отвечает на запрос клиента . Я думаю, это досадное упущение вы сможете исправить сами!
Так же вы можете самостоятельно поэкспериментировать с проблемой блокировки сокета на функции accept. Я предлагаю вам решить ее с помощью функции select (которую мы рассматривали в предыдущей статье) самостоятельно.
Если появятся проблемы пишите на форумы, обсудим их вместе!
Не ленитесь экспериментировать самостоятельно!
В следующем выпуске читайте:
- Решаем проблему блокировки accept
- Разрабатываем собственную ICQ
Искренне Ваш Андрей Онофрейчук aka Pepper (Another Day LTD,
www.anotherd.com ).
Вопросы, комментарии
pepper@anotherd.com