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

"А вечер-то перестаёт быть томным" - подумал я и не ошибся. Чёрт возьми, это какая-то магия, вертелось в моей голове по пути домой. Тут надо отметить, что я не только бедный студент, но ещё и преподаватель в кружке программирования, поэтому после столкновения с такой интересной темой во мне затаилось жгучее желание поделиться ею со своими ребятами.

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

Для начала определимся, что это за зверь такой - сокет?

Представим себе работу ресторана быстрого питания, пусть будет Burger Queen, так вот работник этого заведения и будет сокетом, то есть программой, которая отвечает за обмен данными(бургерами) между клиентом и заведением. Как сокет узнает кому отдать заветное блюдо(данные)? У него есть чек с номером заказа! Вот и у сокета есть порт к которому он привязан, то есть и в Burger Queen и в сетевых технологиях есть приложение, тот самый сокет, который отвечает за работу с определенным портом, так же как и работник, который отвечает за обработку конкретного номера заказа, ведь и к конкретному компьютеру и к конкретной забегаловке одновременно конектятся разные клиенты, и всем им нужны разные данные.

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

Пишем сервер

Ну вот, самая потная часть статьи позади, расчехляем питонов! Первым делом напишем программу серверного сокета, работника Burger Queen, который принимает заказы.

import socket # Подключаем необходимую библиотеку, она встроена


new_socket = socket.socket() #Создаём объект сокета
new_socket.bind(('127.0.0.1', 5000)) # Привязываем наш объект к ip и порту
new_socket.listen(2) # Указываем нашему сокету, что он будет слушать 2 других

print("Server is up now!")

Каюсь, в объяснении сокетов я ни слова не сказал про IP, но тут тоже всё просто, наш работник(сокет) работает в конкретном ресторане по конкретному адресу, то есть, подытоживая можно сказать, в конкретном ресторане по определенному IP адресу работает много работников, сокетов, каждый из которых обслуживает свой порт, номер заказа.

Почему я выбрал 5000-ный порт? Методом научного тыка, другие доступные порты на вашем устройтве могут быть определены с помощью специальных утилит, а IP 127.0.0.1 - стандартный локальный адрес любого компьютера(совсем любого).

Ползём дальше, получаем коннекты.

conn1, add1 = new_socket.accept() 
# сохраняем объект сокета нашего клиента и его адрес
print("First client is connected!")

conn2, add2 = new_socket.accept()
#аналогично со вторым клиентом
print("Second client is connected!")

Далее создадим две функции, которые принимают данные от одного клиента и отправляют их другому.

def acceptor1():
  # Запустим бесконечный цикл, мы хотим общаться постоянно!
    while True:
#Получим 1024 байта от первого клиента
        a = conn1.recv(1024)
 #Перешлём их второму
        conn2.send(a)

def acceptor2():
# А здесь мы получим 1024 байта от второго клиента и перешлём первому.
    while True:
        b = conn2.recv(1024)
        conn1.send(b)

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

from threading import Thread # Подключили класс потока

#Создаём потоки, в качестве именнованного аргумента передаем нашу ф-ю
tread1 = Thread(target=acceptor1) 
tread2 = Thread(target=acceptor2)

#Запускаем потоки
tread1.start()
tread2.start()

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

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

curl 127.0.0.1:5000

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

Итак, приведём полный код сервера.

import socket 
from threading import Thread

new_socket = socket.socket()
new_socket.bind(('127.0.0.1', 5000))

new_socket.listen(2)

print("Server is up now!")

conn1, add1 = new_socket.accept()
print("First client is connected!")

conn2, add2 = new_socket.accept()
print("Second client is connected!")

def acceptor1():
    while True:
        a = conn1.recv(1024)
        conn2.send(a)

def acceptor2():
    while True:
        b = conn2.recv(1024)
        conn1.send(b)

tread1 = Thread(target=acceptor1)
tread2 = Thread(target=acceptor2)

tread1.start()
tread2.start()

Пишем клиента

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

#Подключаем зависимости
import socket
from threading import Thread
#Создаём новый сокет
client_socket = socket.socket()
#Заставляем его подключиться к серверному сокету
client_socket.connect(("127.0.0.1", 5000))
#Создаём ф-и отправки и получения сообщений
def sender():
    while True:
      #Читаем строку с клавиатуры
        a = input()
        #Отправляем её, предварительно закодировав
        client_socket.send(a.encode("utf-8"))
def reciver():
    while True:
      #Получаем строку от сервера
        b = client_socket.recv(1024)
       #Печатаем, предварительно раскодировав
        print(b.decode("utf-8"))
#Создаём по отдельному потоку для каждой функции
tread1 = Thread(target=sender)
tread2 = Thread(target=reciver)
#Потоки запушены, клиент готов получать и отправлять сообщения
tread1.start()
tread2.start()

Вот и всё! Создаем двух клиентов и переписываемся без всякой помощи из вне, как говорится, мы и сами с усами.

P. S.

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

Комментарии (20)


  1. vilgeforce
    21.12.2021 18:50
    +1

    Если у вас все порты до 5000 "заняты" - что ж у вас там происходит?! :-D Скорее всего, какие-то политики безопасности не дают открывать порты с номерами ниже 5000


  1. freemailroot
    21.12.2021 19:29
    +1

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

    я выбрал 5000-ный порт, потому что методом проб и ошибок только с ним запустилась моя программа

    Вы перебирали все 4999 портов до этого? Пользоваться файрволом вас не учили? Смотреть какие сервисы слушают какие порты вы не умеете?

    Учтите, пожалуйста, но не обижайтесь, коллега-студент)


    1. kovalyov-z3 Автор
      21.12.2021 19:38

      Согласен, выразился криво, исправлюсь


      1. freemailroot
        21.12.2021 20:03

        Замечательно, что прислушались. Советую и замечания других комментаторов учесть.


  1. r_noise
    21.12.2021 19:38

    В 2021 чтение из сокета по while true? Серьезно?


    1. kovalyov-z3 Автор
      21.12.2021 19:38

      А что не так?


      1. r_noise
        21.12.2021 21:32
        -1

        Комментарий я оставил в горячке. Не сам while true конечно. Даже не знаю с чего начать, чтобы дать вам конструктивную критику. Статья очень сырая.

        Как писали выше, почему выбран порт 5000? А почему нельзя использовать 1023, или все таки можно, почему?

        Проблема блокируюх сокетов никак не описана. Почему на сервере использовали threading? Какие у этого способа ограничения? А сколько клиентов может обработать такой код? А что на счет select?

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

        А еще было бы неплохо упомянуть про асинхронность и какие проблемы она решает.


        1. Serge78rus
          21.12.2021 22:50

          А что на счет select?
          А почему Вы требуете именно select, а не poll или epoll? Задачи ведь разные бывают, не только обслуживать тысячи одновременных подключений. В некоторых случаях и обычных блокирующих сокетов вполне достаточно.


          1. r_noise
            22.12.2021 04:33

            Я не требую только select, я как раз про то что нужно упоминуть, и poll/epoll в т.ч. Тем более что они реализованы том же модуле select. Да может и нет смысла разбирать все в одной статье, но как минимум нужно указать что существуют разные способы реализации. В идеале бы конечно привести примеры и описание кейсов.


            1. Serge78rus
              22.12.2021 10:37

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


      1. r_noise
        21.12.2021 21:36

        Ну а посыл, собственно, что в 21-м так никто с сокетами не работает, смысла давать такие примеры нет. Разве что на вводном уроке по сокетам и то при условии, что дальше будут углубленно разобраны другие варианты.


  1. kozlyuk
    21.12.2021 20:25

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


    1. kovalyov-z3 Автор
      21.12.2021 20:41

      Абсолютно с вами согласен, эта статья не является грамотным руководством, но ведь многие, будучи школьниками, после того как научились печатать цифры из консоли хотят сотворить что-то поинтереснее, вот для них-то и предназначен этот материал


  1. ne555
    21.12.2021 21:35
    +2

    Вы пишете первый пост, его проверяют модераторы , и, если всё хорошо, отправляют в основную ленту Хабра

    Модераторы ау! Как так проверяете: спустя рукава? Учитывая, что нам всем советы тут раздавали от лица администрации, как надо писать.

    Спойлер


  1. Heliki
    21.12.2021 23:19
    +1

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

    1. Аналогия порта с человеком, который выдает заказ по его номеру, не совсем верная. Сервер принимает множество клиентов на одном и том же порту, в вашем случае это будет равносильно тому, что придет 2 человека с одинаковыми номерами заказа, как их будут обслуживать в этой ситуации?

    2. Уберите упоминание веб-сокетов. WebSocket это отдельная технология, работающая поверх тех, что вы тут описали.


  1. FAT
    22.12.2021 03:44

    Я бы ещё упомянул про UDP и TCP при создании сокета: SOCK_STREAM и SOCK_DGRAM


  1. ScarferNV
    22.12.2021 11:49

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


    1. cutwater
      22.12.2021 15:01

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


  1. cutwater
    22.12.2021 14:51

            #Получим 1024 байта от первого клиента
            a = conn1.recv(1024)

    После этого статью можно в принципе дальше не читать. Хрестоматийный пример того как делать не надо.

          #Получаем строку от сервера
            b = client_socket.recv(1024)
           #Печатаем, предварительно раскодировав
            print(b.decode("utf-8"))

    А здесь еще лучше.

    Во-первых socket.recv возвращает не ровно 1024 байта, а до 1024 байта.

    Во-вторых нет никакой гарантии что эта функция вернет все сообщение целиком (не забываем что у нас здесь потоковые сокеты). Да и вообще нет никакой гарантии того, что байтовая последовательность, которую вернет sock.recv будет корректной utf-8 последовательностью по этой же причине.

    С socket.send ровно та же проблема. То что этот код создает видимость работоспособного всего лишь удачное стечение обстоятельств, не более.

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


    1. cutwater
      22.12.2021 14:57

      new_socket.listen(2) # Указываем нашему сокету, что он будет слушать 2 других

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