Какая библиотека используется для работы с сокетами
Перейти к содержимому

Какая библиотека используется для работы с сокетами

  • автор:

Низкоуровневый сетевой интерфейс в Python

Модуль socket обеспечивает доступ к интерфейсу сокета BSD. Он доступен во всех современных системах Unix, Windows, MacOS и, возможно, на дополнительных платформах.

Он включает в себя функции создания объекта сокета Socket , который и обрабатывает канал данных, а также функции, связанных с сетевыми задачами, такими как преобразование имени сервера в IP адрес и форматирование данных для отправки по сети.

Примечание. Поведение модуля может зависеть от платформы, поскольку выполняются вызовы API сокетов операционной системы.

Интерфейс Python представляет собой прямую трансляцию системного вызова Unix и интерфейса библиотеки для сокетов в объектно-ориентированный стиль Python. Функция socket.socket() возвращает объект Socket , методы которого реализуют различные системные вызовы сокетов.

Типы параметров функций модуля несколько более высокоуровневые, чем в интерфейсе языка C: как и в случае операций чтения/записи с файлами, распределение буфера при операциях приема данных происходит автоматически, а длина буфера неявно определяется операциями отправки.

Сокеты можно настроить для работы в качестве сервера и прослушивания входящих сообщений или для подключения к другим приложениям в качестве клиента. После подключения обоих концов сокета TCP/IP обмен данными становится двунаправленным.

Пример создания и использования сокетов на примере TCP/IP сервер и клиента.

Этот пример, основанный на стандартной документации библиотеки, принимает входящие сообщения и передает их обратно отправителю. Он начинается с создания сокета TCP/IP, а затем метод sock.bind() используется для связывания сокета с адресом сервера.

Примечание. Для успешного тестирования примера, код клиента и сервера необходимо запускать в разных окнах терминала. Код сервера запускается первым.

TCP/IP сервер.
# test-server.py import socket import sys # создаемTCP/IP сокет sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Привязываем сокет к порту server_address = ('localhost', 10000) print('Старт сервера на <> порт <>'.format(*server_address)) sock.bind(server_address) # Слушаем входящие подключения sock.listen(1) while True: # ждем соединения print('Ожидание соединения. ') connection, client_address = sock.accept() try: print('Подключено к:', client_address) # Принимаем данные порциями и ретранслируем их while True: data = connection.recv(16) print(f'Получено: data.decode()>') if data: print('Обработка данных. ') data = data.upper() print('Отправка обратно клиенту.') connection.sendall(data) else: print('Нет данных от:', client_address) break finally: # Очищаем соединение connection.close() 

Вызов метода sock.listen(1) переводит сокет в режим сервера, а метод sock.accept() ожидает входящего соединения. Целочисленный аргумент у метода .listen — это количество соединений, которые система должна поставить в очередь в фоновом режиме, прежде чем отклонять новых клиентов. В этом примере предполагается, что одновременно будет работать только одно соединение.

Метод sock.accept() возвращает открытое соединение между сервером и клиентом вместе с адресом клиента. На самом деле соединение представляет собой другой сокет на другом порту (назначенный ядром). Данные считываются из соединения с помощью метод sock.recv() и передаются с помощью sock.sendall() .

Когда общение с клиентом завершено, соединение необходимо очистить с помощью sock.close() . В этом примере используется блок try/finally , чтобы гарантировать, что метод sock.close() всегда вызывается, даже в случае ошибки.

Клиентская программа настраивает свой сокет иначе, чем сервер. Вместо привязки к порту и прослушивания он использует метод sock.connect() для подключения сокета непосредственно к удаленному адресу.

TCP/IP клиент.
# test-client.py import socket import sys # СоздаемTCP/IP сокет sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Подключаем сокет к порту, через который прослушивается сервер server_address = ('localhost', 10000) print('Подключено к <> порт <>'.format(*server_address)) sock.connect(server_address) try: # Отправка данных mess = 'Hello Wоrld!' print(f'Отправка: mess>') message = mess.encode() sock.sendall(message) # Смотрим ответ amount_received = 0 amount_expected = len(message) while amount_received  amount_expected: data = sock.recv(16) amount_received += len(data) mess = data.decode() print(f'Получено: data.decode()>') finally: print('Закрываем сокет') sock.close() 

После установления соединения данные могут быть отправлены через сокет с помощью метода sock.sendall() и получены с помощью sock.recv() , как и на сервере. Когда все сообщения отправлены, а копия получена, то сокет закрывается, чтобы освободить порт.

Работа клиента и сервера вместе.

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

Старт сервера на localhost порт 10000 Ожидание соединения. Подключено к: ('127.0.0.1', 34800) Получено: Hello Wоrld! Обработка данных. Отправка обратно клиенту. Получено: Нет данных от: ('127.0.0.1', 34800) Ожидание соединения. .

Выходные данные клиента показывают исходящее сообщение и ответ сервера.

Подключено к localhost порт 10000 Отправка: Hello Wоrld! Получено: HELLO WоRLD! Закрываем сокет
  • КРАТКИЙ ОБЗОР МАТЕРИАЛА.
  • Советы по программированию сокетов
  • Константы, определяемые модулем socket
  • Семейства сокетов, поддерживаемых модулем socket
  • Функция socket() модуля socket, создает новый сокет
  • Функция create_connection() модуля socket
  • Функция create_server() модуля socket
  • Функция socketpair() модуля socket
  • Функция fromfd() модуля socket
  • Функция fromshare() модуля socket
  • Объект Socket модуля socket
  • Функция close() модуля socket
  • Функция getaddrinfo() модуля socket
  • Функция getfqdn() модуля socket
  • Функция gethostbyname() модуля socket
  • Функция gethostbyname_ex() модуля socket
  • Функция gethostname() модуля socket
  • Функция gethostbyaddr() модуля socket
  • Функция getnameinfo() модуля socket
  • Функция getprotobyname() модуля socket
  • Функция getservbyname() модуля socket
  • Функция getservbyport() модуля socket
  • Функция has_dualstack_ipv6() модуля socket
  • Функции getdefaulttimeout() и setdefaulttimeout() модуля socket
  • Функции CMSG_LEN и CMSG_SPACE модуля socket
  • Функция sethostname() модуля socket
  • Функция if_nameindex() модуля socket
  • Функции if_nametoindex() и if_indextoname модуля socket
  • Функции различных преобразований модуля socket
  • Ошибки и исключения, определяемые модулем socket

Сокеты в Python для начинающих

В далеком для меня 2010 году я писал статью для начинающих про сокеты в Python. Сейчас этот блог канул в небытие, но статья мне показалась довольно полезной. Статью нашел на флешке в либровском документе, так что это не кросспост, не копипаст — в интернете ее нигде нет.

Что это

Для начала нужно разобраться что такое вообще сокеты и зачем они нам нужны. Как говорит вики, сокет — это программный интерфейс для обеспечения информационного обмена между процессами. Но гораздо важнее не зазубрить определение, а понять суть. Поэтому я тут постараюсь рассказать все как можно подробнее и проще.

Существуют клиентские и серверные сокеты. Вполне легко догадаться что к чему. Серверный сокет прослушивает определенный порт, а клиентский подключается к серверу. После того, как было установлено соединение начинается обмен данными.

Рассмотрим это на простом примере. Представим себе большой зал с множеством небольших окошек, за которыми стоят девушки. Есть и пустые окна, за которыми никого нет. Те самые окна — это порты. Там, где стоит девушка — это открытый порт, за которым стоит какое-то приложение, которое его прослушивает. То есть, если, вы подойдете к окошку с номером 9090, то вас поприветствуют и спросят, чем могут помочь. Так же и с сокетами. Создается приложение, которое прослушивает свой порт. Когда клиент устанавливает соединение с сервером на этом порту именно данное приложение будет ответственно за работу этим клиентом. Вы же не подойдете к одному окошку, а кричать вам будут из соседнего 🙂

После успешной установки соединения сервер и клиент начинают обмениваться информацией. Например, сервер посылает приветствие и предложение ввести какую-либо команду. Клиент в свою очередь вводит команду, сервер ее анализирует, выполняет необходимые операции и отдает клиенту результат.

Сервер

Сейчас создайте два файла — один для сервера, а другой для клиента.

В Python для работы с сокетами используется модуль socket:

import socket 

Прежде всего нам необходимо создать сокет:

sock = socket.socket() 

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

Теперь нам нужно определиться с хостом и портом для нашего сервера. Насчет хоста — мы оставим строку пустой, чтобы наш сервер был доступен для всех интерфейсов. А порт возьмем любой от нуля до 65535. Следует отметить, что в большинстве операционных систем прослушивание портов с номерами 0 — 1023 требует особых привилегий. Я выбрал порт 9090. Теперь свяжем наш сокет с данными хостом и портом с помощью метода bind, которому передается кортеж, первый элемент (или нулевой, если считать от нуля) которого — хост, а второй — порт:

sock.bind(('', 9090)) 

Теперь у нас все готово, чтобы принимать соединения. С помощью метода listen мы запустим для данного сокета режим прослушивания. Метод принимает один аргумент — максимальное количество подключений в очереди. Напряжем нашу бурную фантазию и вспомним про зал с окошками. Так вот этот параметр определяет размер очереди. Если он установлен в единицу, а кто-то, явно лишний, пытается еще подстроится сзади, то его пошлют 🙂 Установим его в единицу:

sock.listen(1) 

Ну вот, наконец-то, мы можем принять подключение с помощью метода accept, который возвращает кортеж с двумя элементами: новый сокет и адрес клиента. Именно этот сокет и будет использоваться для приема и посылке клиенту данных.

conn, addr = sock.accept() 

Вот и все. Теперь мы установили с клиентом связь и можем с ним «общаться». Т.к. мы не можем точно знать, что и в каких объемах клиент нам пошлет, то мы будем получать данные от него небольшими порциями. Чтобы получить данные нужно воспользоваться методом recv, который в качестве аргумента принимает количество байт для чтения. Мы будем читать порциями по 1024 байт (или 1 кб):

while True: data = conn.recv(1024) if not data: break conn.send(data.upper()) 

Как мы и говорили для общения с клиентом мы используем сокет, который получили в результате выполнения метода accept. Мы в бесконечном цикле принимаем 1024 байт данных с помощью метода recv. Если данных больше нет, то этот метод ничего не возвращает. Таким образом мы можем получать от клиента любое количество данных.

Дальше в нашем примере для наглядности мы что-то сделаем с полученными данными и отправим их обратно клиенту. Например, с помощью метода upper у строк вернем клиенту строку в верхнем регистре.

Теперь можно и закрыть соединение:

conn.close() 

Собственно сервер готов. Он принимает соединение, принимает от клиента данные, возвращает их в виде строки в верхнем регистре и закрывает соединение. Все просто 🙂 В итоге у вас должно было получиться следующее:

#!/usr/bin/env python # -*- coding: utf-8 -*- import socket sock = socket.socket() sock.bind(('', 9090)) sock.listen(1) conn, addr = sock.accept() print 'connected:', addr while True: data = conn.recv(1024) if not data: break conn.send(data.upper()) conn.close() 
Клиент

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

#!/usr/bin/env python # -*- coding: utf-8 -*- import socket sock = socket.socket() sock.connect(('localhost', 9090)) sock.send('hello, world!') data = sock.recv(1024) sock.close() print data 

Думаю, что все понятно, т.к. все уже разбиралось ранее. Единственное новое здесь — это метод connect, с помощью которого мы подключаемся к серверу. Дальше мы читаем 1024 байт данных и закрываем сокет.

  • Python
  • Программирование

Программирование сокетов в Linux

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

Библиотека функций работы с сокетами — Socket API (Application Programming Interface) — является основным инструментом программиста при создании сетевых приложений. Socket API был впервые реализован в операционной системе Berkley UNIX. Сейчас этот программный интерфейс доступен практически в любой модификации Unix, в том числе в Linux. Хотя все реализации чем-то отличаются друг от друга, основной набор функций в них совпадает. Изначально сокеты использовались в программах на C/C++, но в настоящее время средства для работы с ними предоставляют многие языки (Perl, Java и др.).

Сокеты предоставляют весьма мощный и гибкий механизм межпроцессного взаимодействия (IPC, InterProcess Communication). Они могут использоваться для организации взаимодействия программ на одном компьютере, по локальной сети или через Internet, что позволяет создавать распределённые приложения различной сложности. Кроме того, с их помощью можно организовать взаимодействие с программами, работающими под управлением других операционных систем. Например, под Windows существует интерфейс Window Sockets, спроектированный на основе socket API.

Сокеты поддерживают многие стандартные сетевые протоколы (конкретный их список зависит от реализации) и предоставляют унифицированный интерфейс для работы с ними. Наиболее часто сокеты используются для работы в IP-сетях. В этом случае их можно использовать для взаимодействия приложений не только по специально разработанным, но и по стандартным протоколам — HTTP, FTP, Telnet и т. д. Например, имеется возможность написать собственный Web-браузер или Web-сервер, способный обслуживать одновременно множество клиентов.

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

Основы socket API

Понятие сокета

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

В программе сокет идентифицируется дескриптором — это просто переменная типа int. Программа получает дескриптор от операционной системы при создании сокета, а затем передаёт его сервисам socket API для указания сокета, над которым необходимо выполнить то или иное действие.

Подобно системному вызову open(), создающему дескриптор для доступа к файлам и системным устройствам, функция socket() создает дескриптор, позволяющий обращаться к компьютерам по сети.

Атрибуты сокета

С каждым сокетом связываются три атрибута: домен (domain), тип (type) и протокол (protocol). Эти атрибуты задаются при создании сокета и остаются неизменными на протяжении всего времени его существования. Для создания сокета используется функция socket, имеющая следующий прототип.

Домен определяет пространство адресов, в котором располагается сокет, и множество протоколов, которые используются для передачи данных. Чаще других используются домены Unix и Internet, задаваемые константами AF_UNIX и AF_INET соответственно (префикс AF означает «address family» — «семейство адресов»). При задании AF_UNIX для передачи данных используется файловая система ввода/вывода Unix. В этом случае сокеты используются для межпроцессного взаимодействия на одном компьютере и не годятся для работы по сети. Константа AF_INET соответствует Internet-домену. Сокеты, размещённые в этом домене, могут использоваться для работы в любой IP-сети. Существуют и другие домены ( AF_IPX для протоколов Novell, AF_INET6 для новой модификации протокола IP — IPv6 и т. д.).

Иногда вместо префикса AF_ используется префикс PF_ («protocol family»).

Тип сокета определяет способ передачи данных по сети. Чаще других применяются:

  • SOCK_STREAM . Передача потока данных с предварительной установкой соединения. Обеспечивается надёжный канал передачи данных, при котором фрагменты отправленного блока не теряются, не переупорядочиваются и не дублируются. Поскольку этот тип сокетов является самым распространённым, до конца раздела мы будем говорить только о нём. Остальным типам будут посвящены отдельные разделы.
  • SOCK_DGRAM . Передача данных в виде отдельных сообщений (датаграмм). Предварительная установка соединения не требуется. Обмен данными происходит быстрее, но является ненадёжным: сообщения могут теряться в пути, дублироваться и переупорядочиваться. Допускается передача сообщения нескольким получателям (multicasting) и широковещательная передача (broadcasting).
  • SOCK_RAW . Этот тип присваивается низкоуровневым (т. н. «сырым») сокетам. Их отличие от обычных сокетов состоит в том, что с их помощью программа может взять на себя формирование некоторых заголовков, добавляемых к сообщению.

Обратите внимание, что не все домены допускают задание произвольного типа сокета. Например, совместно с доменом Unix используется только тип SOCK_STREAM . С другой стороны, для Internet-домена можно задавать любой из перечисленных типов. В этом случае для реализации SOCK_STREAM используется протокол TCP, для реализации SOCK_DGRAM — протокол UDP, а тип SOCK_RAW используется для низкоуровневой работы с протоколами IP, ICMP и т. д.

Наконец, последний атрибут определяет протокол, используемый для передачи данных. Как мы только что видели, часто протокол однозначно определяется по домену и типу сокета. В этом случае в качестве третьего параметра функции socket можно передать 0, что соответствует протоколу по умолчанию. Тем не менее, иногда (например, при работе с низкоуровневыми сокетами) требуется задать протокол явно. Числовые идентификаторы протоколов зависят от выбранного домена; их можно найти в документации.

Адреса

Прежде чем передавать данные через сокет, его необходимо связать с адресом в выбранном домене (эту процедуру называют именованием сокета). Иногда связывание осуществляется неявно (внутри функций connect и accept ), но выполнять его необходимо во всех случаях. Вид адреса зависит от выбранного вами домена. В Unix-домене это текстовая строка — имя файла, через который происходит обмен данными. В Internet-домене адрес задаётся комбинацией IP-адреса и 16-битного номера порта. IP-адрес определяет хост в сети, а порт — конкретный сокет на этом хосте. Протоколы TCP и UDP используют различные пространства портов.

Для явного связывания сокета с некоторым адресом используется функция bind . Её прототип имеет вид:

В качестве первого параметра передаётся дескриптор сокета, который мы хотим привязать к заданному адресу. Второй параметр, addr , содержит указатель на структуру с адресом, а третий — длину этой структуры. Посмотрим, что она собой представляет.

Поле sa_family содержит идентификатор домена, тот же, что и первый параметр функции socket . В зависимости от значения этого поля по-разному интерпретируется содержимое массива sa_data . Разумеется, работать с этим массивом напрямую не очень удобно, поэтому вы можете использовать вместо sockaddr одну из альтернативных структур вида sockaddr_XX (XX — суффикс, обозначающий домен: «un» — Unix, «in» — Internet и т. д.). При передаче в функцию bind указатель на эту структуру приводится к указателю на sockaddr . Рассмотрим для примера структуру sockaddr_in .

Здесь поле sin_family соответствует полю sa_family в sockaddr , в sin_port записывается номер порта, а в sin_addr — IP-адрес хоста. Поле sin_addr само является структурой, которая имеет вид:

Зачем понадобилось заключать всего одно поле в структуру? Дело в том, что раньше in_addr представляла собой объединение (union), содержащее гораздо большее число полей. Сейчас, когда в ней осталось всего одно поле, она продолжает использоваться для обратной совместимости.

И ещё одно важное замечание. Существует два порядка хранения байтов в слове и двойном слове. Один из них называется порядком хоста (host byte order), другой — сетевым порядком (network byte order) хранения байтов. При указании IP-адреса и номера порта необходимо преобразовать число из порядка хоста в сетевой. Для этого используются функции htons (Host TO Network Short) и htonl (Host TO Network Long). Обратное преобразование выполняют функции ntohs и ntohl .

Установка соединения (сервер)

Установка соединения на стороне сервера состоит из четырёх этапов, ни один из которых не может быть опущен. Сначала сокет создаётся и привязывается к локальному адресу. Если компьютер имеет несколько сетевых интерфейсов с различными IP-адресами, вы можете принимать соединения только с одного из них, передав его адрес функции bind . Если же вы готовы соединяться с клиентами через любой интерфейс, задайте в качестве адреса константу INADDR_ANY . Что касается номера порта, вы можете задать конкретный номер или 0 (в этом случае система сама выберет произвольный неиспользуемый в данный момент номер порта).

На следующем шаге создаётся очередь запросов на соединение. При этом сокет переводится в режим ожидания запросов со стороны клиентов. Всё это выполняет функция listen .

Первый параметр — дескриптор сокета, а второй задаёт размер очереди запросов. Каждый раз, когда очередной клиент пытается соединиться с сервером, его запрос ставится в очередь, так как сервер может быть занят обработкой других запросов. Если очередь заполнена, все последующие запросы будут игнорироваться. Когда сервер готов обслужить очередной запрос, он использует функцию accept .

Функция accept создаёт для общения с клиентом новый сокет и возвращает его дескриптор. Параметр sockfd задаёт слушающий сокет. После вызова он остаётся в слушающем состоянии и может принимать другие соединения. В структуру, на которую ссылается addr , записывается адрес сокета клиента, который установил соединение с сервером. В переменную, адресуемую указателем addrlen , изначально записывается размер структуры; функция accept записывает туда длину, которая реально была использована. Если вас не интересует адрес клиента, вы можете просто передать NULL в качестве второго и третьего параметров.

Обратите внимание, что полученный от accept новый сокет связан с тем же самым адресом, что и слушающий сокет. Сначала это может показаться странным. Но дело в том, что адрес TCP-сокета не обязан быть уникальным в Internet-домене. Уникальными должны быть только соединения , для идентификации которых используются два адреса сокетов, между которыми происходит обмен данными.

Установка соединения (клиент)

На стороне клиента для установления соединения используется функция connect , которая имеет следующий прототип.

Здесь sockfd — сокет, который будет использоваться для обмена данными с сервером, serv_addr содержит указатель на структуру с адресом сервера, а addrlen — длину этой структуры. Обычно сокет не требуется предварительно привязывать к локальному адресу, так как функция connect сделает это за вас, подобрав подходящий свободный порт. Вы можете принудительно назначить клиентскому сокету некоторый номер порта, используя bind перед вызовом connect . Делать это следует в случае, когда сервер соединяется с только с клиентами, использующими определённый порт (примерами таких серверов являются rlogind и rshd). В остальных случаях проще и надёжнее предоставить системе выбрать порт за вас.

Обмен данными

После того как соединение установлено, можно начинать обмен данными. Для этого используются функции send и recv . В Unix для работы с сокетами можно использовать также файловые функции read и write , но они обладают меньшими возможностями, а кроме того не будут работать на других платформах (например, под Windows), поэтому пользоваться ими не рекомендуется.

Функция send используется для отправки данных и имеет следующий прототип.

Здесь sockfd — это, как всегда, дескриптор сокета, через который мы отправляем данные, msg — указатель на буфер с данными, len — длина буфера в байтах, а flags — набор битовых флагов, управляющих работой функции (если флаги не используются, передайте функции 0). Вот некоторые из них (полный список можно найти в документации):

  • MSG_OOB . Предписывает отправить данные как срочные (out of band data, OOB). Концепция срочных данных позволяет иметь два параллельных канала данных в одном соединении. Иногда это бывает удобно. Например, Telnet использует срочные данные для передачи команд типа Ctrl+C. В настоящее время использовать их не рекомендуется из-за проблем с совместимостью (существует два разных стандарта их использования, описанные в RFC793 и RFC1122). Безопаснее просто создать для срочных данных отдельное соединение.
  • MSG_DONTROUTE . Запрещает маршрутизацию пакетов. Нижележащие транспортные слои могут проигнорировать этот флаг.

Функция send возвращает число байтов, которое на самом деле было отправлено (или -1 в случае ошибки). Это число может быть меньше указанного размера буфера. Если вы хотите отправить весь буфер целиком, вам придётся написать свою функцию и вызывать в ней send , пока все данные не будут отправлены. Она может выглядеть примерно так.

total += n; >

Использование sendall ничем не отличается от использования send , но она отправляет весь буфер с данными целиком.

Для чтения данных из сокета используется функция recv .

В целом её использование аналогично send . Она точно так же принимает дескриптор сокета, указатель на буфер и набор флагов. Флаг MSG_OOB используется для приёма срочных данных, а MSG_PEEK позволяет «подсмотреть» данные, полученные от удалённого хоста, не удаляя их из системного буфера (это означает, что при следующем обращении к recv вы получите те же самые данные). Полный список флагов можно найти в документации. По аналогии с send функция recv возвращает количество прочитанных байтов, которое может быть меньше размера буфера. Вы без труда сможете написать собственную функцию recvall , заполняющую буфер целиком. Существует ещё один особый случай, при котором recv возвращает 0. Это означает, что соединение было разорвано.

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

Закончив обмен данными, закройте сокет с помощью функции close . Это приведёт к разрыву соединения.

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

Параметр how может принимать одно из следующих значений:

  • 0 — запретить чтение из сокета
  • 1 — запретить запись в сокет
  • 2 — запретить и то и другое

Хотя после вызова shutdown с параметром how , равным 2, вы больше не сможете использовать сокет для обмена данными, вам всё равно потребуется вызвать close , чтобы освободить связанные с ним системные ресурсы.

Обработка ошибок

В процессе работы с сокетами могут происходить ошибки. Все рассмотренные функции при возникновении ошибкивозвращают -1, записывая в глобальную переменную errno код ошибки. Соответственно, имеется возможность проанализировать значение этой переменной и предпринять действия по восстановлению нормальной работы программы, не прерывая её выполнения. А можно просто выдать диагностическое сообщение (для этого удобно использовать функцию perror ), а затем завершить программу с помощью exit .

Отладка программ

На начальном этапе работы с сетевыми приложениями достаточно запустить клиента и сервера на одной машине, используя для соединения адрес интерфейса внутренней петли (loopback interface). В программе ему соответствует константа INADDR_LOOPBACK (не забудьте применять к ней функцию htonl !). Пакеты, направляемые по этому адресу, в сеть не попадают. Вместо этого они передаются стеку протоколов TCP/IP как только что принятые. Таким образом моделируется наличие виртуальной сети, в которой вы можете отлаживать ваши сетевые приложения.

Разбираемся с сокетами на примере стандартной библиотеки Go

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

Модель TCP/IP

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

tcp/ip

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

Транспортный уровень

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

Чтобы программисту не нужно было контролировать доставку пакетов, их целостность и порядок, эти задачи возложены на транспортный уровень стека сетевых протоколов. Есть два наиболее популярных протокола на этом уровне.

Протокол UDP (User Datagram Protocol) противоречит той рекламе, которую я только что дал транспортному уровню. Он является тонкой оберткой над протоколом IP и не обеспечивает никакого контроля над доставкой. Программист берет данные для отправки, упаковывает их в UDP-пакет, размер которого выбирает самостоятельно, и отправляет в сеть. Если пакет не достигнет цели, ни отправитель, ни получатель об этом не узнают. Этот протокол часто применяется в задачах, когда важна скорость доставки, но не столь важно, чтобы все пакеты были доставлены. Например, в онлайн-играх или видео-конференциях.

Протокол, который способен проконтролировать доставку вашего пакета, называется TCP (Transmission Control Protocol). В отличие от UDP, перед передачей данных он формирует виртуальное соединение. Для этого используются пакеты специального назначения, которыми обмениваются инициатор соединения (будем считать, что это клиент) и сервер. Такая процедура называется трехфазным рукопожатием (3-way handshake).

Другим отличием от UDP является то, что программист при работе с TCP не заботится о размере данных, которые будет отправлять. Этот протокол опрерирует понятием потока и самостоятельно нарезает этот поток на пакеты того размера, который посчитает нужным. Главная же особенность протокола TCP в контроле доставки данных. Он будет отправлять нужные пакеты снова, пока получатель не подтвердит (тоже средствами TCP), что они доставлены и прошли контроль целостности.

Сокеты

Транспортный уровень обеспечивает доставку пакета до нужного компьютера, но операционной системе нужно понять, какому приложению предназначается этот пакет. Для этой цели в ОС используются сетевые порты. Порт — это обычное 16-битное число, которое соответствует процессу и сетевому соединению, которое он использует. Извлекая порт получателя из заголовка TCP-пакета, операционная система определяет, какому процессу нужно передать данные.

Таким образом, чтобы доставить данные из процесса на одном устройстве в процесс на другом, необходимо знать IP-адрес получателя и порт, используемый этим процессом. Пара этих параметров образует понятие сокета. В то же время, сокетом называют ресурс операционной системы, который она выделяет, когда процесс запрашивает порт для сетевого взаимодействия. По факту, это два взгляда на одно и то же. Рассмотрим стандартную процедуру, когда клиентское приложение хочет связаться с сервером.

Допустим, клиент каким-то образом знает, какой порт нужно указать в качестве получателя в пакете. Чтобы принимать пакеты, сервер должен быть готов к этому. Это возможно, засчет открытых портов. Они постоянно “слушают” входящие соединения, которых может быть больше одного, в отличие от обычных (закрытых) портов. Как правило, службы, к которым хотят получить доступ процессы на других устройствах, имеют общеизвестные открытые порты, что решает проблему узнавания, к какому порту обращаться клиенту. Клиентские же процессы обычно могут брать произвольный незанятый порт для взаимодействия по сети.

Далее мы реализуем клиент-серверное взаимодействие с помощью сокетов на языке Go. Как уже было сказано, работа с сокетами представлена очень похожим API в разных языках программирования. Поэтому мы постараемся выделить основные моменты, не заостряясь на языковых особенностях.

Начнем с более популярного протокола TCP. Наша задача — реализовать простейший, не очень общительный сервер, который здоровается с клиентом, узнает имя собеседника и прощается с ним. Классическая реализация TCP-сервера:

  1. Создать слушающий сокет с открытым портом.
  2. В бесконечном цикле принимать соединения от клиентов.
  3. При соединении с клиентом создается новый сокет для взаимодействия только с этим клиентом.
  4. Для каждого соединения обрабатывать клиентские запросы в отдельном потоке.

client tcp

client udp

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

Written on February 17th, 2019 by Alexey Kalina

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *