Протокол WebSocket
предоставляет двунаправленный (сервер и клиент могут обмениваться сообщениями) и полнодуплексный (сервер или клиент могут отправлять сообщения одновременно) канал связи, подходящий для сценариев реального времени, таких как приложения-чаты и т. д. Подключенные пользователи чата (клиенты) могут отправлять сообщения в приложение (на сервер WebSocket
) и обмениваться ими друг с другом — аналогично тому, что происходит в одноранговой сети.
В этой статье мы научимся создавать простое приложение-чат с помощью WebSocket
и Go
. При создании приложения также будет использован Redis
(подробнее об этом далее).
Вы освоите:
SET
и PUBSUB
);go-redis
;gorilla WebSocket
с полной и протестированной реализацией протокола WebSocket;Посмотрим, как создаётся приложение-чат. При подключении пользователя первым делом внутри приложения (на сервере WebSocket
) создаётся соответствующее соединение WebSocket
, которое связано с отдельным экземпляром приложения. Благодаря этому соединению WebSocket
пользователи чата имеют возможность отправлять друг другу сообщения. Мы можем осуществить (горизонтальное) масштабирование нашего приложения (например, для охвата большой базы пользователей), запустив несколько экземпляров. Теперь каждого нового пользователя можно подключить к новому экземпляру. Таким образом, у нас есть сценарий, в котором разные пользователи (вместе с соответствующими подключениями WebSocket
) связаны с разными экземплярами, но не имеют возможности обмениваться сообщениями друг с другом. А это неприемлемо даже для нашего простенького приложения-чата. ????
Redis — это универсальное хранилище данных в формате «ключ — значение», которое поддерживает самые разные структуры данных с широкой функциональностью (List
, Set
, Sorted Set
, Hash
и другие). Одной из функциональных возможностей является также PubSub
, с помощью которой издатели могут отправлять сообщения на канал(ы) Redis, а подписчики могут прослушивать сообщения на этом(их) канале(ах) абсолютно независимо, будучи не связанными друг с другом. Этим можно воспользоваться для решения нашей проблемы. Вместо того чтобы зависеть только от подключений WebSocket
, мы можем использовать Redis channel
, на который можно подписать любое приложение-чат. Так сообщения, отправляемые на соединение WebSocket
, теперь могут передаваться по каналу Redis, что обеспечивает их получение всеми экземплярами приложения (и связанными с ними пользователями чата).
Подробнее поговорим об этом далее, когда перейдём к коду. Он доступен на Github.
Обратите внимание, что вместо обычногоWebSocket
можно также использовать технологии типаAzure SignalR, которые позволяют приложениям отправлять обновления контента подключенным клиентам, например одностраничному веб-сайту или мобильному приложению.В результате клиенты обновляются без необходимости опрашивать сервер или отправлять новые HTTP-запросы на обновления.
Дальше для развертывания этого решения на Azure вам потребуется учётная запись в Microsoft Azure. Если у вас её ещё нет, сейчас можно получить её бесплатно!
А теперь быстренько разберём код. Вот структура приложения:
.
├── Dockerfile
├── chat
│ ├── chat-session.go
│ └── redis.go
├── go.mod
├── go.sum
├── main.go
В main.go
регистрируем наш обработчик WebSocket
и запускаем веб-сервер. Здесь надо использовать обычный пакет net/http
:
http.Handle("/chat/", http.HandlerFunc(websocketHandler))
server := http.Server{Addr: ":" + port, Handler: nil}
go func() {
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal("failed to start server", err)
}
}()
WebSocket
обрабатывает пользователей чата (которые являются не кем иным, как клиентами WebSocket
) и запускает новый чат.
func websocketHandler(rw http.ResponseWriter, req *http.Request) {
user := strings.TrimPrefix(req.URL.Path, "/chat/") peer, err := upgrader.Upgrade(rw, req, nil)
if err != nil {
log.Fatal("websocket conn failed", err)
}
chatSession := chat.NewChatSession(user, peer)
chatSession.Start()
}
ChatSession
(часть chat/chat-session.go
) представляет пользователя и соответствующее ему соединение WebSocket
(на стороне сервера).
type ChatSession struct {
user string
peer *websocket.Conn
}
Когда чат начинает работу, он запускает goroutine
для приёма сообщений от пользователя, только что присоединившегося к чату. Это делается с помощью вызова ReadMessage()
(из websocket.Conn
) в цикле for
. Выполнение горутины завершается (exit
), если пользователь отсоединяется (закрывается соединение WebSocket
), или выключается приложение (например, нажатием ctrl+c
). Для каждого пользователя создаётся отдельная горутина под его сообщения в чате.
func (s *ChatSession) Start() {
...
go func() {
for {
_, msg, err := s.peer.ReadMessage()
if err != nil {
_, ok := err.(*websocket.CloseError)
if ok {
s.disconnect()
}
return
}
SendToChannel(fmt.Sprintf(chat, s.user, string(msg)))
}
}()
Когда сообщение от пользователя получено (через соединение WebSocket
), оно пересылается другим пользователям с помощью функции SendToChannel
, которая является частью chat/redis.go
. Она передаёт сообщение на канал Redis pubsub
.
func SendToChannel(msg string) {
err := client.Publish(channel, msg).Err()
if err != nil {
log.Println("could not publish to channel", err)
}
}
Важная роль в этой задаче отводится sub
(подписчику). В отличие от случая, когда для каждого подключённого пользователя чата выделялась отдельная горутина, мы используем единую
горутину (в рамках приложения), с тем чтобы и подписываться на канал Redis, и получать сообщения, и пересылать их всем пользователям через соответствующее их соединение WebSocket
.
func startSubscriber() {
go func() {
sub = client.Subscribe(channel)
messages := sub.Channel()
for message := range messages {
from := strings.Split(message.Payload, ":")[0]
for user, peer := range Peers {
if from != user {
peer.WriteMessage(websocket.TextMessage, []byte(message.Payload))
}
}
}
Подписка завершается, когда экземпляр приложения выключается. А это, в свою очередь, останавливает цикл канала for-range
, и выполнение горутины завершается.
Функция startSubscriber
вызывается из функции init()
в redis.go
. Функция init()
запускается при подключении к Redis, а в случае сбоя подключения приложение завершает работу.
Теперь настроим экземпляр Redis, к которому можно подключить внутреннюю часть нашего приложения-чата. Давайте создадим сервер Redis в облаке!
Azure Redis Cache предоставляет доступ к защищённому выделенному кэшу Redis, который размещён в Azure и доступен любому приложению как в платформе Azure, так и за её пределами.
Для достижения наших целей мы будем настраивать Azure Redis Cache с уровнем Basic
, который предусматривает кэш одного узла и идеально подходит для разработки/тестирования и некритичных рабочих нагрузок. А кроме базового, можно выбрать уровень Standard
или Premium
с дополнительным набором различных функциональных возможностей, в том числе постоянным хранением данных, кластеризацией, георепликацией и другими.
Для установки будем использовать Azure CLI. Если вам привычнее работать в браузере, можно также воспользоваться облачной оболочкой Azure Cloud Shell.
А быстро настроить экземпляр Azure Redis Cache можно командой az redis create
. Например, вот так:
az redis create --location westus2 --name chat-redis --resource-group
chat-app-group --sku Basic --vm-size c0
Загляните в пошаговое руководство по созданию Azure Cache для Redis.
По завершении вам понадобится информация для подключения к экземпляру Azure Redis Cache, т.е. узел, порт и клавиши быстрого доступа. Получаем эту информацию также через CLI. Например, вот так:
//имя узла и порт (SSL)
az redis show --name chat-redis --resource-group chat-app-group
--query [hostName,sslPort] --output tsv
//первичный ключ доступа
az redis list-keys --name chat-redis --resource-group chat-app-group --query [primaryKey] --output tsv
Загляните в пошаговое руководство по получению имени узла, портов и ключей для Azure Cache для Redis”.
Вот и всё…
Для простоты приложение будет доступно в виде докерного образа.
Первым делом зададим несколько переменных окружения:
//используем порт 6380 для SSL
export REDIS_HOST=[redis cache host name as obtained from CLI]:6380
export REDIS_PASSWORD=[redis cache primary access key as obtained from CLI]
export EXT_PORT=9090
export NAME=chat1
Приложение использует статический порт 8080
внутренне (для веб-сервера). Мы используем внешний порт, указываемый в строке с EXT_PORT
, и сопоставляем его с портом 8080
внутри нашего контейнера (используя -p $EXT_PORT:8080
).
Запускаем докерный контейнер:
docker run --name $NAME -e REDIS_HOST=$REDIS_HOST -e REDIS_PASSWORD=$REDIS_PASSWORD -p $EXT_PORT:8080 abhirockzz/redis-chat-goТеперь можно присоединиться к чату! Вы можете использовать любой клиент WebSocket
. Я предпочитаю использовать wscat
в терминале и/или расширение WebSocket для Chrome в браузере.
Открываем два отдельных терминала, чтобы с помощью wscat
сымитировать действия двух разных пользователей:
//терминал 1 (пользователь "foo")
wscat -c ws://localhost:9090/chat/foo
//терминал 2 (пользователь "bar")
wscat -c ws://localhost:9090/chat/bar
Вот как может выглядеть чат с участием пользователей foo
и bar
:
foo
подключается первым и получает приветственное сообщение: «Добро пожаловать, foo!
». После foo
к чату присоединяется и bar
, получающий аналогичное приветственное сообщение: «Добро пожаловать, bar!
». Причём foo
получил уведомление о том, что bar
подключился к чату. foo
и bar
обменялись несколькими сообщениями, прежде чем bar
покинул чат (foo
получил уведомление и об этом тоже).
Чтобы потренироваться, вы можете запустить свой экземпляр приложения-чата. Разверните ещё один докерный контейнер с другим значением для внешнего порта EXT_PORT
и названием чата. Например, такой:
//используем порт 6380 для SSL
export REDIS_HOST=[redis cache host name as obtained from CLI]:6380
export REDIS_PASSWORD=[redis cache primary access key as obtained from CLI]
export EXT_PORT=9091
export NAME=chat2
docker run --name $NAME -e REDIS_HOST=$REDIS_HOST -e REDIS_PASSWORD=$REDIS_PASSWORD -p $EXT_PORT:8080 abhirockzz/redis-chat-go
Теперь подключаемся через порт 9091
(или выбранный вами порт), чтобы сымитировать действия другого пользователя:
//пользователь "pi"
wscat -c ws://localhost:9091/chat/pi
foo
всё ещё активен в чате, поэтому он получит уведомление о прибытии нового участника pi, с которым они теперь могут обменяться любезностями.
Давайте получим подтверждение, заглянув в структуры данных Redis. Для этого можно воспользоваться redis-cli
. При работе с Azure Redis Cache я бы рекомендовал очень полезную веб-консоль для Redis.
У нас есть SET
(с названием chat-users
), в котором хранятся активные пользователи:
Теперь вы должны увидеть такой результат:
1) "foo"
2) "bar"
Это означает, что пользователи foo
и bar
сейчас подключены к приложению-чату и имеют активное подключение WebSocket
.
А что с каналом PubSub
?
Так как для всех пользователей у нас один канал, вы должны получить такой результат от сервера Redis:
1) "chat"Вот и всё.
Перевод статьи Abhishek Gupta: Let’s learn how to to build a chat application with Redis, WebSocket and Go
Комментарии