-
Notifications
You must be signed in to change notification settings - Fork 0
/
protocol.txt
343 lines (251 loc) · 23.4 KB
/
protocol.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
Описание протокола, используемого в нашем мега-курсаче
======================================================
DISCLAIMER: описание ведется на уровне "ну идея понятна, да?". Для полноценного
описания для РПЗ следует либо нагуглить ГОСТ, либо какие-нить UML-штуки, либо
Алешин даст пример, либо... короче, что-нить придумаем.
Назовем наш протокол ENCP (Experimental Network Chat Protocol).
В обмене по протоколу участвует один Сервер и несколько Клиентов.
/------\
/----------/[ сервер ]--------\
/ / \------/ \
[ клиент ] / | \ \[ клиент ]
[ клиент ] | [ клиент ]
[ клиент ]
Ну, в ASCII-графике я не эксперт...
Вообще-то, ENCP - это не один протокол, а стек из двух протоколов (да, понты, понты):
ENCP/C (Chat) и ENCP/T (Transport). Первый определяет пакеты сообщений, на которых
строится реализация чата, а второй - способ передачи этих сообщений.
----------------------------
ENCP/T
----------------------------
1. Данные, передаваемые по протоколу.
На самом высоком уровне, приложения (реализующие ENCP/C) считают, что используют
протокол ENCP/T для передачи объектов.
[ прога1 ] -- <объект> --> ...канал ENCP/T... -- <объект> --> [ прога2 ]
ENCP/T не специфицирует, какие именно объекты передавать. Ему пофигу. Этим занимается
ENCP/C. Задача ENCP/T - молча передать, что ему вручили, и не вякать.
Под объектами тут понимаются экземпляры типов, нативных для используемых языков
программирования. Прежде всего - словарей, у которых ключи и значения являются
строками. Но значения могут быть также и числами, и массивами строк, и массивами
словарей, у которых ключи и значения...
Короче!!!
Все, что может быть представлено в нотации JSON.
Перейдем на уровень ниже, и увидим, что ENCP/T как раз и занимается сериализацией
и десериализацией вверенных ему объектов в JSON.
[ прога1 ] -- <объект> --+ +-- <объект> --> [ прога2 ]
| |
сериализация десериализация
| ^
V |
JSON --> ...канал...--> JSON
Что такое канал на этой диаграмме?
Канал - это веб-сокет (WebSocket).
Веб-сокеты занимаются передачей текстовых строк в UTF-8. Сериализация в JSON
порождает текстовую строку ASCII, а мы знаем, что любая строка ASCII является
строкой UTF-8. Это так, к слову.
Еще к слову: строки, передающиеся по WebSocket, окружаются ограничителями -
байтами 0x00 в начале и 0xFF в конце. Джаваскриптовое API делает это само,
а Питон, не знающий, что такое веб-сокеты, не делает. Поэтому эти байты добавляем и
убираем мы сами. Конечно, можно написать уровень абстракции и сказать, что так оно
и было))
Ну и процедуру рукопожатия (handshake) протокола веб-сокетов мы на Питоне делаем
ручками. Ту самую процедуру, где нужно делить на число пробелов.
Вот.
----------------------------
ENCP/C
----------------------------
Протокол ENCP/C использует ENCP/T для передачи объектов между клиентом и сервером
для достижения высшей цели - обмена сообщениями в виде чата.
Передающиеся объекты, в общем случае, являются словарями. Все объекты JavaScript
и так уже являются словарями, а Питон имеет соответствующий тип данных.
Как минимум, передающиеся объекты-словари должны иметь поле 'type', указывающее на тип
сообщения. Предполагается, что функция-диспетчер (привет, К.Л.! привет, А.В.!)
по значению этого поля определит, какой функции второго уровня передавать объект,
а та уже сможет разобраться с остальными полями.
Некоторые сообщения содержат еще и поле subtype для дальнейшего мультиплексирования.
Тип сообщения определяется парой значений полей type и subtype (если subtype есть);
назовем эти два поля типообразующими.
Типообразующие поля присутствуют всегда; если в тексте встретится предложение
"клиент формирует сообщение с полями X и Y", это значит, что кроме полей X и Y
в сообщении есть поле type, и, возможно, subtype.
Если мы когда-нибудь доберемся до реализации нескольких чат-комнат на одном сервере,
то у всех сообщений будет обязательное поле "room_id". Текст данного документа
написан без учета возможности наличия нескольких чат-комнат; в частности, нет
описания сообщений подключения к конкретной чат-комнате, выхода из комнаты,
получения списка комнат...
Некоторые сообщения идут только от клиента к серверу или только от сервера к клиенту.
Некоторые ходят в обоих направлениях, но имеют различный набор полей в зависимости
от направления.
Общая схема для большинства сообщений такова: клиент посылает сообщение серверу,
сервер дополняет его некоторыми полями (например именем отправителя) и рассылает
всем клиентам, подключенным к серверу (в т.ч. тому, от которого оно только что
пришло!)
Каждому типу сообщений соответствует код - буква M и порядковый номер по
приведенному ниже списку.
Для краткости фраза "формирует сообщение М1" сокращена в данном тексте до
"формирует М1".
Сообщения, реализованные в настоящий момент:
M1. Текстовое сообщение
{ 'type': 'text', 'sender': sender, 'value': value }
sender: имя (ник) отправителя. Изначально отсутствует, добавляется сервером.
value: строка-текст сообщения.
Жизненный цикл сообщения:
1. Пользователь набирает сообщение и жмет Enter
2. Программа-клиент формирует M1 с полем value, содержащем введенный текст,
и отправляет серверу.
3. Сервер добавляет поле sender, содержащее имя (ник) отправителя,
опционально проводит валидацию value (чтобы не включали злобный JavaScript
и т.п.) и отправляет всем клиентам.
4. Клиент, получив M1, использует его содержимое для добавления строки в
лог чата.
Комментарии:
- возможно, данное сообщение будет дополнено полем 'private_to',
чтобы реализовать передачу приватных сообщений. В таком случае сервер
на шаге 3 передаст сообщение только клиенту, указанному в этом поле,
вместо массовой рассылки.
М2. Уведомление о новом участнике чата
{ 'type': 'notify', 'subtype': 'user_joined', 'user': user }
user: имя (ник) нового участника
Жизненный цикл сообщения:
1. Программа-клиент нового участника производит первое (за сеанс) обращение
к серверу. Выполняется процедура рукопожатия WebSocket.
2. По завершении рукопожатия сервер присваивает участнику имя (ник),
формирует M2 с этим ником и отправляет всем клиентам.
3. Клиент, получив M2, добавляет в лог чата фразу "к нам пришел %user%" и
добавляет нового участника в отображаемый список сидящих в чат-комнате
людей.
Комментарии:
- скорее всего, процедура задания имени будет переработана. Вероятный
сценарий: Клиент после рукопожатия отправляет серверу сообщение M4
"меня зовут %user%", после чего сервер выполняет рассылку М2. До этого
момента клиент не считается участником чата.
M3. Уведомление об уходе участника
{ 'type': 'notify', 'subtype': 'user_left', 'user': user }
user: имя (ник) покинувшего чат-комнату участника
Жизненный цикл сообщения:
1. Участник закрывает страницу в браузере или программу-клиент, таким образом
обрывая подключение по веб-сокету.
2. Сервер фиксирует закрытие веб-сокета, формирует M3 с именем участника,
сокет которого закрылся, и рассылает всем клиентам.
3. Клиент, получив M3, добавляет в лог чата фразу "нас покинул %user%"
и удаляет user'а из отображаемого списка участников.
M4. Запрос о задании имени
{ 'type': 'set-name', 'new_name': new_name }
new_name: новое имя участника
Жизненный цикл сообщения:
1. Пользователь решает сменить свой ник и вводит новый ник в программу-клиент.
2. Программа-клиент формирует М4 с полем new_name и отправляет серверу.
3. Сервер изменяет имя клиента в своем внутреннем списке и смотрит, было ли
отправлено сообщение M2 для данного клиента (является ли клиент
участником чата) - см. комментарий к М2
4а Если клиент не является участником чата, то сервер формирует сообщение М2
с новым именем клиента; далее goto жизненный цикл М2.
# замечание: такого не может быть после введения регистрации/авторизации. В коде эта альтернатива игнорируется.
4б Если клиент является участником чата, то сервер формирует сообщение М8.
M5. Запрос списка участников.
{ 'type': 'roommates', 'list': roommates }
roommates - массив строк - ников участников чата. Добавляется сервером.
TODO : отправлять не массив строк, а массив объектов {'nick': nick, 'color': color},
чтобы клиент мог знать, кто каким цветом рисует.
Жизненный цикл сообщения:
1. Программа-клиент раз в 30 секунд, а так же при приходе сообщения типа notify,
решает, что ей неплохо бы синхронизировать свой список участников чата
с серверным.
2. Программа-клиент формирует М5 без полей (кроме 'type', разумеется) и
отправляет серверу.
3. Сервер дополняет M5 полем roommates и отправляет сообщение клиенту, запросив-
шему список (а не всем клиентам!)
4. Клиент, получив М5 со списком, использует его для обновления отображаемого
списка участников чата.
Кроме того, при подключении нового клиента сервер формирует и отправляет ему
сообщение М5 со списком.
М6. Графическое сообщение
должно содержать изображение, нарисованное клиентом, целиком.
Как мы его разместим в юникодной строке - это тот еще вопрос.
Скорее всего, реализовано не будет.
М7. Изменение общей доски для рисования
{ 'type': 'public_drawing', 'sender': sender, 'commands': [drawing_command, ...] }
Где sender добавляется сервером,
drawing_command = { 'color': color, 'tool': tool, 'param': param },
или drawing_command = 'clearall'
где tool - имя инструмента ('pencil', 'line', 'rect', ...),
а param = {'p1': {'x': x1, 'y': y1}, 'p2': {'x': x2, 'y': y2}}.
Интерпретация param и color в зависимости от tool:
tool | интерпретация x1, y1, x2, y2, color
---------------------------------------------
pencil | рисуется отрезок из точки (x1,y1) в точку (x2,y2) цветом color
line | --//--
rect | рисуется, но не закрашивается, прямоугольник с противоположными
| углами в точках (x1,y1) и (x2,y2)
fillrect | то же, но прямоугольник закрашивается
Жизненный цикл A:
1. Пользователь рисует закорючку на доске рисования.
В процессе рисования клиентский буфер команд (draw_history) пополняется
командами рисования.
2. По завершении операции рисования (пользователь отпустил кнопку мыши)
программа-клиент формирует сообщение М7 с содержимым буфера команд
(одно сообщение содержит массив, представляющий буфер целиком),
отправляет М7 на сервер и очищает буфер.
3. Сервер, получив М7, дополняет его именем пользователя (sender) и рассылает
всем клиентам, кроме клиента-отправителя. Затем сервер очищает поле
commands (commands = []) и отправляет М7 клиенту-отправителю.
Сервер сохраняет команды в истории команд рисования,
чтобы выдавать новым участникам уже готовую картинку.
Серверная история команд очищается, если commands содержит 'clearall'.
4. Клиент, получив М7 от сервера, выполняет содержащиеся в нем
команды рисования одна за другой, применяя их к общей доске для рисования.
Дополнительно, клиент может отобразить имя пользователя, указанное в
поступившем сообщении, в надписи "Автор последней закорючки".
Жизненный цикл Б:
1. При подключении к чату нового участника пользователь отправляет ему
всю серверную историю рисования в виде сообщения М7, но без поля
sender.
2. См. п.4 цикла А.
М8. Уведомление о смене ника
{ 'type': 'notify', 'subtype': 'user_renamed', 'new_nick': new_nick,
'old_nick': old_nick }
Жизненный цикл:
1. Сервер, получив сообщение М4, в пункте ж.цикла 4б формирует М8 и рассылает
всем клиентам.
2. Клиент, получив сообщение М4, добавляет в лог чата фразу "%old_name% теперь
известен под именем %new_name%" и изменяет отображаемый список участников
соответствующим образом.
М9. Отправление только что вошедшему в чат пользователю последних N сообщений
{'type': 'notify', 'subtype': 'last_messages', 'messages':self.websocket.lastNMessages}
где lastNMessages массив словарей:
{'sender': datagram['sender'], 'value': datagram['value'], 'time': datetime.datetime.now().strftime('%H:%M:%S')}
Жизненный цикл:
1. Сервер, узнав, что к конференции присоединился пользователь отсылает ему массив последних
10 сообщений.
2. Клиент, получив сообщение М9, уведомляет пользователся о новых сообщениях и выводит
их в окно чата.
М10. Вход на сервер
{'type': 'login', 'nick': chat_nick, 'password': password}
Жизненный цикл:
1. Пользователь решает подключиться к чату. Он вводит адрес сервера, логин,
пароль, и жмет "Войти".
2. Клиент устанавливает WebSocket-соединение с сервером. После успешного
установления и проведения handshake (т.е. в обработчике onopen) клиент
формирует и отправляет на сервер сообщение М10.
3. Сервер, получив М10, медитирует над его содержимым; результатом медитации
является сообщение М11.
М11. Результат входа на сервер
{'type': 'login_result', 'logged_in': logged_in, 'is_super': is_super, 'color': color, 'message': message}
1. Сервер получает М10.
2. Выполняются следующие проверки:
(1) есть ли указанный ник в списке зарегистрированных пользователей;
(2) если (1), то правильно ли указан пароль;
(3) является ли уже этот пользователь участником чат-комнаты.
3. Если (1) неверно, сервер выполняет регистрацию пользователя в своей базе,
сохраняя ник и пароль. После этого считается, что условия (1) и (2) выполнены.
4. Вход на сервер считается успешным при одновременном выполнении первых двух
условий и невыполнении третьего.
5а. Если вход успешен, то сервер формирует М11 с полем logged_in = True.
Поле color содержит значение цвета, ассоциируемого с пользователем.
Сервер отправляет М11 клиенту; затем сервер выполняет прочие действия,
связанные с входом клиента в чат, такие как отправка последних сообщений.
5б. Если вход не успешен, то сервер формирует М11 с logged_in = False.
В поле message заносится причина отказа во входе (эту строку увидит
пользователь). Сервер отправляет клиенту сформированное сообщение и
разрывает связь.
Продолжение следует)