Вот мы и добрались до самого главного — обсуждения взаимодействия между сервером и клиентом. Как они соединяются друг с другом? Какие протоколы нужно использовать и что такое сокет?
Протокол
Сетевое программирование — это та его часть, которая подразумевает обмен данными между сервером и клиентом. При этом клиент и сервер — это не обязательно разные физические сервера, компьютеры, гаджеты. Клиент-серверная архитектура может быть реализована логически, на одном «железе», в одной комнате, за одним столом. Для взаимодействия между ними используется некоторый свод правил по транспортировке, инкапсуляции, очередности пакетов и сетевого взаимодействия — это называется протоколом передачи данных. Приложения, написанные на Python обычно используют протоколы транспортного уровня TCP и UDP.
TCP используют, чтобы формировать между компьютерами двусторонний канал обмена данными. Благодаря TCP пакеты гарантированно доставляются с соблюдением порядка их очередности, с автоматическим разбиением данных на пакеты и контролем их передачи. В то же время TCP работает медленно, так как потерянные пакеты многократно повторно отправляются, а операций, выполняемых над пакетами, слишком много.
Протокол UDP — низкоуровневый. С его помощью компьютеры могут отправлять и получать информацию в виде отдельных пакетов, не создавая логическое соединение. В отличие от TCP, взаимодействия по протоколу UDP не отличаются надежностью. Это усложняет управление ими в приложениях, в которых при обмене информацией нужны гарантии. Поэтому большинство интернет-приложений используют TCP.
[adace-ad id=»3470″]Сокеты
С протоколами разобрались. Но как этими протоколами пользоваться нашим клиентам и сервером? Это взаимодействие осуществляется с помощью сокета. Сокет — абстрактный объект, представляющий конечную точку соединения. . С сокетом в Питоне можно работать, как с файлом — считывать его и получать данные. Сокет содержит в себе два параметра: IP-адрес и порт.
Сервер, принимая соединение присваивает своему сокету определенный порт. Порт — число в заголовках пакетов TCP, UDP, указывающее, для какого приложения в системе предназначен данный IP-пакет. Использовать порты с номерами 0-1023 нельзя — они зарезервированы под служебные сетевые протоколы (например, 21 — FTP, 80 — HTTP и т.д.). Клиент, отправляя данные тоже должен создать свой сокет. Два сокета с обоих сторон создают виртуальное соединение по которому будет идти передача данных. Нужно отметить, что при работе с протоколом TCP, создается два сокета: один из них — слушающий (listen). Он переходит в режим ожидания и активизируется при появлении нового соединения. При этом можно проверять актуальные активные соединения, установить периодичность операции. Второй — сокет для обмена данных с клиентом (accept). Это два разных сокета, не путайте
[adace-ad id=»3482″]
Работа с сокетами в Python
Для создания сокетов в питоне используется модуль socket. В нем так же имеются методы для , установление и закрытие соединения, отправку данных по сети и их получение и другие операции.
Общие | Серверные | Клиентские |
socket — создать сокет | bind — привязать сокет к IP-адресу и порту машины | connect — установить соединение |
send — передать данные | listen — просигнализировать о готовности принимать соединения | |
recv — получить данные | accept — принять запрос на установку соединения | |
close — закрыть соединение |
Работа ТСР протокола
Чтобы понять, как с сокетом работает протокол ТСР, посмотрим на изображение ниже. Пояснение будет в коде программы (для примера мы отправляем клиенту текущее время)
Серверная часть:
Функция socket() инициализирует создание сокета. В ней передаются два параметра: communication domain и type of socket. AF_INET — это коммуникационный домен, который задает сетевую направленность нашему сокету. Тип сокета — SOCK_STREAM — он определяет сокет как потоковый, то есть реализующий последовательный, надежный двусторонний поток байтов по протоколу ТСР. Создалась конечная точка подключения — сокет. Функция socket() возвращает нам файловый дескриптор, который позволяет работать с сокетом, как с файлом — записывать и считывать данные в/из него. Метод encode применяется здесь, т.к. данные нужно отправлять по сети в виде байтов.
[code]# серверная часть
from socket import *
import time
s = socket(AF_INET, SOCK_STREAM) # Создается сокет протокола TCP
s.bind((», 10000)) # Присваиваем ему порт 10000
s.listen(10) # Максимальное количество одновременных запросов
while True:
client, addr = s.accept() # акцептим запрос на соединение
print(client)
print(«Запрос на соединение от %s» % str(addr))
timestr = time.ctime(time.time()) + «\n»
client.send(timestr.encode(‘utf-8’)) #передаем данные, предварительно упаковав их в байты
client.close() # закрываем соединение
Если вы работаете в среде программирования, то разрешите вашему серверу работать в вашей локальной сети:
Клиентская часть
[adace-ad id=»3475″]
Клиент устанавливает соединение с помощью метода connect (в нашем случае, localhost, т.к. сервер и клиент на одной машине). Как мы уже знаем, сервер отправляет нам последовательность кодированных байтов — наша задача декодировать их в строки юникода
[code]# клиентская часть
from socket import *
s = socket(AF_INET, SOCK_STREAM) # создаем аналогичный сокет, как у сервера
s.connect((‘localhost’, 10000)) # коннектимся с сервером
tm = s.recv(1024) # Принимаем не более 1024 байта данных
s.close() # закрываем соединение
print(«Текущее время: %s» % tm.decode(‘utf-8’)) # получаем данные, декодировав байты
Результат клиентской части (после запуска сервера):
Результат серверной части (после подключения клиента):
Как происходит кодирование/декодирование данных?
Строки, байты, изменяемые строки байтов:
Код / данные | Результат print(type()) |
i= ‘Data’ | <class ‘str’> — строка |
bi = b’Data’ | <class ‘bytes’> — строка байтов |
ba = bytearray(bi) | <class ‘bytearray’> — изменяемая строка байтов |
i2 = bi.decode(‘cp1251’) | <class ‘str’> — из строки байт в unicode-строку |
bi2 = i.encode(‘koi8-r’) | <class ‘bytes’> — из unicode-строки в строку байт |
ba2 = bytearray(i, ‘utf-8’) | <class ‘bytearray’> — из unicode-строки в массив байтов |
Отправка и приём сообщений
В качестве примера можно рассмотреть простой механизм отправки сообщений от клиента к серверу и обратно. Сервер получает приветствие от клиента и отправляет ответ клиенту. Клиент, соответственно, отправляет приветствие серверу и получает от него ответ
Серверная часть:
[code]# клиентская часть
from socket import *
import time
s = socket(AF_INET, SOCK_STREAM) # Создаем сокет TCP
s.bind((», 11111)) # Присваиваем порт 11111
s.listen(5) # пять запросов максимум
while True: # пока выполняется условие (пока есть запросы на подключение от клиента)
client, addr = s.accept() # принимаем запрос на соединение
data = client.recv(1000000) # указываем максимальное количество данных, которое можно принять от клиента
print(‘Месседж: ‘, data.decode(‘utf-8’), ‘, пришло от него: ‘, addr)
msg = ‘Купи виски’
client.send(msg.encode(‘utf-8’)) #передаем данные, предварительно упаковав их в байты
client.close()
Клиентская часть:
[code]# клиентская часть
from socket import *
s = socket(AF_INET, SOCK_STREAM) # Создаем сокет TCP
s.connect((‘localhost’, 11111)) # коннект к серверу
msg = ‘Привет, сервер’
s.send(msg.encode(‘utf-8’)) #передаем данные, предварительно упаковав их в байты
data = s.recv(1000000) #получаем не более 1000000 байт
print(‘Сообщение от сервера: ‘, data.decode(‘utf-8’), ‘, длиной ‘, len(data), ‘ байт’) #получаем сообщение от сервера, декодировав байты юникод
s.close()
JSON Instant Messaging
JIM — протокол для обмена данных между клиентом и сервером, который работает через TCP-сокеты (SOCK_STREAM) и передачу JSON-объектов. Все сетевые операции проходят в байтовом представлении. Данные в JSON-формате в протоколе JIM всегда содержат два поля: action и time.
Поле action задает характер действия — авторизация или отправка сообщения и т.п.
Поле time показывает время отправки данного сообщение (используется UNIX-время — определяется как количество секунд, прошедших с полуночи (00:00:00 UTC) 1 января 1970 года)
JSON-объекты в JIM имеют ограничение по количеству символов. Например, сам текст сообщения ограничен 500 символами. Остальные ограничения:
Поле action — «Действие», 15 символов
Поле response — «Код ответа сервера», 3 символа (цифры)
Поле name — «Имя пользователя или название чата». Здесь максимум 25 символов;
Весь скомпилированный JSON-объект должен уложиться в 640 символов.
Аутентификация
Для того, чтобы инициализировать процесс аутентификации, надо создать такой JSON-объект:
[code]{
«action»: «authenticate»,
«time»: <unix timestamp>,
«user»: {
«account_name»: «digital2»,
«password»: «superpythontools»
}
}
Ответы сервера будут содержать поле response, и может быть еще одно (необязательное) поле alert/error с текстом ошибки.
[code]{
«response»: код,
«alert»: текст
}
Подключение, отключение, авторизация
Авторизация — не обязательное условие при использовании JIM, т.е. его могут использовать любые пользователи. Если авторизация будет нужна на каком-то этапе, сервер выдаст алерт с кодом 401. Если аутентификация всё же нужна, то сервер может выдать один из нескольких вариантов респонзов:
[code]{
«response»: 200,
«alert»:»Необязательное сообщение/уведомление»
}
{
«response»: 402,
«error»: «This could be «wrong password» or «no account with that name»»
}
{
«response»: 409,
«error»: «Someone is already connected with the given user name»
}
Отключение от сервера должно сопровождаться сообщением “quit”:
[code]{
«action»: «quit»
}
В сети/ не в сети
Для того, чтобы обозначить своё присутствие в «онлайне», клиент должен отправлять специальное presence сообщение с полем type
[code]{
«action»: «presence»,
«time»: <unix timestamp>,
«type»: «status»,
«user»: {
«account_name»: «digital2»,
«status»: «User is online»
}
}
В свою очередь, сервер посылает специальный probe-запрос для проверки доступности клиента:
[code]{
«action»: «probe»,
«time»: <unix timestamp>,
}
Алерты и ошибки сервера
Код | Что означает? |
1xx — информационные сообщения | 100 — базовое уведомление; 101 — важное уведомление. |
2xx — успешное завершение: | 200 — OK; 201 (created) — объект создан; 202 (accepted) — подтверждение. |
4xx — ошибка на стороне клиента: | 400 — неправильный запрос/JSON-объект; 401 — не авторизован; 402 — неправильный логин/пароль; 403 (forbidden) — пользователь заблокирован; 404 (not found) — пользователь/чат отсутствует на сервере; 409 (conflict) — уже имеется подключение с указанным логином; 410 (gone) — адресат существует, но недоступен (offline). |
5xx — ошибка на стороне сервера: | 500 — ошибка сервера. |
Обмен сообщениями
Экшн msg для сервера означает одно — ему надо передать сообщение адресату из поля to. Если не задана кодировка в поле encoding, то сервер будет считывать данные в ascii-формате
[code]{
«action»: «msg»,
«time»: <unix timestamp>,
«to»: «account_name»,
«from»: «account_name»,
«encoding»: «ascii»,
«message»: «message»
}
Сообщение в «чат»
Тоже самое, что и отправка обычному пользователю, только в поле to ставится решетка с названием чатрума
[code]{
«action»: «msg»,
«time»: <unix timestamp>,
«to»: «#room_name»,
«from»: «account_name»,
«message»: «Hello World»
}
Чтобы зайти в чат:
[code]{
«action»: «join»,
«time»: <unix timestamp>,
«room»: «#room_name»
}
Выйти из чата:
[code]{
«action»: «leave»,
«time»: <unix timestamp>,
«room»: «#room_name»
}
Метод Actions
Методы протокола «действия» в JIM:
“action”: “presence” — присутствие. Сервисное сообщение для извещения сервера о присутствии клиента online;
“action”: “prоbe” — проверка присутствия. Сервисное сообщение от сервера для проверки присутствии клиента online;
“action”: “msg” — простое сообщение пользователю или в чат;
“action”: “quit” — отключение от сервера;
“action”: “authenticate” — авторизация на сервере;
“action”: “join” — присоединиться к чату;
“action”: “leave” — покинуть чат.
Настоятельно рекомендуем ознакомиться с сокетами на Python в материале Хабра — https://habr.com/ru/post/149077/
