Как работают сокеты c
Состояние TIME-WAIT
После активного закрытия для данного конкретного соединения стек входит в состояние TIME-WAIT на время 2MSL (максимальное время жизни пакета) для того, чтобы
- заблудившийся пакет не попал в новое соединение с такими же параметрами.
- если потерялся ACK, подтверждающий закрытие соединения, с активной стороны, пассивная снова пощлёт FIN, активная, игнорируя TIME-WAIT уже закрыла соединение, поэтому пассивная сторона получит RST.
Отключение состояния TIME-WAIT крайне не рекомендуется, так как это нарушает безопасность TCP соединения, тем не менее существует возможность сделать это - опция сокета SO_LINGER.
Штатная ситуация - перезагрузка сервера может пострадать из-за наличия TIME-WAIT. Эта проблема решается заданием опции SO_REUSEADDR.
Отложенное подтверждение и алгоритм Нейгла.
Алгоритм Нейгла используется для предотвращения забивания сети мелкими пакетами и имеет очень простую формулировку - запрещено посылать второй маленький пакет до тех пор, пока не придет подтверждение на первый. Тем не менее данные отправляются при выполнении хотя бы одного из следующих условий:
- можно послать полный сегмент размером MSS (максимальный размер сегмента)
- соединение простаивает, и можно опустошить буфер передачи
- алгоритм Нейгла отключен, и можно опустошить буфер передачи
- есть срочные данные для отправки
- есть маленьки сегмент, но его отправка уже задержана на достаточно длительное время (таймер терпения persist timer на тайм-аут ретрансмиссии RTO )
- окно приема, объявленное хостом на другом конце, открыто не менее чем на половину
- необходимо повторно передать сегмент
- требуется послать ACK на принятые данные
- нужно объявить об обновлении окна
Кроме того, существует отложенное подтверждение. При этом хост, получивший сегмент, старается задержать отправку ACK, чтобы послать её вместе с данными.
Алгоритм Нейгла в купе с отложенным подтверждением в резонансе дают нежелательные задержки. Поэтому часто его отключают. Отключение алгоритма Нейгла производится заданием опции TCP_NODELAY
Но более правильным было бы проектировать приложение таким образом, чтобы было как можно меньше маленьких блоков. Лучше писать большие. Для этого можно объединять данные самостоятельно, а можно пользоваться аналогом write, работающим с несколькими буферами:
У некоторых запросов есть тело. Когда отправляется форма методом POST, в теле запроса передаются значения полей этой формы.
Тело запроса отделяется от заголовков одной пустой строкой. Заголовок «Content-Type» говорит серверу, в каком формате закодировано тело запроса. По умолчанию, в HTML-форме данные кодируются методом «application/x-www-form-urlencoded».
Иногда необходимо передать данные в другом формате. Например, при загрузке файлов на сервер, бинарные данные кодируются методом «multipart/form-data».
Сервер обрабатывает запрос клиента и возвращает ответ.
Пример ответа сервера:
В первой строке ответа передается версия протокола и статус ответа. Для успешных запросов обычно используется статус «200 OK». Если ресурс не найден на сервере, возвращается «404 Not Found».
Тело ответа так же, как и у запроса, отделяется от заголовков одной пустой строкой.
Что будет делать сервер?
Сервер будет принимать запросы клиентов, парсить заголовки и тело запроса, и возвращать тестовую HTML-страничку, на которой отображены данные запроса клиента (запрошенный URL, метод запроса, cookie и другие заголовки).
О сокетах
Для работы с сетью на низком уровне традиционно используют сокеты. Сокет — это абстракция, которая позволяет работать с сетевыми ресурсами, как с файлами. Мы можем писать и читать данные из сокета почти так же, как из обычного файла.
В этом материале мы будем работать с виндовой реализацией сокетов, которая находится в заголовочном файле <WinSock2.h> . В Unix-подобных ОС принцип работы с сокетами такой же, только отличается API. Вы можете подробнее почитать о сокетах Беркли, которые используются в GNU/Linux.
Создание сокета
Создадим сокет с помощью функции socket , которая находится в заголовочном файле <WinSock2.h> . Для работы с IP-адресами нам понадобится заголовочный файл <WS2tcpip.h> .
Мы подготовили все данные, которые необходимо для создания сокета и создали сам сокет. Функция socket возвращает целочисленное значение файлового дескриптора, который выделен операционной системой под сокет.
Привязка сокета к адресу (bind)
Следующим шагом, нам необходимо привязать IP-адрес к сокету, чтобы он мог принимать входящие соединения. Для привязки конкретного адреса к сокету используется фукнция bind . Она принимает целочисленный идентификатор файлового дескриптора сокета, адрес (поле ai_addr из структуры addrinfo ) и размер адреса в байтах (используется для поддержки IPv6).
Подготовка сокета к принятию входящих соединений (listen)
Подготовим сокет к принятию входящих соединений от клиентов. Это делается с помощью функции listen . Она принимает дескриптор слушающего сокета и максимальное количество одновременных соединений.
В случае ошибки, функция listen возращает значение константы SOCKET_ERROR . При успешном выполнении она вернет 0.
В константе SOMAXCONN хранится максимально возможное число одновременных TCP-соединений. Это ограничение работает на уровне ядра ОС.
Ожидание входящего соединения (accept)
Функция accept ожидает запрос на установку TCP-соединения от удаленного хоста. В качестве аргумента ей передается дескриптор слушающего сокета.
При успешной установке TCP-соединения, для него создается новый сокет. Функция accept возвращает дескриптор этого сокета. Если произошла ошибка соединения, то возвращается значение INVALID_SOCKET .
Получение запроса и отправка ответа
При успешном выполнении функция recv вернет размер полученных данных. В случае ошибки возвращается значение SOCKET_ERROR . Если соединение было закрыто клиентом, то возвращается 0.
После получения запроса мы сразу же отправили ответ клиенту с помощью функции send . Она принимает дескриптор сокета, строку с данными ответа и размер ответа в байтах.
В случае ошибки, функция возвращает значение SOCKET_ERROR . В случае успеха — количество переданных байт.
Попробуем скомпилировать программу, не забыв предварительно завершить функцию main .
Последовательная обработка запросов
Чтобы сервер не завершал работу после обработки первого запроса, а продолжал обрабатывать новые соединения, нужно зациклить ту часть кода, которая принимает запрос на установку соединения и возвращает ответ.
Когда сервер закончит обработку запроса одного клиента, он закроет соединение с ним и будет ожидать нового запроса.
Примечание: если вы используете MinGW в Windows, то библиотеку Ws2_32.lib нужно вручную прописать в настройках линковщика.
Рассмотрим основные свойства данного класса:
AddressFamily : возвращает все адреса, используемые сокетом. Данное свойство представляет одно из значений, определенных в одноименном перечислении AddressFamily . Перечисление содержит 18 различных значений, наиболее используемые:
InterNetwork : адрес по протоколу IPv4
InterNetworkV6 : адрес по протоколу IPv6
Ipx : адрес IPX или SPX
NetBios : адрес NetBios
Available : возвращает объем данных, которые доступны для чтения
Connected : возвращает true, если сокет подключен к удаленному хосту
LocalEndPoint : возвращает локальную точку, по которой запущен сокет и по которой он принимает данные
ProtocolType : возвращает одно из значений перечисления ProtocolType , представляющее используемый сокетом протокол. Есть следующие возможные значения:
IPSecAuthenticationHeader (Заголовок IPv6 AH)
IPSecEncapsulatingSecurityPayload (Заголовок IPv6 ESP)
IPv6DestinationOptions (Заголовок IPv6 Destination Options)
IPv6FragmentHeader (Заголовок IPv6 Fragment)
IPv6HopByHopOptions (Заголовок IPv6 Hop by Hop Options)
IPv6NoNextHeader (Заголовок IPv6 No next)
IPv6RoutingHeader (Заголовок IPv6 Routing)
Unknown (неизвестный протокол)
Unspecified (неуказанный протокол)
Каждое значение представляет соответствующий протокол, но наиболее используемыми являются Tcp и Udp.
RemoteEndPoint : возвращает адрес удаленного хоста, к которому подключен сокет
SocketType : возвращает тип сокета. Представляет одно из значений из перечисления SocketType :
Dgram : сокет будет получать и отправлять дейтаграммы по протоколу Udp. Данный тип сокета работает в связке с типом протокола - Udp и значением AddressFamily.InterNetwork
Seqpacket : обеспечивает надежную двустороннюю передачу данных с установкой постоянного подключения
Stream : обеспечивает надежную двустороннюю передачу данных с установкой постоянного подключения. Для связи используется протокол TCP, поэтому этот тип сокета используется в паре с типом протокола Tcp и значением AddressFamily.InterNetwork
Unknown : адрес NetBios
Для создания объекта сокета можно использовать один из его конструкторов. Например, сокет, использующий протокол Tcp:
Или сокет, использующий протокол Udp:
Таким образом, при создании сокета мы можем указывать разные комбинации протоколов, типов сокета, значений из перечисления AddressFamily. Однако в то же время не все комбинации являются корректными. Так, для работы через протокол Tcp, нам надо обязательно указать параметры: AddressFamily.InterNetwork, SocketType.Stream и ProtocolType.Tcp. Для Udp набор параметров будет другим: AddressFamily.InterNetwork, SocketType.Dgram и ProtocolType.Udp. Для других протоколов набор значений будет отличаться. Поэтому использование сокетов может потребовать некоторого знания принципов работы отдельных протоколов. Хотя в отношении Tcp и Udp все относительно просто.
Общий принцип работы сокетов
При работе с сокетами вне зависимости от выбранных протоколов мы будем опираться на методы класса Socket:
Accept() : создает новый объект Socket для обработки входящего подключения
Bind() : связывает объект Socket с локальной конечной точкой
Close() : закрывает сокет
Connect() : устанавливает соединение с удаленным хостом
Listen() : начинает прослушивание входящих запросов
Poll() : определяет состояние сокета
Receive() : получает данные
Send() : отправляет данные
Shutdown() : блокирует на сокете прием и отправку данных
В зависимости от применяемого протокола (TCP, UDP и т.д.) общий принцип работы с сокетами будет немного различаться.
При применении протокола, который требует установление соединения, например, TCP, сервер должен вызвать метод Bind для установки точки для прослушивания входящих подключений и затем запустить прослушивание подключений с помощью метода Listen. Далее с помощью метода Accept можно получить входящие запросы на подключение в виде объекта Socket, который используется для взаимодействия с удаленным узла. У полученного объекта Socket вызываются методы Send и Receive соответственно для отправки и получения данных. Если необходимо подключиться к серверу, то вызывается метод Connect. Для обмена данными с сервером также применяются методы Send или Receive.
Если применяется протокол, для которого не требуется установление соединения, например, UDP, то после вызова метода Bind не надо вызывать метод Listen. И в этом случае для приема данных используется метод ReceiveFrom, а для отправки данных - метод SendTo.
— это один конец двустороннего канала связи между двумя программами, работающими в сети. Соединяя вместе два сокета, можно передавать данные между разными процессами (локальными или удаленными). Реализация сокетов обеспечивает инкапсуляцию протоколов сетевого и транспортного уровней.
Первоначально сокеты были разработаны для UNIX в Калифорнийском университете в Беркли. В UNIX обеспечивающий связь метод ввода-вывода следует алгоритму open/read/write/close. Прежде чем ресурс использовать, его нужно открыть, задав соответствующие разрешения и другие параметры. Как только ресурс открыт, из него можно считывать или в него записывать данные. После использования ресурса пользователь должен вызывать метод Close(), чтобы подать сигнал операционной системе о завершении его работы с этим ресурсом.
Интерфейс IPC для взаимодействия между разными процессами построен поверх методов ввода-вывода. Они облегчают для сокетов отправку и получение данных. Каждый целевой объект задается адресом сокета, следовательно, этот адрес можно указать в клиенте, чтобы установить соединение с целью.
Типы сокетов
Существуют два основных типа сокетов — потоковые сокеты и дейтаграммные.
Потоковые сокеты (stream socket)
Потоковый сокет — это сокет с установленным соединением, состоящий из потока байтов, который может быть двунаправленным, т, е. через эту конечную точку приложение может и передавать, и получать данные.
Потоки базируются на явных соединениях: сокет А запрашивает соединение с сокетом В, а сокет В либо соглашается с запросом на установление соединения, либо отвергает его.
Если данные должны гарантированно доставляться другой стороне или размер их велик, потоковые сокеты предпочтительнее дейтаграммных. Следовательно, если надежность связи между двумя приложениями имеет первостепенное значение, выбирайте потоковые сокеты.
Дейтаграммные сокеты (datagram socket)
Потоковые сокеты по сравнению с дейтаграммными действительно дают более надежный метод, но для некоторых приложений накладные расходы, связанные с установкой явного соединения, неприемлемы (например, сервер времени суток, обеспечивающий синхронизацию времени для своих клиентов). В конце концов на установление надежного соединения с сервером требуется время, которое просто вносит задержки в обслуживание, и задача серверного приложения не выполняется. Для сокращения накладных расходов нужно использовать дейтаграммные сокеты.
Кроме двух рассмотренных типов существует также обобщенная форма сокетов, которую называют необрабатываемыми или сырыми.
Сырые сокеты (raw socket)
Главная цель использования сырых сокетов состоит в обходе механизма, с помощью которого компьютер обрабатывает TCP/IP. Это достигается обеспечением специальной реализации стека TCP/IP, замещающей механизм, предоставленный стеком TCP/IP в ядре — пакет непосредственно передается приложению и, следовательно, обрабатывается гораздо эффективнее, чем при проходе через главный стек протоколов клиента.
По определению, — это сокет, который принимает пакеты, обходит уровни TCP и UDP в стеке TCP/IP и отправляет их непосредственно приложению.
При использовании таких сокетов пакет не проходит через фильтр TCP/IP, т.е. никак не обрабатывается, и предстает в своей сырой форме. В таком случае обязанность правильно обработать все данные и выполнить такие действия, как удаление заголовков и разбор полей, ложится на получающее приложение — все равно, что включить в приложение небольшой стек TCP/IP.
Однако нечасто может потребоваться программа, работающая с сырыми сокетами. Если вы не пишете системное программное обеспечение или программу, аналогичную анализатору пакетов, вникать в такие детали не придется. Сырые сокеты главным образом используются при разработке специализированных низкоуровневых протокольных приложений. Например, такие разнообразные утилиты TCP/IP, как trace route, ping или arp, используют сырые сокеты.
Работа с сырыми сокетами требует солидного знания базовых протоколов TCP/UDP/IP.
Порты
Порт определен, чтобы разрешить задачу одновременного взаимодействия с несколькими приложениями. По существу с его помощью расширяется понятие IP-адреса. Компьютер, на котором в одно время выполняется несколько приложений, получая пакет из сети, может идентифицировать целевой процесс, пользуясь уникальным номером порта, определенным при установлении соединения.
Сокет состоит из IP-адреса машины и номера порта, используемого приложением TCP. Поскольку IP-адрес уникален в Интернете, а номера портов уникальны на отдельной машине, номера сокетов также уникальны во всем Интернете. Эта характеристика позволяет процессу общаться через сеть с другим процессом исключительно на основании номера сокета.
За определенными службами номера портов зарезервированы — это широко известные номера портов, например порт 21, использующийся в FTP. Ваше приложение может пользоваться любым номером порта, который не был зарезервирован и пока не занят. Агентство Internet Assigned Numbers Authority (IANA) ведет перечень широко известных номеров портов.
Обычно приложение клиент-сервер, использующее сокеты, состоит из двух разных приложений - клиента, инициирующего соединение с целью (сервером), и сервера, ожидающего соединения от клиента.
Например, на стороне клиента, приложение должно знать адрес цели и номер порта. Отправляя запрос на соединение, клиент пытается установить соединение с сервером:
Если события развиваются удачно, при условии что сервер запущен прежде, чем клиент попытался с ним соединиться, сервер соглашается на соединение. Дав согласие, серверное приложение создает новый сокет для взаимодействия именно с установившим соединение клиентом:
Класс Socket
Класс Socket играет важную роль в сетевом программировании, обеспечивая функционирование как клиента, так и сервера. Главным образом, вызовы методов этого класса выполняют необходимые проверки, связанные с безопасностью, в том числе проверяют разрешения системы безопасности, после чего они переправляются к аналогам этих методов в Windows Sockets API.
Прежде чем обращаться к примеру использования класса Socket, рассмотрим некоторые важные свойства и методы этого класса:
Читайте также: