Второй шаг – создание объекта "сокет". Это осуществляется функцией
SOCKET socket (int af, int type, int protocol)
Первый слева аргумент указывает на семейство используемых протоколов. Для интернет-приложений он должен иметь значение AF_INET. Следующий аргумент задает тип создаваемого сокета - потоковый (SOCK_STREAM) или дейтаграммный (SOCK_DGRAM). Последний аргумент уточняет, какой транспортный протокол следует использовать. Нулевое значение соответствует выбору по умолчанию: TCP для потоковых сокетов и UDP для дейтаграммных. Если функция завершилась успешно, она возвращает дескриптор сокета, в противном случае значение INVALID_SOCKET.
Дальнейшие шаги зависит от того, является приложение сервером или клиентом. Ниже эти два случая будут описаны раздельно.
Клиент: шаг третий - для установки соединения с удаленным узлом потоковый сокет должен вызвать функцию
int connect (SOCKET s, const struct sockaddr FAR* name, int namelen)
Датаграмные сокеты работают без установки соединения, поэтому, обычно не обращаются к функции connect.
Первый слева аргумент - дескриптор сокета, возращенный функцией socket; второй - указатель на структуру "sockaddr", содержащую в себе адрес и порт удаленного узла с которым устанавливается соединение. Последний аргумент сообщает функции размер структуры sockaddr.
После вызова connect система предпринимает попытку установить соединение с указанным узлом. Если по каким-то причинам это сделать не удастся (адрес задан неправильно, узел не существует или "висит", компьютер находится не в сети), функция возвратит ненулевое значение.
Сервер: шаг третий – прежде, чем сервер сможет использовать сокет, он должен связать его с локальным адресом. Локальный, как, впрочем, и любой другой адрес Интернета, состоит из IP-адреса узла и номера порта. Если сервер имеет несколько IP адресов, то сокет может быть связан как со всеми сразу (для этого вместо IP-адреса следует указать константу INADDR_ANY равную нулю), так и с каким-то конкретным одним.
Связывание осуществляется вызовом функции
int bind (SOCKET s, const struct sockaddr FAR* name, int namelen)
Первым слева аргументом передается дескриптор сокета, возращенный функций socket, за ним следуют указатель на структуру sockaddr и ее длина.
Строго говоря, клиент также должен связывать сокет с локальным адресом перед его использованием, однако, за него это делает функция connect, ассоциируя сокет с одним из портов, наугад выбранных из диапазона 1024-5000. Сервер же должен "садиться" на заранее определенный порт, например, 21 для FTP, 23 для telnet, 25 для SMTP, 80 для WEB, 110 для POP3 и т.д. Поэтому ему приходится осуществлять связывание "вручную".
При успешном выполнении функция возвращает нулевое значение и ненулевое в противном случае.
Сервер: шаг четвертый - выполнив связывание, потоковый сервер переходит в режим ожидания подключений, вызывая функцию
int listen (SOCKET s, int backlog),
где s – дескриптор сокета, а backlog – максимально допустимый размер очереди сообщений.
Размер очереди ограничивает количество одновременно обрабатываемых соединений, поэтому, к его выбору следует подходить "с умом". Если очередь полностью заполнена, очередной клиент при попытке установить соединение получит отказ (TCP пакет с установленным флагом RST). В то же время максимально разумное количество подключений определяются производительностью сервера, объемом оперативной памяти и т.д.
Датаграммные серверы не вызывают функцию listen, т.к. работают без установки соединения и сразу же после выполнения связывания могут вызывать recvfrom для чтения входящих сообщений, минуя четвертый и пятый шаги.
Сервер: шаг пятый – извлечение запросов на соединение из очереди осуществляется функцией
SOCKET accept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen),
которая автоматически создает новый сокет, выполняет связывание и возвращает его дескриптор, а в структуру sockaddr заносит сведения о подключившемся клиенте (IP-адрес и порт). Если в момент вызова accept очередь пуста, функция не возвращает управление до тех пор, пока с сервером не будет установлено хотя бы одно соединение. В случае возникновения ошибки функция возвращает отрицательное значение.
Для параллельной работы с несколькими клиентами следует сразу же после извлечения запроса из очереди порождать новый поток (процесс), передавая ему дескриптор созданного функцией accept сокета, затем вновь извлекать из очереди очередной запрос и т.д. В противном случае, пока не завершит работу один клиент, север не сможет обслуживать всех остальных.
Клиент и сервер: после того как соединение установлено, потоковые сокеты могут обмениваться с удаленным узлом данными, вызывая функции
int send (SOCKET s, const char FAR * buf, int len,int flags)
int recv (SOCKET s, char FAR* buf, int len, int flags)
для посылки и приема данных соответственно.
Функция send возвращает управление сразу же после ее выполнения независимо от того, получила ли принимающая сторона наши данные или нет. При успешном завершении функция возвращает количество передаваемых (не переданных!) данных - т. е. успешное завершение еще не свидетельствует об успешной доставке! В общем-то, протокол TCP (на который опираются потоковые сокеты) гарантирует успешную доставку данных получателю, но лишь при условии, что соединение не будет преждевременно разорвано. Если связь прервется до окончания пересылки, данные останутся не переданными, но вызывающий код не получит об этом никакого уведомления! А ошибка возвращается лишь в том случае, если соединение разорвано до вызова функции send.
Функция же recv возвращает управление только после того, как получит хотя бы один байт. Точнее говоря, она ожидает прихода целой дейтаграммы. Дейтаграмма - это совокупность одного или нескольких IP пакетов, посланных вызовом send. Упрощенно говоря, каждый вызов recv за один раз получает столько байтов, сколько их было послано функцией send. При этом подразумевается, что функции recv предоставлен буфер достаточных размеров, - в противном случае ее придется вызвать несколько раз. Однако, при всех последующих обращениях данные будут браться из локального буфера, а не приниматься из сети, т.к. TCP-провайдер не может получить "кусочек" дейтаграммы, а только ею всю целиком.
Дейтаграммный сокет так же может пользоваться функциями send и recv, если предварительно вызовет connect (см. "Клиент: шаг третий"), но у него есть и свои, "персональные", функции:
int sendto (SOCKET s, const char FAR * buf, int len,int flags, const struct sockaddr FAR * to, int tolen)
int recvfrom (SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen)
Они очень похожи на send и recv, - разница лишь в том, что sendto и recvfrom требуют явного указания адреса узла принимаемого или передаваемого данные. Вызов recvfrom не требует предварительного задания адреса передающего узла - функция принимает все пакеты, приходящие на заданный UDP-порт со всех IP адресов и портов. Напротив, отвечать отправителю следует на тот же самый порт откуда пришло сообщение. Поскольку, функция recvfrom заносит IP-адрес и номер порта клиента после получения от него сообщения, программисту фактически ничего не нужно делать - только передать sendto тот же самый указатель на структуру sockaddr, который был ранее передан функции recvfrem, получившей сообщение от клиента.
Еще одна деталь – транспортный протокол UDP, на который опираются дейтаграммные сокеты, не гарантирует успешной доставки сообщений и эта задача ложиться на плечи самого разработчика. Решить ее можно, например, посылкой клиентом подтверждения об успешности получения данных. Правда, клиент тоже не может быть уверен, что подтверждение дойдет до сервера, а не потеряется где-нибудь в дороге. Подтверждать же получение подтверждения - бессмысленно, т. к. это рекурсивно неразрешимо. Лучше вообще не использовать дейтаграммные сокеты на ненадежных каналах.