Народ, это моя первая статья, так что задолбите меня критикой, дабы повысить качество следующих статей. :)
Вступление
Ну, начнем как и везде с определений, берите тетрадь и ручку сейчас начнется нудятина. Чтобы мы cмогли написать свой сервер, нужно для начала понимать как он вообще работает, ловите определение:
Сервер – это программное обеспечение, которое ожидает запросов клиентов и обслуживает или обрабатывает их соответственно.
Если объяснять это своими словами, представьте фургон с хот-догами(сервер), проголодавшись, вы(клиент) подходите и говорите повару, что вы хотите заказать(запрос), после чего повар обрабатывает, что вы ему сказали и начинает готовить, в конечном итоге вы получаете свой хот-дог(результат) и сытый радуетесь жизни. Для наглядности посмотри схему.
Околопрактика
Для написания сервера мы будем использовать Python и модуль Socket.
Socket позволяет нам общаться с сервером с помощью сокетов. Весь код я постараюсь пояснять, дабы ты мой дорогой читатель все понял. В конце статьи будет готовый код.
Создайте два файла в одной директории:
socket_server.py
socket_client.py
Практика
Пишем код для серверной части, так что открывайте файл socket_server.py.
Начнем с импорта модуля и создания TCP-сокета:
import socket
Далее весь код будет с комментариями:
s.bind(('localhost', 3030)) # Привязываем серверный сокет к localhost и 3030 порту.
s.listen(1) # Начинаем прослушивать входящие соединения
conn, addr = s.accept() # Метод который принимает входящее соединение.
Добавим вечный цикл, который будет считывать данные с клиентской части, и отправлять их обратно.
while True: # Создаем вечный цикл.
data = conn.recv(1024) # Получаем данные из сокета.
if not data:
break
conn.sendall(data) # Отправляем данные в сокет.
print(data.decode('utf-8')) # Выводим информацию на печать.
conn.close()
Переходим к клиентской части, весь код теперь пишем в файле socket_client.py.
Начало у клиентской части такое-же как и у серверной.
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Далее подключимся к нашему серверу и отправим сообщение "Hello. Habr!".
s.connect(('localhost', 3030)) # Подключаемся к нашему серверу.
s.sendall('Hello, Habr!'.encode('utf-8')) # Отправляем фразу.
data = s.recv(1024) #Получаем данные из сокета.
s.close()
Результат:
Заключение
Вот мы с вами и написали свой первый сервер, рад был стараться для вас, ниже будет готовый код.
socket_server.py:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 3030)) # Привязываем серверный сокет к localhost и 3030 порту.
s.listen(1) # Начинаем прослушивать входящие соединения.
conn, addr = s.accept() # Метод который принимает входящее соединение.
while True:
data = conn.recv(1024) # Получаем данные из сокета.
if not data:
break
conn.sendall(data) # Отправляем данные в сокет.
print(data.decode('utf-8'))
conn.close()
socket_client.py:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 3030)) # Подключаемся к нашему серверу.
s.sendall('Hello, Habr!'.encode('utf-8')) # Отправляем фразу.
data = s.recv(1024) #Получаем данные из сокета.
s.close()
Комментарии (13)
unstopppable
19.05.2022 07:42+1Предлагаю усложнять сервер в следующих статьях шаг за шагом. В чем косяк этого сервера - если подключиться еще одним клиентом, то он будет получать от сервера ответ только когда первый отключится... Соответственно, нужно переключать сервер между клиентами, например, используя опрос сокетов через select. Высший пилотаж - aiohttp:)
XenixFSociety Автор
19.05.2022 08:10В планах продолжать данный цикл статей. Спасибо за критику в следующих статьях думаю реализую)
artemlight
19.05.2022 09:18Как два байта переслать! (с)
Ошибка статьи в том, что вы берете слишком низкоуровневый примитив и ожидаете, что поверх него пользователь будет строить какой-то велосипед для передачи данных. В реальности уже давно написаны практически всевозможные обертки над голыми сокетами, и крайне маловероятно что кому-то понадобится ещё одна.
А ведь сервер - это не просто socket.recv(). Это дофига всего - ААА, сериализация, валидация, и много-чего-другого-ция. Да и даже socket.recv() уже мало кому интересен - слишком медленно и тяжко с асинхронностью.
TL;DR: статья называется "как построить простой дом", но начинаем с добычи глины для лепки кирпичей.
artemlight
19.05.2022 09:41+2Ну и тут выше был вопрос про сокет - поэтому отвечу вкратце.
Сокет в переводе с английского языка - разъем или розетка, проще говоря - дырка в которую можно вставить кабель. Как и в случае с обычной розеткой в стене - сама по себе она мало чем ценна, в неё нужно что-то включить. Отсюда возникает разделение на серверные (слушающие, Listening) сокеты и клиентские сокеты. И если серверный сокет - это условная розетка в стене, то клиентский сокет - это пальцы, которые мы в неё вставляем.
Фактически серверный сокет - это такой механизм в ОС, который ждет входящего подключения. И как только к нему подключаются - сразу вызывает какой-то Ваш код, который обрабатывает это подключение. Ну то есть нормальное состояние серверного сокета - ничего не делать и ждать, он потому и называется слушающим.
Клиентский же сокет - это механизм в ОС, который подключается к серверному сокету, и если подключение произошло удачно - то можно писать в сокет данные и читать из сокета данные.
Технически эта вся история реализована при помощи семейства интернет-протоколов TCP\IP. IP - это про адресацию (те самые IP-адреса, да), бывает версии 4 и 6. TCP - это про надежность доставки (там есть два варианта - TCP медленнее и с гарантией и UDP быстро, но как повезет). На уровне TCP сделано ещё много чего полезного - например Congestion Control, которые определяют честность разделения лимитированного по ширине канала между несколькими одновременными соединениями.
Чтобы не запихивать весь этот функционал в какую-то одну огромную библиотеку - умные люди придумали модель OSI. Это такая референсная и недостижимая (как социализм) штука, идея которой заключается в том, что сетевое взаимодействие между двумя системами делится на абстрактные "уровни", каждый из которых полностью реализует свою задачу. Условно говоря - протоколу TCP на уровне L3 по идее не важно, что у него за физический носитель на уровне L1 - медный кабель, оптоволокно или Wi-Fi. (на самом деле это совсем не так, но для 70-х годов прошлого века идея тотальной унификации всего на свете выглядела прогрессивной). Много полезной инфы об этом можно почерпнуть из старого курса Cisco ICND, и несмотря на его почтенный возраст он имхо обязателен к прочтению всем, кто пытается что-то соорудить из сокетов.
16tomatotonns
19.05.2022 15:56+2Ответ хороший, но суть в том, что по хорошему он должен быть в теле статьи :)
ivymike
21.05.2022 00:07Почему все прицепились к какому-то сокету...
Изначально проведена не самая корректная аналогия: фургон с булками - это БД, сервер - повар.
souls_arch
21.05.2022 04:58Не очень удачная абстракция с фургоном и поваром. Имхо, если любите покушать, то - ресторан.
16tomatotonns
Очень просто. Я бы сказал что чересчур :)
Во-первых, используемый вами протокол TCP безусловно хорош, но сервер который принимает только одно соединение, извлекает из него всё что в него отправили и тут же закрывается это немножко малоюзабельно. Что-то на уровне
> Как в языке Python сделать Hello World? Всего лишь print("Hello World")! Ничего другого не надо!
То есть это прямо скажем не сервер, а "Как прослушать порт, и принять из него что-нибудь", притом его легко полностью парализовать не завершая соединение :)
Первое что имеет любой сервер:
- Некоторый менеджер соединений. В него мы добавляем клиентов.
- Некоторые абстракции над sock'ами, условные клиенты, которыми мы такие, желательно в неблокирующем виде, принимаем все присланные данные всеми sock'ами (сколько прислали столько неблокирующе и считываем, и если сообщение не закончено то переходим к следующему), отключаем клиентов по таймаутам, проверяем что они нам что-то прислали и выполняем на присланные сообщения какие-нибудь callback'и.
- Некоторый абстрактный формат сообщений. Протокол TCP ориентирован не на сообщения, а на потоки, и ему нужен дополнительный формат для разделения сообщений, вы же не хотите использовать одно целое соединение чтобы прислать одно сообщение, когда их может быть пачка? Простейший формат сообщений -- message + "\r\n", и эти "\r\n" работают разделителями сообщений. Но у него проблема с самими данными, их приходится паковать в какой-нибудь base64 чтобы из-за тела сообщения содержащего эти самые "\r\n" мессага не распилилась, поэтому чуть более сложный вариант, например
text = "abcdef"
formatted = '{0:04d}{1:s}'.format(len(text), text)
> '0006abcdef'
Приведёт к уникальной возможности указывать длину сообщения перед самим сообщением, и корректно их сепарировать по длине, принимая и отправляя их сразу пачками (хотя появляется лимит на длину, которую можно указать произвольной, но она внесёт некоторый оверхед). Я сам использую похожие прикладные протоколы для своих небольших нужд, где HTTP слишком тяжеловесен, и что-то похожее используется в самом HTTP, только там чуть более комплексный заголовок, содержащий не только длину а сразу множество опций, и само тело там может быть а может не быть, но это уже частности.
И вот чего-то подобного хотелось бы видеть в статье а не только "Мы открыли сокет! Мы отправили в него строчку и приняли! Ура!". Статья неплоха как вводная часть, перед дальнейшей кухней вроде той что я описал, но ПОСЛЕ теоретической части которой тоже не хватает: что такое сервер? что такое сокет? что такое порт? что такое tcp/udp/ip? Почему localhost и что он означает, во что трансформируется и почему? Ваше описание избыточно упрощено, и мало имеет отношения к действительности :)
Ну и в третьих, хаб "Серверное администрирование" немножко кажется тоже лишним, здесь примерно ничего про администрирование, про роутинг, пробитие или использование портов, фаерволы и так далее :)
Pinguin
Присоединяюсь насчёт сокета. Всё делается через сокет, а что такое сокет -- непонятно.
artemlight
я там чуть ниже ответил на Ваш вопрос :)
v2ray
da nedavnyaya statya pro kolichestvo soedinenii na server byla dostatichno information chego tam proishodit + statya pro server-eto-script = chtoby obyasnit chto povar sdelal s sobakoi da bez vsyakih grishkovec
XenixFSociety Автор
Спасибо большое за критику, в дальнейшем буду развивать эту ветку статей.
Kreastr
Получить подключение, прочитать запрос и отправить ответ это и есть принцип работы, наверное, самого распространенного типа серверов - веб серверов.
В текущем сценарии менеджер соединений не обязателен, потому что нет параллельной работы с соединениями. А на уровне получить запрос от клиента и подождать пока сервер примет соединение оно реализовано на уровне ОС.
Вообще КМК архитектура веб-серверов плавно ползет именно к таким супер простым серверам запущенным в нескольких копиях с балансировкой нагрузки через reverse proxy.
Но теории добавить стоит, конечно. И дампы траффика разобрать.