Что вообще значит «прослушивать порт»?

Что вообще значит «прослушивать порт»?


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


На другом конце кофейни бариста оторвался от своего телефона, чтобы окинуть взглядом помещение. Один наушник вставлен в ухо, второй висит, телефон воспроизводит видео с заданием, выданным на лекции по кинематографии. В кофейне, где работают студенты, есть неписанное правило: в ночную смену сотрудники могут пользоваться длинными промежутками между клиентами, чтобы выполнять домашние задания. Кроме Тима и Лиз в помещении по одиночке сидит ещё пара студентов, уже много часов сосредоточенно смотрящих в свои ноутбуки. Вся остальная часть кофейни пуста.


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


«Чёрт, а сколько времени?», — спрашивает он.


Лиз переводит взгляд на часы на экране ноутбука. «Два с небольшим».


Тим зевает и начинает писать с начала новой страницы, но Лиз его прерывает.


«Тим».


«Что?», — отвечает Тим, преувеличенно демонстрируя своё раздражение от того, что его прервали, когда он только начал писать.


«Что значит „прослушивать порт“?»


«Хм».


«Мне нужно написать веб-сервер для курса net», — Лиз сокращает полное название курса Computer Networks 201, который Тим прошёл в прошлом семестре.


«Ага, помню такое».


«И я слушаю соединения к порту».


«Порт 80», — уверенно отвечает Тим, надеясь прервать разговор, опередив её вопрос.


«На самом деле, мы должны прослушивать 8080, чтобы он мог работать без рута, но я не об этом».


«Ну ладно, тогда о чём?»


«Что значит прослушивание порта?»


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


«Да, это я знаю, но как


Прежде чем ответить, Тим несколько секунд размышляет.


«Наверно, у операционной системы есть большая таблица портов и слушающих их процессов. Когда ты привязываешься к порту, она помещает указатель на твой сокет в эту таблицу».


«Ага, наверно», — отвечает Лиз нерешительно.


Каждый из них возвращается к своей работе. Спустя какое-то время тишину прерывает победоносное тихое «Да!» Тима, он зачёркивает номер на распечатанном листе бумаги. Он наконец нашёл доказательство, которое с трудом искал для задачи по матанализу.


Лиз воспользовалась возможностью снова привлечь его внимание.


«Смотри, Тим, я запустила два процесса, одновременно привязанные к одному порту».


Она разворачивает два окна с кодом на Python:


# server1.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept())


И рядом с ним:


# server2.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('127.0.0.1', 8080))
print(sock.recv(1024))


Потом она показывает, что обе программы работают в отдельных окнах терминала через shell-подключение к Debian-серверу университета cslab3.


Тим разворачивает ноутбук к себе. Открывает третий терминал, на секунду останавливается, освежая воспоминания в своём усталом мозгу, и вводит netcat 127.0.0.1 8080.


netcat запускается и мгновенно завершается. В другом окне терминала завершается запущенная программа python server1.py, выводя следующее:


(<socket.socket fd=4, family=AddressFamily.AF_INET,

type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080),

raddr=('127.0.0.1', 59558)>, ('127.0.0.1', 59558))


Он изучает код server1.py, рассуждая вслух.


«Итак, сервер выполняет привязку к порту, принимает первый сокет для подключения, а затем выполняет выход. Понятно, значит выведенный на экран кортеж был результатом вызова accept, после чего выполняется выход. Но теперь (он наводит курсор на редактор с кодом server2.py) этот код вообще что-то прослушивает?»


Он снова запускает netcat 127.0.0.1 8080 -v в том же терминале, что и раньше, и в нём выводится следующее:


netcat: connect to 127.0.0.1 port 8080 (tcp) failed: Connection refused


«Видишь», — говорит он — «в твоём коде баг. server2 по-прежнему запущен, но ты не вызываешь listen. На самом деле он ничего не делает с портом 8080».


«Да нет, точно делает», — отвечает Лиз, хватая ноутбук.


Она добавляет -u в конец команды netcat и нажимает Enter. На этот раз она не выдаёт ошибку и не выходит сразу же, а ждёт ввода с клавиатуры. Раздражённая тем, что Тим сразу предположил наличие бага в её коде, она набирает timmy, зная, что это имя его бесит.


Сессия netcat завершается без вывода и одновременно программа python server2.pyзавершает работу, выведя:


b'timmy\n'


Тим замечает попытку Лиз поддеть его, но игнорирует её, не желая доставлять ей удовольствие. Он тянется к клавиатуре. Лиз поворачивает ноутбук к нему и он вводит man netcat, чтобы открыть документацию по команде netcat, в которой этот инструмент описывается как «швейцарский армейский нож для TCP/IP». Он доходит до флага -u, который в документации кратко описан как «UDP mode».


«Ага», — говорит он, вспомнив. «Понял, server1 слушает по TCP, а server2 слушает по UDP. Наверно, это и означает SOCK_DGRAM. То есть это разные протоколы. Думаю, у операционной системы есть отдельные таблицы для каждого. Кажется, тему UDP в курсе net проходят позже».


«Ага, я прочитала учебник заранее».


«Естественно. Как это у тебя есть время читать лишние темы, но нет времени выполнять задания так, чтобы не пришлось делать их ночью перед сдачей?»


«Могу задать тебе тот же вопрос про Counter Strike», — парирует Лиз.


Тим ворчит.


Они снова начинают работать в тишине, но спустя несколько минут Лиз её прерывает.


«Тим, посмотри-ка. Я могу прослушивать один порт двумя процессами, даже если они оба TCP».


Тим отвлекается от своей работы. На этот раз на экране у Лиз всего одна программа на Python, запущенная в двух терминалах:


# server3.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept())


Лиз объясняет: «Видишь, эта команда показывает, что процесс прослушивает порт». Она вводит lsof -i:8080 и нажимает на Enter.


Программа выводит следующее:


> lsof -i:8080

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME

python3 174265 liz 3u IPv4 23850797 0t0 TCP localhost:http-alt (LISTEN)

python3 174337 liz 3u IPv4 23853188 0t0 TCP localhost:http-alt (LISTEN)


«Что произойдёт, если к нему подключиться?», — спрашивает Тим, на этот раз с долей интереса.


«Смотри».


Лиз один раз выполняет netcat localhost 8080, и один из процессов сервера завершается, а второй продолжает работать. При повторном выполнении команды завершается и другой процесс.


Тим начинает изучать код и водит пальцем по экрану, чтобы читать его. Лиз ненавидит заляпанный экран, поэтому говорит «аккуратно!» и отталкивает его руку. «Я не буду касаться», — возражает Тим. Держа руку на подчёркнуто безопасном расстоянии, он указывает на строку с setsockopt и спрашивает: «А это что ещё за магия?»


«Здесь мы задаём опцию сокета, позволяющую многократно использовать порт».


«Хм, это есть в учебнике?»


«Не знаю, нашла это на Stack Overflow».


«Не думал, что можно так использовать порт несколько раз».


«Я тоже», — она остановилась и подумала. «То есть в операционной системе не может быть просто таблица портов к сокетам, это должна быть таблица портов к списку сокетов. И ещё одна для UDP. И, возможно, для других протоколов».


«Ага, вроде логично», — соглашается Тим.


«Хм-м-м», — говорит Лиз, внезапно её голос стал менее уверенным.


«Что?»


«Ой, да ладно», — отвечает она и начинает сосредоточенно что-то печатать.


Тим возвращается к своему заданию, и спустя несколько минут он перечёркивает ещё один вопрос. Он почти закончил, и его поза становиться более расслабленной. Лиз наклоняет к нему ноутбук и говорит: «Зацени». Она показывает ему две программы.


# server4.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.2', 8080))
sock.listen()
print(sock.accept())


# server5.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.3', 8080))
sock.listen()
print(sock.accept())


«Разве они не одинаковые?», — спрашивает Тим после изучения кода.


«Посмотри на IP привязки».


«А, так ты слушаешь один порт, но два разных IP. И это работает?»


«Похоже, да. И я могу подключиться к обоим».


Лиз выполнила netcat 127.0.0.2, а затем netcat 127.0.0.3.


Тим задумался. «Так, посмотрим. У операционной системы должна быть таблица от каждого сочетания порта и IP к сокету. Хотя нет, на самом деле, две: одна для TCP и одна для UDP».


«Ага», — кивнула Лиз. «И это может быть не один, а много сокетов. Но посмотри». Она меняет IP в коде сервера на 0.0.0.0.


# server6.py
import socket

sock socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 8080))
sock.listen()
print(sock.accept())


«Теперь когда я запускаю сервер, привязанный к 127.0.0.2, то получаю следующее», — продолжает она.


Traceback (most recent call last):

File "server5.py", line 4, in <module>

s.bind(('127.0.0.2', 8080))

OSError: [Errno 99] Cannot assign requested address


«Но если я выполню netcat 127.0.0.2 8080, то подключение к серверу будет по адресу 0.0.0.0».


«Ну да, 0.0.0.0 означает „привязаться ко всем локальным IP“, разве в лекции об этом не говорили? И адреса, начинающиеся с 127. — это локальные IP loopback-а, поэтому логично, что они привязаны именно так».


«Ага, но как это работает? Есть примерно 16 миллионов IP, начинающихся с 127.. Операционная система ведь не создаёт большую таблицу со всеми этими адресами?»


«Думаю, нет», — у него не было ответа, поэтому он решил сменить тему. «Ну ладно, а как там дела с твоим HTTP-сервером?», — вопрос риторический, ведь он знает, что Лиз ещё не написала ни одной строки самого задания.


Она отвечает что-то неопределённое, потому что её уже поглотил другой эксперимент.


Прошло ещё немного времени. Завершив свою работу, Тим поглядывает на время на своём телефоне. Он подумывает, не пойти ли домой, к своему неровному матрасу в общежитии. Он решил, что диван кофейни такой же удобный и откинул голову на его высокую спинку.


Полусонный, он смотрит на потолок, и тут Лиз толкает его и говорит: «Тим, посмотри».


Она показывает ему ещё одну программу:


# server7.py
import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 8080))
sock.listen()
print(sock.accept())


«Зацени. Это IPv6-сервер».


Тим зевнул и придвинулся. К тому времени утреннее солнце начало светить через окно на диван, на котором они сидели. Два других студента незаметно вышли из кофейни в первые утренние часы и пришёл первый дневной клиент, ожидавший своего кофе на вынос.


«Что это за двоеточия?», — спросил Тим.


«Это краткая запись восьми нулей в IPv6, они означают то же, что и 0.0.0.0 в IPv4».


«То есть этот код приказывает прослушивать все локальные IP IPv6? Так работает IPv6?»


«Ну да, по сути, так».


Она ввела netcat "::1" 8080 -v и объяснила, что ::1 — это loopback-адрес в IPv6.


«То есть типа 127.0.0.1 для обычных IP».


«IPv4. Да, именно. Но посмотри сюда. По данным lsof, я слушаю только по IPv6, видишь?», — Лиз выполнила lsof -i :8080, и команда вывела одну строку


COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME

python3 455017 liz 3u IPv6 25152485 0t0 TCP *:http-alt (LISTEN)


«Но я могу подключиться к нему через IP IPv4».


netcat 127.0.0.1 8080 -v


«Хм, а наоборот? Можно подключиться к IPv4-серверу с IP IPv6?»


«Неа, смотри».


Она запустила python3 server6.py, а затем netcat "::1" 8080 -v, получив такой результат:


netcat: connect to ::1 port 8080 (tcp) failed: Connection refused


Тим спросил: «А что будет, если попробовать прослушать IPv6 по 8080, когда IPv4-сервер продолжает работать?»


Лиз показала ему, запустив python server7.py.


Traceback (most recent call last):

File "server7.py", line 4, in <module>

s.bind(('::', 8080))

OSError: [Errno 98] Address already in use


«Но посмотри», — сказала она, открыв код ещё одной программы.


# server8.py
import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
sock.bind(('::', 8080))
sock.listen()
print(sock.accept())


Она показала на строку с setsockopt, объяснив: «Если я добавляю это, то могу слушать по IPv6 и IPv4 через один порт из разных процессов».


Она запустила python server8.py, а затем lsof -i :8080.


COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME

python3 460409 liz 3u IPv6 25188010 0t0 TCP *:http-alt (LISTEN)

python3 460813 liz 3u IPv4 25191765 0t0 TCP *:http-alt (LISTEN)


Тим подвёл итог тому, что ему показала Лиз. «То есть при прослушивании порта ты на самом деле слушает комбинацию порта, IP-адреса, протокола и версии IP?»


«Да, только если ты не прослушиваешь все локальные IP. И если ты прослушиваешь все IP IPv6 ты одновременно прослушиваешь все IP IPv4, если только специально не откажешься от этого перед вызовом bind».


«Понятно. То есть у операционной системы должна быть какая-то hash map от пары порта и IP к сокету для каждой комбинации TCP или UDP, IPv4 или IPv6».


«К списку сокетов», — поправила его Лиз. «Помнишь, что я могла прослушивать несколько сокетов?»


«А, ну да».


«Но ей нужно ещё и обрабатывать прослушивание всех „домашних“ IP, и иметь возможность находить сокет, прослушивающий IPv6 с IP IPv4».


«Ну ладно, мне надо это сдать», — сказал Тим, показав на кучу листов бумаги. «А ты закончишь свой HTTP-сервер к сроку?»


Лиз пожала плечами: «У меня ещё есть сегодня время».


Тим покачал головой, как недовольный родитель.


Лиз закатила глаза и сказала: «Беги, Тим».


«В то же время на следующей неделе?»


«Ага».


Report Page