Add week 3 materials
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
## 3. Протокол HTTP, REST и веб-сервисы.
|
||||
|
||||
### Лекция
|
||||
|
||||
- [Презентация](03-http.pdf)
|
||||
- См. также материалы в конце
|
||||
|
||||
### Семинар
|
||||
|
||||
- [Материалы семинара](seminar/readme.md)
|
||||
- [Пример WebSocket-сервера](seminar/websockets/README.md) — факультативный пример, который не разбирался на семинаре.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Семинар 3. HTTP на практике
|
||||
|
||||
На семинаре мы проследили путь HTTP-запроса: от `curl`, браузера или Python-клиента через Nginx до Flask-приложения и обратно. Ниже — основные понятия и способы повторить демонстрации на [стенде](website/docker-compose.yaml).
|
||||
|
||||
## HTTP: запрос и ответ
|
||||
|
||||
**Запрос** содержит метод, адрес ресурса (URI), версию HTTP, заголовки и, при необходимости, тело. **Ответ** содержит версию HTTP, код состояния, заголовки и тело. Пустая строка отделяет заголовки от тела.
|
||||
|
||||
На запущенном стенде выполните:
|
||||
|
||||
```bash
|
||||
curl -v http://localhost:8080/const
|
||||
```
|
||||
|
||||
В подробном выводе `curl` строки с `>` относятся к запросу, с `<` — к ответу, а с `*` описывают работу самого клиента. `/const` возвращает заранее заданный текст непосредственно из Nginx.
|
||||
|
||||
Например, при `curl -4sv http://localhost:8080/` на запущенном стенде получили такой фрагмент вывода:
|
||||
|
||||
```text
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> User-Agent: curl/8.7.1
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/1.1 200 OK
|
||||
< Server: nginx/1.31.6
|
||||
< Content-Type: text/html; charset=utf-8
|
||||
< Content-Length: 17
|
||||
< Connection: keep-alive
|
||||
< X-Upstream: 172.18.0.2:5002
|
||||
<
|
||||
Hello from app v2
|
||||
```
|
||||
|
||||
Здесь Nginx передал запрос одному из Flask-серверов и вернул его ответ клиенту. `X-Upstream` показывает адрес выбранного сервера внутри сети Docker; при другом запуске адрес, версия приложения и версия Nginx могут отличаться. Флаги `-4` и `-s` в примере убирают попытку соединиться по IPv6 и индикатор прогресса, а `-v` показывает обмен запросом и ответом.
|
||||
|
||||
Заголовки, которые разбирали на занятии:
|
||||
|
||||
| Заголовок | Где смотреть | Что показывает |
|
||||
| --- | --- | --- |
|
||||
| `Host` | Запрос | К какому сайту или приложению обращается клиент, в том числе когда один адрес и порт обслуживают несколько сайтов. |
|
||||
| `User-Agent` | Запрос | Сведения о клиенте, например `curl`. |
|
||||
| `Accept` | Запрос | Какие форматы ответа клиент готов принять. |
|
||||
| `Accept-Encoding` | Запрос браузера | Какие способы сжатия ответа клиент поддерживает. |
|
||||
| `Server` | Ответ | Какой сервер отправил ответ; в демонстрации через proxy виден Nginx. |
|
||||
| `Content-Type` | Ответ | Формат тела ответа, например обычный текст, HTML или JSON. |
|
||||
| `Connection: keep-alive` | Ответ в примере | TCP-соединение можно использовать повторно, не устанавливая его заново перед каждым запросом. |
|
||||
|
||||
**Браузер тоже HTTP-клиент.** Во вкладке **Network** в DevTools найдите запрос к `/const` и сравните его заголовки и ответ с выводом `curl`. Браузер может посылать дополнительные заголовки: два клиента не обязаны формировать запрос одинаково.
|
||||
|
||||
## HTTP запросы из кода
|
||||
|
||||
На примере Python-библиотеки `requests` разобрали GET-запрос, query-параметры, timeout и проверку статуса.
|
||||
|
||||
- **Query-параметры** — пары «ключ — значение» после `?` в URI. Передавайте их через аргумент `params`, а не собирайте URI вручную: символ `&` внутри значения иначе можно принять за разделитель параметров.
|
||||
- `timeout=(1, 3)` в показанном примере задаёт 1 секунду на установление соединения и 3 секунды на ожидание данных при чтении ответа. Второе число — не общий предел времени на получение всего ответа: пока сервер регулярно присылает данные, запрос может длиться дольше. Без timeout вызов зависимого сервиса может ждать слишком долго.
|
||||
- `raise_for_status()` помогает обнаружить ответ с ошибочным HTTP-статусом и обработать его в коде.
|
||||
- Если после timeout или ошибки зависимого сервиса нужных данных нет, приложение может вернуть заранее предусмотренный **fallback**. На семинаре обсуждали fallback-текст и запасную картинку кота.
|
||||
|
||||
## Что происходит в Flask-приложении
|
||||
|
||||
В [приложении стенда](website/kittens/app.py) Flask связывает пути `/` и `/kittens` с функциями-обработчиками через `@app.route(...)`.
|
||||
|
||||
- `/` возвращает текст с версией приложения: `v1` или `v2`.
|
||||
- `/kittens` запрашивает JSON у внешнего Cat API, извлекает URL изображения и подставляет его в [HTML-шаблон](website/kittens/templates/index.html) через `render_template`.
|
||||
- Получив HTML, браузер делает **ещё один HTTP-запрос** за самой картинкой. Оба обращения можно увидеть в DevTools Network.
|
||||
|
||||
Этот пример не рассматривали как готовое production-приложение. Исходящий запрос к Cat API сделан без явного timeout и проверки HTTP-статуса. Если внешний сервис зависнет, ответит ошибкой или пришлёт неожиданные данные, нужно обработать сбой. В качестве возможного ответа на семинаре предложили заранее сохранённую картинку.
|
||||
|
||||
## Как устроен демонстрационный стенд
|
||||
|
||||
[Docker Compose](website/docker-compose.yaml) запускает два экземпляра Flask-приложения и Nginx. Запись `порт_хоста:порт_контейнера` означает, что слева указан порт для обращения с вашей машины, справа — порт внутри контейнера.
|
||||
|
||||
| Сервис | Внутри контейнера | С вашей машины | Назначение |
|
||||
| --- | --- | --- | --- |
|
||||
| `server1` | `5001` | `9001` | Flask-приложение с `APP_VERSION=v1`. |
|
||||
| `server2` | `5002` | `9002` | То же приложение с `APP_VERSION=v2`. |
|
||||
| `proxy` | `80` | `8080` | Nginx: proxy, redirect и `/const`. |
|
||||
| `proxy` | `10000` | `8081` | Nginx: раздача файлов из `static`. |
|
||||
|
||||
Compose подключает [конфигурацию Nginx](website/nginx.conf) и каталог `static` внутрь контейнера `proxy`. Суффикс `:ro` у файла конфигурации задаёт доступ только для чтения.
|
||||
|
||||
Чтобы поднять стенд, перейдите из корня репозитория в каталог с `docker-compose.yaml`. Понадобятся работающий Docker и команда `docker compose`:
|
||||
|
||||
```bash
|
||||
cd materials/03-http/seminar/website
|
||||
docker compose up --build -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
`up --build` собирает образ Flask-приложения и запускает сервисы; `-d` оставляет контейнеры работать в фоне. `docker compose ps` показывает их состояние. Современный Compose может предупредить, что поле `version` в файле устарело; это предупреждение не мешает запуску. Закончив с примерами, остановите и удалите контейнеры стенда командой `docker compose down` из того же каталога.
|
||||
|
||||
<details>
|
||||
<summary>Что такое Docker и как в стенде связаны порты</summary>
|
||||
|
||||
Docker запускает приложения в контейнерах — изолированных окружениях с нужными зависимостями. Образ Flask-приложения собирается по [Dockerfile](website/kittens/Dockerfile), а для Nginx Compose использует готовый образ `nginx`. Compose запускает три контейнера, задаёт их переменные окружения и публикует нужные порты на вашей машине.
|
||||
|
||||
```text
|
||||
Ваша машина Контейнеры
|
||||
localhost:9001 ── 9001:5001 ──▶ server1:5001 (Flask v1)
|
||||
localhost:9002 ── 9002:5002 ──▶ server2:5002 (Flask v2)
|
||||
localhost:8080 ── 8080:80 ──▶ proxy:80 (Nginx)
|
||||
└─▶ server1:5001 или server2:5002
|
||||
localhost:8081 ── 8081:10000 ─▶ proxy:10000 (файлы из static)
|
||||
```
|
||||
|
||||
Слева от `:` в Compose указан порт вашей машины, справа — порт контейнера. Когда Nginx обращается к `server1:5001` или `server2:5002`, он использует внутренние адреса сервисов, а не порты `9001` и `9002` вашей машины.
|
||||
|
||||
</details>
|
||||
|
||||
## Возможности Nginx и файл его конфигурации
|
||||
|
||||
В этом стенде Nginx принимает запросы как единая точка входа, распределяет их между копиями приложения, возвращает redirect и сам отдаёт текст или файлы. Какую из этих задач выполнять, определяет его [конфигурация](website/nginx.conf).
|
||||
|
||||
**Reverse proxy** принимает запрос клиента и сам обращается к backend. В группе **upstream** стенда перечислены `server1:5001` и `server2:5002`. Для `/` и `/kittens` Nginx выбирает один из них и добавляет в ответ `X-Upstream` с адресом выбранного backend.
|
||||
|
||||
Если один backend перестаёт отвечать, Nginx временно исключает его из балансировки и направляет новые запросы к оставшимся живым репликам. При ошибке соединения он может попробовать другую реплику и для текущего запроса, поэтому отказ одного сервера не обязательно приводит к ошибке у клиента.
|
||||
|
||||
Когда мы несколько раз отправили запрос к `/` через Nginx, он по очереди направил запросы к двум Flask-серверам: ответы `v1` и `v2` чередовались. Так мы увидели **round-robin** — простой алгоритм выбора backend по очереди. Для трёх серверов порядок мог бы выглядеть так: `3 → 1 → 2 → 3 → 1 → 2`. Алгоритм не оценивает, сколько работы потребует конкретный запрос. Если серверы различаются по мощности или сетевой задержке, равное число запросов может дать им разную нагрузку; в обсуждении упоминали веса backend-серверов. При этом клиент обращается к одной точке входа — Nginx — и не выбирает реплику сам.
|
||||
|
||||
При **redirect** Nginx возвращает клиенту код 3xx и заголовок `Location` с новым URI. Следующий запрос по этому URI делает уже клиент; при reverse proxy Nginx обращается к backend сам. Правило `/search/` на стенде перенаправляет запрос в поиск Google: браузер следует переходу автоматически, а `curl` можно передать флаг `-L`. Как прикладной случай обсудили перенаправление со старого URI `/promotions` на новый `/discounts`.
|
||||
|
||||
Кроме проксирования и redirect, Nginx сам отдаёт текст по `/const` и статические файлы через порт `8081`. Для каталога `static` включён `autoindex`, поэтому в браузере можно переходить по его содержимому.
|
||||
|
||||
## Как повторить примеры
|
||||
|
||||
Команды ниже рассчитаны на запущенный [стенд](website/docker-compose.yaml). `localhost` означает машину, на которой опубликованы порты контейнеров. Флаг `-i` показывает заголовки ответа.
|
||||
|
||||
```bash
|
||||
# Посмотреть запрос, ответ и заголовки.
|
||||
curl -v http://localhost:8080/const
|
||||
|
||||
# Обратиться напрямую к двум экземплярам Flask-приложения.
|
||||
curl -i http://localhost:9001/
|
||||
curl -i http://localhost:9002/
|
||||
|
||||
# Обратиться через Nginx; повторите команду и сравните ответ и X-Upstream.
|
||||
curl -i http://localhost:8080/
|
||||
|
||||
# Посмотреть redirect на поиск Google, затем пройти по нему.
|
||||
curl -i http://localhost:8080/search/cats
|
||||
curl -L http://localhost:8080/search/cats
|
||||
|
||||
# Получить файл, который отдаёт сам Nginx.
|
||||
curl -i http://localhost:8081/greetings.txt
|
||||
```
|
||||
|
||||
Ещё два опыта удобно провести в браузере: откройте `http://localhost:8080/const` и сравните его запрос с `curl` во вкладке Network; затем откройте `http://localhost:9001/kittens` и найдите отдельную загрузку изображения. `/kittens` зависит от внешнего Cat API, а переход на Google — от доступности Google.
|
||||
|
||||
## API заказов: набросок контракта
|
||||
|
||||
В конце занятия мы наметили операции интернет-магазина: создать заказ, получить его данные, изменить адрес или другую часть заказа. Для создания предложили `POST`, для чтения — `GET`, для частичного изменения — `PATCH`; в зависимости от контракта приложение может использовать `PUT`. Упомянули и системы, где для упрощения чтение реализуют через `POST` с телом запроса, но не представляли это как универсальное правило.
|
||||
|
||||
В результате можно предложить следующий контракт:
|
||||
|
||||
| Операция | Запрос | Успешный ответ | Почему так |
|
||||
| --- | --- | --- | --- |
|
||||
| Создать заказ | `POST /api/orders` с данными заказа в JSON | `201 Created`, заголовок `Location` с URI заказа | Запрос отправляют коллекции заказов; сервер обрабатывает данные и создаёт новый заказ с собственным ID. |
|
||||
| Получить заказ | `GET /api/orders/{id}` | `200 OK` и данные заказа в JSON | Клиент обращается к уже известному заказу по его ID и читает данные, не создавая новую сущность. |
|
||||
| Частично изменить заказ | `PATCH /api/orders/{id}` с JSON, например с новым адресом | `200 OK` и обновлённый заказ в JSON | Клиент указывает существующий заказ и передаёт только те данные, которые нужно изменить. |
|
||||
| Удалить заказ | `DELETE /api/orders/{id}` | `204 No Content` | Клиент указывает конкретный заказ, который нужно удалить; тело успешного ответа не требуется. |
|
||||
|
||||
Если заказ не найден, для чтения, изменения или удаления предусмотрен `404 Not Found`; если данные для создания неверны — ошибка клиента, например `400 Bad Request`. Для долгой обработки допускается отдать `202 Accepted` и отдельный ресурс операции.
|
||||
|
||||
### Повторы запросов и идемпотентность
|
||||
|
||||
`GET` только читает заказ, поэтому его повтор не создаёт новую сущность. С `POST /api/orders` иначе: если клиент не получил ответ и повторил запрос, он может случайно создать ещё один заказ. Сервису нужно понять, относится ли повтор к той же логической операции. Для этого можно использовать токен идемпотентности: повтор с тем же токеном сервис распознаёт и не создаёт второй заказ. На семинаре обсудили два способа получить такой идентификатор.
|
||||
|
||||
- **Ключ создаёт клиент.** Например, фронтенд генерирует токен и передаёт его в заголовке или в JSON-теле запроса. Сервис сохраняет токен, чтобы определить возможные повторы запросов. Если ключ нужен лишь на время нескольких попыток и позже клиент его не воспроизводит, хранить его бессрочно незачем: подойдёт кэш на стороне сервиса с ограниченным сроком жизни, например несколько минут. Важно, чтобы на протяжении этого срока повторные попытки запроса приходили с тем же ключом.
|
||||
- **Ключ создаёт сервис.** Если предметная область такова, что у сущности имеется некоторый уникальный идентификатор (например, `order.id` в случае создания заказа), то сервис может использовать этот ID как основу ключа идемпотентности. Поскольку `order.id` скорее всего хранится в базе вместе с заказом, такой ключ идемпотентности может жить столько же, сколько запись о заказе; отдельное короткое время жизни, как у временного ключа в кэше, ему не обязательно.
|
||||
|
||||
Таким образом, выбор способа и срока хранения ключа идемпотентности зависит от контракта API и предметной области.
|
||||
@@ -0,0 +1,30 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
server1:
|
||||
build:
|
||||
context: ./kittens
|
||||
environment:
|
||||
APP_VERSION: 'v1'
|
||||
BIND_PORT: 5001
|
||||
ports:
|
||||
- 9001:5001
|
||||
|
||||
server2:
|
||||
build:
|
||||
context: ./kittens
|
||||
environment:
|
||||
APP_VERSION: 'v2'
|
||||
BIND_PORT: 5002
|
||||
ports:
|
||||
- 9002:5002
|
||||
|
||||
proxy:
|
||||
image: nginx
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8081:10000
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./static:/static
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,3 @@
|
||||
docker-compose.yaml
|
||||
Dockerfile
|
||||
__pycache__
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM python:3.11.5-slim-bullseye
|
||||
|
||||
WORKDIR /application
|
||||
|
||||
# Copy the requirements file and download the dependencies.
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
# Copy other data.
|
||||
COPY . .
|
||||
|
||||
CMD [ "python3", "app.py"]
|
||||
@@ -0,0 +1,45 @@
|
||||
from flask import Flask, render_template, request
|
||||
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
|
||||
app = Flask(__name__)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
APP_VERSION = os.getenv('APP_VERSION')
|
||||
BIND_HOST = os.getenv('BIND_HOST') or '0.0.0.0'
|
||||
BIND_PORT = os.getenv('BIND_PORT') or '8000'
|
||||
|
||||
|
||||
@app.before_request
|
||||
def log_request():
|
||||
app.logger.info(
|
||||
'method=%s path=%s host=%s remote_addr=%s',
|
||||
request.method, request.path, request.host, request.remote_addr,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
return f'Hello from app {APP_VERSION}'
|
||||
|
||||
|
||||
@app.route('/kittens')
|
||||
def kittens():
|
||||
# Get a URL to a random kitten photo.
|
||||
response = requests.get('https://api.thecatapi.com/v1/images/search?api_')
|
||||
|
||||
try:
|
||||
# Response structure is as follows:
|
||||
# [{"id":"bL3lrUi1A","url":"ex.com/bL3lrUi1A.jpg","width":1280,"height":720}]
|
||||
data = response.json()
|
||||
kitten_url = data[0]['url']
|
||||
|
||||
return render_template('index.html', kitten_url=kitten_url)
|
||||
except Exception as e:
|
||||
return f'Failed to fetch a kitten image :(\n {e}'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=BIND_HOST, port=BIND_PORT)
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==2.3.3
|
||||
requests==2.28.2
|
||||
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>DistSys kittens</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="{{ kitten_url }}">
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
events {}
|
||||
|
||||
http {
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
gzip on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name distsys.baliuk.me;
|
||||
|
||||
location /kittens {
|
||||
proxy_pass http://backends;
|
||||
add_header X-Upstream $upstream_addr;
|
||||
}
|
||||
|
||||
location = / {
|
||||
proxy_pass http://backends;
|
||||
add_header X-Upstream $upstream_addr;
|
||||
}
|
||||
|
||||
location /search/ {
|
||||
rewrite ^/search/(.*)$ https://google.com/search?q=$1 redirect;
|
||||
}
|
||||
|
||||
location = /const {
|
||||
add_header Content-Type text/plain;
|
||||
return 200 "Any preset text!";
|
||||
}
|
||||
}
|
||||
|
||||
upstream backends {
|
||||
server server1:5001;
|
||||
server server2:5002;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 10000;
|
||||
|
||||
autoindex on; # enable directory listing output
|
||||
autoindex_exact_size off; # output file sizes rounded to kilobytes, megabytes, and gigabytes
|
||||
autoindex_localtime on; # output local times in the directory
|
||||
|
||||
location / {
|
||||
root /static;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Hello, distributed systems!
|
||||
@@ -0,0 +1,127 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background-color:#fff" viewBox="-.5 -.5 310 191">
|
||||
<rect width="100%" height="100%" fill="#FFF"/>
|
||||
<rect width="297" height="87" x="7" y="58" fill="#e6e6e6" pointer-events="all" rx="6.96" ry="6.96"/>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M32.62 58.64 30.9 30" pointer-events="stroke"/>
|
||||
<path d="m32.93 63.88-2.75-6.84 2.44 1.6 2.22-1.88Z" pointer-events="all"/>
|
||||
</g>
|
||||
<path fill="none" d="M0 0h60v30H0z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:58px;height:1px;padding-top:15px;margin-left:1px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">
|
||||
<div style="font-size:16px">Method</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="30" y="20" font-family="Helvetica" font-size="16" text-anchor="middle">Method</text>
|
||||
</switch>
|
||||
<path fill="none" d="M56 0h60v30H56z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:58px;height:1px;padding-top:15px;margin-left:57px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">Path</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="86" y="20" font-family="Helvetica" font-size="16" text-anchor="middle">Path</text>
|
||||
</switch>
|
||||
<path fill="none" d="M113 0h122v30H113z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:120px;height:1px;padding-top:15px;margin-left:114px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">Protocol version</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="174" y="20" font-family="Helvetica" font-size="16" text-anchor="middle">Protocol version</text>
|
||||
</switch>
|
||||
<path fill="none" d="M127 161h60v30h-60z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:58px;height:1px;padding-top:176px;margin-left:128px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">
|
||||
<div style="font-size:16px">Headers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="157" y="181" font-family="Helvetica" font-size="16" text-anchor="middle">Headers</text>
|
||||
</switch>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M79.45 58.2 83.73 30" pointer-events="stroke"/>
|
||||
<path d="m78.67 63.39-1.26-7.27 2.04 2.08 2.57-1.38Z" pointer-events="all"/>
|
||||
</g>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M153.71 58.24 166.96 30" pointer-events="stroke"/>
|
||||
<path d="m151.48 62.99.86-7.33 1.37 2.58 2.85-.6Z" pointer-events="all"/>
|
||||
</g>
|
||||
<rect width="36" height="21" x="15" y="65" fill="#f2fffc" pointer-events="all" rx="1.68" ry="1.68"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe flex-start;width:1px;height:1px;padding-top:76px;margin-left:19px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:left">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:nowrap">
|
||||
<p style="line-height:100%">
|
||||
<font color="#509ad4" style="font-size:16px">GET</font>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="19" y="80" fill="#333" font-family="Monospace" font-size="16">GET</text>
|
||||
</switch>
|
||||
<rect width="17" height="21" x="70" y="64.5" fill="#f5f5ff" pointer-events="all" rx="1.36" ry="1.36"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe flex-start;width:1px;height:1px;padding-top:75px;margin-left:74px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:left">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:nowrap">
|
||||
<p style="line-height:100%">/</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="74" y="80" fill="#333" font-family="Monospace" font-size="16">/</text>
|
||||
</switch>
|
||||
<rect width="100" height="22" x="101" y="64" fill="#f5ebef" pointer-events="all" rx="1.76" ry="1.76"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe flex-start;width:1px;height:1px;padding-top:75px;margin-left:105px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:left">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:nowrap">
|
||||
<p style="line-height:100%">
|
||||
<font color="#5d837e" style="font-size:16px">HTTP</font>/<font color="#cd5b92" style="font-size:16px">1.1</font>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="105" y="80" fill="#333" font-family="Monospace" font-size="16">HTTP/1.1</text>
|
||||
</switch>
|
||||
<rect width="284" height="48" x="15" y="90" fill="#cfe3f5" pointer-events="all" rx="3.84" ry="3.84"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe flex-start;width:278px;height:1px;padding-top:114px;margin-left:19px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:left">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">
|
||||
<div style="font-size:16px">
|
||||
<font color="#5d837e" style="font-size:16px">Host</font>: developer.mozilla.org</div>
|
||||
<div style="font-size:16px">
|
||||
<font color="#5d837e" style="font-size:16px">Accept-Language</font>: fr</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="19" y="119" fill="#333" font-family="Monospace" font-size="16">Host: developer.mozilla.org...</text>
|
||||
</switch>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M157 144.37V161" pointer-events="stroke"/>
|
||||
<path d="m157 139.12 2.33 7-2.33-1.75-2.33 1.75Z" pointer-events="all"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,121 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background-color:#fff" viewBox="-0.5 -0.5 422 205">
|
||||
<rect width="100%" height="100%" fill="#FFF"/>
|
||||
<rect width="381" height="105" x="30" y="51" fill="#e6e6e6" pointer-events="all" rx="3.15" ry="3.15"/>
|
||||
<path fill="none" d="M233 0h137v30H233z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:135px;height:1px;padding-top:15px;margin-left:234px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">Status message</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="302" y="20" font-family="Helvetica" font-size="16" text-anchor="middle">Status message</text>
|
||||
</switch>
|
||||
<path fill="none" d="M137 0h94v30h-94z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:92px;height:1px;padding-top:15px;margin-left:138px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">Status code</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="184" y="20" font-family="Helvetica" font-size="16" text-anchor="middle">Status code</text>
|
||||
</switch>
|
||||
<path fill="none" d="M0 0h118v30H0z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:116px;height:1px;padding-top:15px;margin-left:1px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">Protocol version</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="59" y="20" font-family="Helvetica" font-size="16" text-anchor="middle">Protocol version</text>
|
||||
</switch>
|
||||
<path fill="none" d="M192 175h60v30h-60z" pointer-events="all"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:58px;height:1px;padding-top:190px;margin-left:193px">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0);" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:Helvetica;color:#000;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">
|
||||
<div style="font-size:16px">Headers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="222" y="195" font-family="Helvetica" font-size="16" text-anchor="middle">Headers</text>
|
||||
</switch>
|
||||
<rect width="36" height="21" x="197" y="56" fill="#f2fffc" pointer-events="all" rx="1.68" ry="1.68"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:1px;height:1px;padding-top:66px;margin-left:215px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:nowrap">OK</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="215" y="71" fill="#333" font-family="Monospace" font-size="16" text-anchor="middle">OK</text>
|
||||
</switch>
|
||||
<rect width="42" height="21" x="139" y="56" fill="#f5f5ff" pointer-events="all" rx="1.68" ry="1.68"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:1px;height:1px;padding-top:66px;margin-left:160px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:nowrap">200</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="160" y="71" fill="#333" font-family="Monospace" font-size="16" text-anchor="middle">200</text>
|
||||
</switch>
|
||||
<rect width="92" height="22" x="38" y="55" fill="#f5ebef" pointer-events="all" rx="1.76" ry="1.76"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe center;width:1px;height:1px;padding-top:66px;margin-left:84px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:center">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:nowrap">
|
||||
<p style="line-height:100%">
|
||||
<font color="#5d837e" style="font-size:16px">HTTP</font>/<font color="#cd5b92" style="font-size:16px">1.1</font>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="84" y="71" fill="#333" font-family="Monospace" font-size="16" text-anchor="middle">HTTP/1.1</text>
|
||||
</switch>
|
||||
<rect width="368" height="63" x="38" y="83" fill="#cfe3f5" pointer-events="all" rx="1.89" ry="1.89"/>
|
||||
<switch transform="translate(-.5 -.5)">
|
||||
<foreignObject width="100%" height="100%" pointer-events="none" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow:visible;text-align:left">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;align-items:unsafe center;justify-content:unsafe flex-start;width:362px;height:1px;padding-top:115px;margin-left:42px">
|
||||
<div data-drawio-colors="color: #333333;" style="box-sizing:border-box;font-size:0;text-align:left">
|
||||
<div style="display:inline-block;font-size:16px;font-family:monospace;color:#333;line-height:1.2;pointer-events:all;white-space:normal;overflow-wrap:normal">
|
||||
<div style="font-size:16px">
|
||||
<font color="#5d837e" style="font-size:16px">date</font>: Tue, 18 Jun 2024 10:03:55 GMT</div>
|
||||
<div style="font-size:16px">
|
||||
<font color="#5d837e" style="font-size:16px">cache-control</font>: public, max-age=3600</div>
|
||||
<div style="font-size:16px">
|
||||
<font color="#5d837e" style="font-size:16px">content-type</font>: text/html</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="42" y="119" fill="#333" font-family="Monospace" font-size="16">date: Tue, 18 Jun 2024 10:03:55 GMT...</text>
|
||||
</switch>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M222 152.37V175" pointer-events="stroke"/>
|
||||
<path d="m222 147.12 2.33 7-2.33-1.75-2.33 1.75Z" pointer-events="all"/>
|
||||
</g>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M229.63 53.02 273.15 30" pointer-events="stroke"/>
|
||||
<path d="m224.99 55.48 5.09-5.34-.45 2.88 2.64 1.25Z" pointer-events="all"/>
|
||||
</g>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="m163.22 50.5 12-20.5" pointer-events="stroke"/>
|
||||
<path d="m160.56 55.04 1.53-7.22 1.13 2.68 2.89-.33Z" pointer-events="all"/>
|
||||
</g>
|
||||
<g stroke="#000" stroke-miterlimit="10">
|
||||
<path fill="none" d="M60.68 48.64 59.75 30" pointer-events="stroke"/>
|
||||
<path d="m60.94 53.88-2.68-6.87 2.42 1.63 2.25-1.86Z" pointer-events="all"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,32 @@
|
||||
# websockets
|
||||
|
||||
В данной директории находится простая реализация websocket сервера на Go.
|
||||
|
||||
## Управление
|
||||
|
||||
Запуск сервера: `go run .`.
|
||||
|
||||
Для подключения к серверу предлагается использовать утилиту `wscat`:
|
||||
```bash
|
||||
wscat --connect 'localhost:8000/connect?author=igor'
|
||||
# В параметре author указываем наш никнейм
|
||||
```
|
||||
|
||||
Далее можно отправить JSON сообщение с полем `text`, после чего это сообщение будет доставлено всем
|
||||
клиентам сервера:
|
||||
```bash
|
||||
~ wscat --connect 'localhost:8000/connect?author=igor'
|
||||
> {"text": "privet"}
|
||||
< {"Text":"privet","Author":"igor","SendTime":"2024-09-24T01:49:50.623671+03:00"}
|
||||
```
|
||||
|
||||
## Бонус
|
||||
|
||||
Чтобы открыть свой сервер, запущенный локально, всему интернету,
|
||||
можно использовать ngrok:
|
||||
```
|
||||
ngrok http 8000
|
||||
```
|
||||
|
||||
После выполнения вам будет выдан публичный домен, по которому будет доступен
|
||||
порт на вашей локальной системе.
|
||||
@@ -0,0 +1,8 @@
|
||||
module ws
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
Connect client 1:
|
||||
|
||||
wscat --connect 'localhost:8000/connect?author=lupa'
|
||||
|
||||
Connect client 2:
|
||||
|
||||
wscat --connect 'localhost:8000/connect?author=lupa'
|
||||
Message: {"Text": "hi there"}
|
||||
*/
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type MessageStream struct {
|
||||
Text string
|
||||
Author string
|
||||
SendTime *time.Time
|
||||
}
|
||||
|
||||
type MessengerServer struct {
|
||||
history chan *MessageStream
|
||||
toSend map[string]chan *MessageStream
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func (s *MessengerServer) Connect(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract author from the URL query parameters
|
||||
author := r.URL.Query().Get("author")
|
||||
if author == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("failed to upgrade: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
id := uuid.New()
|
||||
s.toSend[id.String()] = make(chan *MessageStream, 1000)
|
||||
log.Printf("Connected: %v\n", id)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
var msg MessageStream
|
||||
err := conn.ReadJSON(&msg)
|
||||
if err != nil {
|
||||
log.Println("Failed to decode a message:", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Received from %s: %v\n", author, msg)
|
||||
|
||||
now := time.Now()
|
||||
msg.SendTime = &now
|
||||
msg.Author = author
|
||||
s.history <- &msg
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
mes := <-s.toSend[id.String()]
|
||||
err := conn.WriteJSON(mes)
|
||||
if err != nil {
|
||||
log.Printf("Deleted %v stream, sending error: %v", id.String(), err)
|
||||
delete(s.toSend, id.String())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Sent to stream %v: %v\n", id, mes)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MessengerServer) fanout() {
|
||||
for {
|
||||
mes := <-s.history
|
||||
|
||||
log.Printf("Got from history: %v", mes)
|
||||
|
||||
for _, ch := range s.toSend {
|
||||
ch <- mes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("MESSENGER_SERVER_PORT")
|
||||
if port == "" {
|
||||
port = "8000"
|
||||
log.Println("Missing MESSENGER_SERVER_PORT, using default value: " + port)
|
||||
}
|
||||
server := MessengerServer{
|
||||
history: make(chan *MessageStream, 1000),
|
||||
toSend: make(map[string]chan *MessageStream),
|
||||
}
|
||||
|
||||
http.HandleFunc("/connect", server.Connect)
|
||||
|
||||
go server.fanout()
|
||||
|
||||
log.Fatal(http.ListenAndServe("0.0.0.0:"+port, nil))
|
||||
}
|
||||
Reference in New Issue
Block a user