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

Просто резюмирую:

Сетевой сокет — это конечная точка соединения в компьютерной сети. Это дескриптор (абстрактная ссылка), который программа может передать сетевому интерфейсу прикладного программирования (API), чтобы использовать соединение для приема и отправки данных. Сокеты часто представляются внутри как целые числа.

Создание сокета

Создание одинаково, независимо от того, хотим ли мы создать сервер, клиент или просто отправлять дейтаграммы. Об этом подробно рассказано в первом посте.

Подключение розетки

Следующее, что нам нужно сделать, это подключить наш локальный сокет к удаленному. Это делается с помощью системного вызова connect.

int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

Как вы знаете, сокеты, которые должны принимать соединения, привязаны к имени (адресу). Для установления соединения нам нужно указать две вещи — локальный сокет, к которому мы хотим подключиться, и адрес удаленного узла.

Обратите внимание, что это имеет смысл только в том случае, если базовый протокол ориентирован на соединение (например, TCP). Если это не так (UDP), connect в основном просто устанавливает адрес назначения по умолчанию для send.

Таким образом, системный вызов connect соединяет сокет, указанный файловым дескриптором sockfd, с адресом, указанным addr. Аргумент addrlen указывает размер addr. Формат адреса в addr определяется семейством адресов сокета.

Давайте посмотрим на грубый пример:

sockfd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_aton("127.0.0.1", &serv_addr.sin_addr);
serv_addr.sin_port = htons(8000);
connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr))

Отправка данных

После того, как у нас есть клиентский сокет, мы можем начать обмен данными. Обмен данными работает двумя способами — отправка и получение. Сначала давайте отправим некоторые данные, используя семейство отправки функций.

Мы объясним использование следующих двух:

ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

Разница между send и sendto заключается в том, что send предназначен только для сокетов с connect, а sendto может использоваться с сокетами, использующими протоколы без установления соединения.

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

Хватит говорить, давайте посмотрим на пример, когда сокет имеет connect-ed.

// ...
// continues from connect example
connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr))
nbytes = send(sockfd, buf, sizeof(buf), 0);

И пример, когда сокет использует UDP, поэтому он не connect-ed и использует sendto.

sockfd = socket(PF_INET, SOCK_DGRAM, 0); // note SOCK_DGRAM type
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_aton("127.0.0.1", &serv_addr.sin_addr);
serv_addr.sin_port = htons(5553);
nbytes = sendto(sockfd, msg, strlen(msg)+1, 0,
        (struct sockaddr *) &serv_addr, sizeof(serv_addr));

Получение данных

Вещи для получения почти такие же, как и для отправки. Давайте посмотрим на семейство recv функций.

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

Опять же, recv предназначен для подключенных сокетов, а recvfrom может использоваться с сокетами, использующими протокол без установления соединения. Единственная особенность заключается в том, что src_addr на самом деле является аргументом «значение-результат», которому будет присвоен адрес отправителя, если его предоставляет базовый протокол.

Если нас не волнует удаленный адрес, следует передать NULL. Аргумент addrlen также является аргументом значения-результата, который вызывающая сторона должна инициализировать перед вызовом размером буфера, связанным с src_addr, и изменить его по возвращении, чтобы указать фактический размер исходного адреса.

Давайте снова посмотрим на примеры. Если мы используем протокол, ориентированный на соединение, и мы уже выполнили connect-ed, нам просто нужно вызвать recv для сокета:

buf = malloc(1024);
nbytes = recv(sockfd, buf, 1024, 0);

Но если мы не устанавливаем соединение, нам нужно сначала bind адрес сокета, потому что мы фактически действуем как сервер, а не клиент!

sockfd = socket(PF_INET, SOCK_DGRAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(5553);
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
buf = malloc(1024);
nbytes = recvfrom(sockfd, buf, 1024, 0,
        (struct sockaddr *) &peer_addr, &peer_addr_size);

Закрытие сокета

После того, как мы закончили использовать сокет, мы должны освободить ресурсы, выделенные для него. Это можно просто сделать через close. Если есть еще данные, ожидающие передачи по соединению, обычно close пытается завершить эту передачу.

close(sockfd);

Закрытие предотвратит дальнейшее чтение и запись в сокет. Любой, кто попытается прочитать или записать сокет на удаленном конце, получит сообщение об ошибке.

Но если нам нужно закрыть соединение только одним способом, например, мы не хотим выключать прием, мы должны использовать shutdown.

int shutdown(int sockfd, int how);

Аргумент how управляет тем, что должно быть выключено, и должен иметь одно из следующих значений:

SHUT_RD - further receptions will be disallowed
SHUT_WR - further transmissions will be disallowed
SHUT_RDWR - further receptions and transmissions will be disallowed

Обратите внимание, что вызов shutdown фактически не закрывает дескриптор сокета. Таким образом, вызов close также необходим, если вы хотите освободить дескриптор.

shutdown(sockfd, SHUT_WR); // tell remote peer you're done
recv(sockfd, buf, 1024, 0); // read all what's left
close(sockfd); // free the socket descriptor

Резюме

Вот и все — мы создали и сервер, и клиент, используя как протоколы с установлением соединения, так и протоколы без установления соединения. Вот системные вызовы, которые мы использовали в этой главе:

connect - initiate a connection on a socket
send - send a message from a socket
recv - receive a message from a socket
shutdown - shut down part of a full-duplex connection on a socket
close - close a socket descriptor

Оставайтесь с нами в следующей части, где мы рассмотрим другие интересные вещи, связанные с сокетами.