Как подключиться к сокету в другой сети
Сегодня мы рассмотрим пример программирования сокетов Python. Мы создадим серверные и клиентские приложения на Python.
Программирование сокетов
Чтобы понять программирование сокетов Python, нам нужно знать о трех интересных темах – Socket Server, Socket Client и Socket.
Итак, что такое сервер? Сервер – это программное обеспечение, которое ожидает запросов клиентов и обслуживает или обрабатывает их соответственно.
С другой стороны, клиент запрашивает эту услугу. Клиентская программа запрашивает некоторые ресурсы к серверу, и сервер отвечает на этот запрос.
Socket – это конечная точка двунаправленного канала связи между сервером и клиентом. Сокеты могут обмениваться данными внутри процесса, между процессами на одной машине или между процессами на разных машинах. Для любого взаимодействия с удаленной программой мы должны подключаться через порт сокета.
Основная цель этого руководства по программированию сокетов – познакомить вас с тем, как сервер сокетов и клиент взаимодействуют друг с другом. Вы также узнаете, как написать программу сервера сокетов в Python.
Пример
Ранее мы говорили, что клиент сокета запрашивает некоторые ресурсы у сервера, и сервер отвечает на этот запрос.
Итак, мы разработаем и серверную, и клиентскую модель, чтобы каждый мог общаться с ними. Шаги можно рассматривать так:
Сервер сокетов
Мы сохраним программу сервера сокетов, как socket_server.py. Чтобы использовать соединение, нам нужно импортировать модуль сокета.
Затем последовательно нам нужно выполнить некоторую задачу, чтобы установить соединение между сервером и клиентом.
Мы можем получить адрес хоста с помощью функции socket.gethostname(). Рекомендуется использовать адрес порта пользователя выше 1024, поскольку номер порта меньше 1024 зарезервирован для стандартного интернет-протокола.
Смотрите приведенный ниже пример кода сервера:
Итак, наш сервер сокетов работает на порту 5000 и будет ждать запроса клиента. Если вы хотите, чтобы сервер не завершал работу при закрытии клиентского соединения, просто удалите условие if и оператор break. Цикл while используется для бесконечного запуска серверной программы и ожидания клиентского запроса.
Клиент сокета
Мы сохраним клиентскую программу сокета python как socket_client.py. Эта программа похожа на серверную, за исключением привязки.
Основное различие между серверной и клиентской программой состоит в том, что в серверной программе необходимо связать адрес хоста и адрес порта вместе.
Смотрите ниже пример кода клиента сокета:
Вывод
Чтобы увидеть результат, сначала запустите программу сервера сокетов. Затем запустите клиентскую программу. После этого напишите что-нибудь из клиентской программы. Затем снова напишите ответ от серверной программы.
Наконец, напишите «до свидания» из клиентской программы, чтобы завершить обе программы. Ниже короткое видео покажет, как это работало на моем тестовом прогоне примеров программ сервера сокетов и клиента.
Обратите внимание, что сервер сокетов работает на порту 5000, но клиенту также требуется порт сокета для подключения к серверу. Этот порт назначается случайным образом при вызове клиентского соединения. В данном случае это 57822.
В далеком для меня 2010 году я писал статью для начинающих про сокеты в Python. Сейчас этот блог канул в небытие, но статья мне показалась довольно полезной. Статью нашел на флешке в либровском документе, так что это не кросспост, не копипаст — в интернете ее нигде нет.
Что это
Для начала нужно разобраться что такое вообще сокеты и зачем они нам нужны. Как говорит вики, сокет — это программный интерфейс для обеспечения информационного обмена между процессами. Но гораздо важнее не зазубрить определение, а понять суть. Поэтому я тут постараюсь рассказать все как можно подробнее и проще.
Существуют клиентские и серверные сокеты. Вполне легко догадаться что к чему. Серверный сокет прослушивает определенный порт, а клиентский подключается к серверу. После того, как было установлено соединение начинается обмен данными.
Рассмотрим это на простом примере. Представим себе большой зал с множеством небольших окошек, за которыми стоят девушки. Есть и пустые окна, за которыми никого нет. Те самые окна — это порты. Там, где стоит девушка — это открытый порт, за которым стоит какое-то приложение, которое его прослушивает. То есть, если, вы подойдете к окошку с номером 9090, то вас поприветствуют и спросят, чем могут помочь. Так же и с сокетами. Создается приложение, которое прослушивает свой порт. Когда клиент устанавливает соединение с сервером на этом порту именно данное приложение будет ответственно за работу этим клиентом. Вы же не подойдете к одному окошку, а кричать вам будут из соседнего :)
После успешной установки соединения сервер и клиент начинают обмениваться информацией. Например, сервер посылает приветствие и предложение ввести какую-либо команду. Клиент в свою очередь вводит команду, сервер ее анализирует, выполняет необходимые операции и отдает клиенту результат.
Сервер
Сейчас создайте два файла — один для сервера, а другой для клиента.
В Python для работы с сокетами используется модуль socket:
Прежде всего нам необходимо создать сокет:
Здесь ничего особенного нет и данная часть является общей и для клиентских и для серверных сокетов. Дальше мы будем писать код для сервера. Это вполне логично — зачем нам писать клиентское приложение, если некуда подключаться :)
Теперь нам нужно определиться с хостом и портом для нашего сервера. Насчет хоста — мы оставим строку пустой, чтобы наш сервер был доступен для всех интерфейсов. А порт возьмем любой от нуля до 65535. Следует отметить, что в большинстве операционных систем прослушивание портов с номерами 0 — 1023 требует особых привилегий. Я выбрал порт 9090. Теперь свяжем наш сокет с данными хостом и портом с помощью метода bind, которому передается кортеж, первый элемент (или нулевой, если считать от нуля) которого — хост, а второй — порт:
Теперь у нас все готово, чтобы принимать соединения. С помощью метода listen мы запустим для данного сокета режим прослушивания. Метод принимает один аргумент — максимальное количество подключений в очереди. Напряжем нашу бурную фантазию и вспомним про зал с окошками. Так вот этот параметр определяет размер очереди. Если он установлен в единицу, а кто-то, явно лишний, пытается еще подстроится сзади, то его пошлют :) Установим его в единицу:
Ну вот, наконец-то, мы можем принять подключение с помощью метода accept, который возвращает кортеж с двумя элементами: новый сокет и адрес клиента. Именно этот сокет и будет использоваться для приема и посылке клиенту данных.
Вот и все. Теперь мы установили с клиентом связь и можем с ним «общаться». Т.к. мы не можем точно знать, что и в каких объемах клиент нам пошлет, то мы будем получать данные от него небольшими порциями. Чтобы получить данные нужно воспользоваться методом recv, который в качестве аргумента принимает количество байт для чтения. Мы будем читать порциями по 1024 байт (или 1 кб):
Как мы и говорили для общения с клиентом мы используем сокет, который получили в результате выполнения метода accept. Мы в бесконечном цикле принимаем 1024 байт данных с помощью метода recv. Если данных больше нет, то этот метод ничего не возвращает. Таким образом мы можем получать от клиента любое количество данных.
Дальше в нашем примере для наглядности мы что-то сделаем с полученными данными и отправим их обратно клиенту. Например, с помощью метода upper у строк вернем клиенту строку в верхнем регистре.
Теперь можно и закрыть соединение:
Собственно сервер готов. Он принимает соединение, принимает от клиента данные, возвращает их в виде строки в верхнем регистре и закрывает соединение. Все просто :) В итоге у вас должно было получиться следующее:
Клиент
Думаю, что теперь будет легче. Да и само клиентское приложение проще — нам нужно создать сокет, подключиться к серверу послать ему данные, принять данные и закрыть соединение. Все это делается так:
Думаю, что все понятно, т.к. все уже разбиралось ранее. Единственное новое здесь — это метод connect, с помощью которого мы подключаемся к серверу. Дальше мы читаем 1024 байт данных и закрываем сокет.
Этот учебник знакомит с программированием сокетов Java по протоколу TCP/IP с реальным приложением Клиент/сервер.
1. Обзор
Термин сокет программирование относится к написанию программ, которые выполняются на нескольких компьютерах, в которых все устройства подключены друг к другу с помощью сети.
Существует два протокола связи, которые можно использовать для программирования сокетов: Протокол пользовательских дейтаграмм (UDP) и протокол управления передачей (TCP) .
Основное различие между ними заключается в том, что UDP не имеет соединения, что означает отсутствие сеанса между клиентом и сервером, в то время как TCP ориентирован на соединение, что означает, что сначала должно быть установлено исключительное соединение между клиентом и сервером для осуществления связи.
Этот учебник представляет введение в программирование сокетов по сетям TCP/IP и демонстрирует, как писать клиентские/серверные приложения на Java. UDP не является основным протоколом и как таковой может встречаться нечасто.
2. Настройка проекта
Java предоставляет набор классов и интерфейсов, которые заботятся о деталях низкоуровневой связи между клиентом и сервером.
Нам также нужны java.io пакет, который дает нам входные и выходные потоки для записи и чтения во время общения:
3. Простой Пример
Давайте испачкаем руки в самых основных примерах, связанных с клиентом и сервером . Это будет двустороннее коммуникационное приложение, в котором клиент приветствует сервер, а сервер отвечает.
Давайте создадим серверное приложение в классе с именем GreetServer.java со следующим кодом.
Мы включаем метод main и глобальные переменные, чтобы привлечь внимание к тому, как мы будем запускать все серверы в этой статье. В остальных примерах в статьях мы опустим этот тип более повторяющегося кода:
Давайте также создадим клиент под названием GreetClient.java с этим кодом:
Давайте запустим сервер; в вашей IDE вы делаете это, просто запустив его как Java-приложение.
А теперь давайте отправим приветствие на сервер с помощью модульного теста, который подтверждает, что сервер действительно отправляет приветствие в ответ:
Не волнуйтесь, если вы не совсем понимаете, что здесь происходит, так как этот пример призван дать нам представление о том, чего ожидать в дальнейшем в статье.
В следующих разделах мы проанализируем связь сокетов , используя этот простой пример, и углубимся в детали с большим количеством примеров.
4. Как Работают Сокеты
Мы будем использовать приведенный выше пример, чтобы пройти через различные части этого раздела.
По определению, сокет -это одна конечная точка двустороннего канала связи между двумя программами, запущенными на разных компьютерах в сети. Сокет привязан к номеру порта , чтобы транспортный уровень мог идентифицировать приложение, которому предназначены данные для отправки.
4.1. Сервер
Обычно сервер работает на определенном компьютере в сети и имеет сокет, привязанный к определенному номеру порта. В нашем случае мы используем тот же компьютер, что и клиент, и запустили сервер по порту 6666 :
Сервер просто ждет, слушая сокет, чтобы клиент сделал запрос на подключение. Это происходит на следующем шаге:
Когда код сервера встречает метод accept , он блокируется до тех пор, пока клиент не сделает запрос на подключение к нему.
Если все идет хорошо, сервер принимает соединение. После принятия сервер получает новый сокет clientSocket , привязанный к тому же локальному порту, 6666 , а также имеет свою удаленную конечную точку, установленную на адрес и порт клиента.
Чтобы обеспечить непрерывность связи, нам придется читать из входного потока внутри цикла while и выходить только тогда, когда клиент отправляет запрос на завершение, мы увидим это в действии в следующем разделе.
Для каждого нового клиента серверу требуется новый сокет, возвращаемый вызовом accept . ServerSocket используется для продолжения прослушивания запросов на подключение в соответствии с потребностями подключенных клиентов. Мы еще не допустили этого в нашем первом примере.
4.2. Клиент
Клиент должен знать имя хоста или IP-адрес машины, на которой работает сервер, и номер порта, на котором сервер прослушивает.
Чтобы сделать запрос на подключение, клиент пытается встретиться с сервером на компьютере и порту сервера:
Клиент также должен идентифицировать себя с сервером, чтобы привязаться к локальному номеру порта, назначенному системой, который он будет использовать во время этого соединения. Мы не занимаемся этим сами.
Приведенный выше конструктор создает новый сокет только тогда, когда сервер принял соединение, в противном случае мы получим исключение отказа в соединении. После успешного создания мы можем получить входные и выходные потоки от него для связи с сервером:
5. Непрерывная Связь
Таким образом, это полезно только в запросах ping, но представьте, что мы хотели бы внедрить сервер чата, и, безусловно, потребуется непрерывная обратная связь между сервером и клиентом.
Обратите внимание, что мы добавили условие завершения, при котором цикл while завершается, когда мы получаем символ точки.
Мы запустим Echo Server , используя метод main так же, как мы это сделали для GreetServer . На этот раз мы запускаем его на другом порту, таком как 4444 чтобы избежать путаницы.
Клиент Echo похож на Отличный клиент , поэтому мы можем дублировать код. Мы разделяем их для ясности.
В другом тестовом классе мы создадим тест, чтобы показать, что несколько запросов к EchoServer будут обслуживаться без закрытия сокета сервером. Это верно до тех пор, пока мы отправляем запросы от одного и того же клиента.
Работа с несколькими клиентами-это другой случай, который мы рассмотрим в следующем разделе.
Давайте создадим метод setup для инициирования соединения с сервером:
Мы также создадим метод tearDown для освобождения всех наших ресурсов, это лучшая практика для каждого случая, когда мы используем сетевые ресурсы:
Это улучшение по сравнению с первоначальным примером, когда мы общались только один раз, прежде чем сервер закрыл наше соединение; теперь мы посылаем сигнал завершения, чтобы сообщить серверу, когда мы закончим сеанс .
6. Сервер С Несколькими Клиентами
Несмотря на то, что предыдущий пример был улучшением по сравнению с первым, он все еще не является отличным решением. Сервер должен иметь возможность обслуживать множество клиентов и множество запросов одновременно.
Работа с несколькими клиентами-это то, что мы рассмотрим в этом разделе.
Еще одна особенность, которую мы увидим здесь, заключается в том, что один и тот же клиент может отключиться и снова подключиться, не получая исключения отказа в подключении или сброса соединения на сервере. Раньше мы не могли этого сделать.
Это означает, что наш сервер будет более надежным и устойчивым к многочисленным запросам от нескольких клиентов.
Мы сделаем это, чтобы создать новый сокет для каждого нового клиента и обслуживать запросы этого клиента в другом потоке. Количество клиентов, обслуживаемых одновременно, будет равно количеству запущенных потоков.
Основной поток будет выполнять цикл while, когда он прослушивает новые соединения.
Хватит разговоров, давайте создадим еще один сервер под названием EchoMultiServer.java. Внутри него мы создадим класс потока обработчика для управления коммуникациями каждого клиента в его сокете:
Обратите внимание, что теперь мы вызываем accept внутри цикла while . Каждый раз, когда выполняется цикл while , он блокирует вызов accept до тех пор, пока не подключится новый клиент , а затем для этого клиента создается поток обработчика EchoClientHandler .
Давайте запустим наш сервер, используя его основной метод на порту 5555 .
Для ясности мы все равно поместим тесты в новый набор:
Мы могли бы создать столько тестовых случаев, сколько нам заблагорассудится, каждый из которых порождает нового клиента, и сервер будет обслуживать их все.
7. Заключение
В этом уроке мы сосредоточились на введении в программирование сокетов через TCP/IP и написали простое клиент-серверное приложение на Java.
Сокеты являются основой почти любого сетевого приложения, даже если их использование скрыто от ваших глаз. Наша цель — разобраться, что это такое, и написать простое приложение для взаимодействия по сети. Реализуем мы это приложение на языке Go, но принципы использования сокетов одинаковы для любых языков программирования, а библиотеки имеют очень похожий API.
Сетевое взаимодействие можно описать с помощью стека протоколов, которые его обеспечивают. Сетевой протокол — это набор правил, с помощью которых можно соединиться и обмениваться данными с другим устройством сети. Эта задача включает в себя множество подзадач: кодирование данных в физические сигналы, поиск получателя в сети, обеспечение надежной передачи, шифрование трафика и так далее. Поэтому и существует такое понятие как стек протоколов: протоколы разных уровней решают разные задачи. При этом протоколы одного уровня решают поставленную задачу по-разному и является взаимозаменяемыми без влияния на протоколы других уровней.
Существуют разные модели представления сетевого взаимодействия. Одной из самых популярных является модель TCP/IP, названная по двум самым популярным протоколам современной глобальной сети. Она включает в себя четыре уровня. На самом низком уровне — уровне сетевых интерфейсов, решается задача физического доступа и кодирования информации. Сетевой уровень соединяет сети всего мира в глобальную сеть. Главный протокол этого уровня — IP. Он определяет адресацию (IP-адреса), благодаря которой устройства могут связываться друг с другом на любых расстояниях. Какие задачи решает транспортный уровень мы обсудим в следующем разделе. На прикладном же уровне работают большинство сетевых приложений компьютера. Они определяют свои протоколы под конкретные задачи.
Чтобы программисту не нужно было контролировать доставку пакетов, их целостность и порядок, эти задачи возложены на транспортный уровень стека сетевых протоколов. Есть два наиболее популярных протокола на этом уровне.
Протокол 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-сервера:
- Создать слушающий сокет с открытым портом.
- В бесконечном цикле принимать соединения от клиентов.
- При соединении с клиентом создается новый сокет для взаимодействия только с этим клиентом.
- Для каждого соединения обрабатывать клиентские запросы в отдельном потоке.
Все, что требуется от клиента, это подключиться к серверу по нужному порту и обмениваться с ним данными через сокет. Код клиента:
Так выглядит взаимодействие клиента с сервером:
Взаимодействие по UDP отличается тем, что в этом протоколе нет понятия соединения. Поэтому мы реализуем совсем простой вариант, в котором сервер отвечает клиенту тем же текстом, что получил от него. При этом клиентский код отличается только именем протокала, который передается в функцию net.Dial. Код сервера:
Компьютерные сети являются обширной темой, о которой нужно иметь представление любому бэкенд-разработчику. Сегодня мы рассмотрели сокеты, с помощью которых программист может обеспечить взаимодействовие с другими устройствами сети на транспортном уровне.
Для создания сокета существует функция, называемая socket . Она принимает аргументы family , type и proto (подробнее см. в документации). Чтобы создать TCP-сокет, нужно использовать socket.AF_INET или socket.AF_INET6 для family и socket.SOCK_STREAM для type .
Пример Python socket:
Функция возвращает объект сокета, который имеет следующие основные методы:
- bind()
- listen()
- accept()
- connect()
- send()
- recv()
Здесь мы создаем серверный сокет, привязываем его к localhost и 50000-му порту и начинаем прослушивать входящие соединения.
Чтобы принять входящее соединение, мы вызываем метод accept() , который будет блокироваться до тех пор, пока не подключится новый клиент. Когда это произойдет, метод создаcт новый сокет и вернет его вместе с адресом клиента.
Затем он в бесконечном цикле считывает данные из сокета партиями по 1024 байта, используя метод recv() , пока не вернет пустую строку. После этого он отправляет все входящие данные обратно, используя метод sendall() , который в свою очередь многократно вызывает метод send() . И после этого сервер просто закрывает клиентское соединение. Данный пример может обрабатывать только одно входящее соединение, потому что он не вызывает accept() в цикле.
Код на стороне клиента выглядит проще:
Вместо методов bind() и listen() он вызывает только метод connect() и сразу же отправляет данные на сервер. Затем он получает обратно 1024 байта, закрывает сокет и выводит полученные данные.
Все методы сокета являются блокирующими. Это значит, что когда метод считывает данные из сокета или записывает их в него, программа больше ничего делать не может.
Для решения этой проблемы существует так называемый способ асинхронного взаимодействия с сокетами. Основная идея состоит в том, чтобы делегировать поддержание состояния сокета операционной системе и позволить ей уведомлять программу, когда есть данные для чтения из сокета или когда сокет готов к записи.
Существует множество интерфейсов для разных операционных систем:
Все они примерно одинаковы, поэтому давайте создадим сервер с помощью Python select. Пример Python select :
Как видите, кода гораздо больше, чем в блокирующем Echo-сервере. Это в первую очередь связано с тем, что мы должны поддерживать набор очередей для различных списков сокетов, то есть сокетов для записи, чтения и отдельный список для ошибочных сокетов.
Создание серверного сокета происходит так же, кроме одной строки: server.setblocking(0) . Это нужно для того, чтобы сокет не блокировался. Такой сервер более продвинутый, поскольку он может обслуживать более одного клиента. Главная причина заключается в сокетах selecting :
Этот вызов (если не передан аргумент timeout ) блокирует программу до тех пор, пока какие-либо из переданных сокетов не будут готовы. В этот момент вызов вернет три списка сокетов для указанных операций.
Так работают сокеты на низком уровне. Однако в большинстве случаев нет необходимости реализовывать настолько низкоуровневую логику. Рекомендуется использовать более высокоуровневые абстракции, такие как Twisted, Tornado или ZeroMQ, в зависимости от ситуации.
Читайте также: