[Из песочницы] Чат на Go (часть 1)
Начинаем разработку чата на Go. Со стеком технологий пока не определились, но для начала сделаем каркас на Go. Берем за основу стандартный пример и пробуем разобраться, что здесь к чему:
https://github.com/golang-samples/websocket/tree/master/websocket-chat
Структура
Вводим 3 структуры Message, Client, Server, которые определяют сервер, клиента со стороны сервера и сообщение.
Message
Сообщение определено структурой:
type Message struct {
Author string `json:"author"`
Body string `json:"body"`
}
func (self *Message) String() string {
return self.Author + " says " + self.Body
}
С сообщением все совсем просто… Так, что перейдем сразу к клиенту.
Client
Клиент определен структурой и имеет id, ссылку на сокет ws websocket.Conn, ссылку на сервер server Server, канал для отправки сообщений ch chan *Message, канал для завершения doneCh chan bool.
type Client struct {
id int
ws *websocket.Conn
server *Server
ch chan *Message
doneCh chan bool
}
Метод Listen, запускает процессы прослушивания чтения и записи в сокет.
func (c *Client) Listen() {
go c.listenWrite()
c.listenRead()
}
Метод прослушивания записи запускает бесконечный цикл, в котором мы проверяем каналы. Как только в канал c.ch прилетает сообщение, мы его отправляем в сокет websocket.JSON.Send (c.ws, msg). А если прилетает в канал c.doneCh, то мы завершаем цикл и горутину.
func (c *Client) listenWrite() {
...
for {
select {
// send message to the client
case msg := <-c.ch:
websocket.JSON.Send(c.ws, msg)
// receive done request
case <-c.doneCh:
c.server.Del(c)
c.doneCh <- true // for listenRead method
return
}
}
}
Метод прослушивания чтения, так же запускает бесконечный цикл и тоже слушает каналы. По приходу сообщения в канал c.doneCh — завершает цикл и горутину. А по умолчанию опрашивает сокет websocket.JSON.Receive (c.ws, &msg). И ка только в сокете есть сообщение, оно отдается серверу c.server.SendAll (&msg) для массовой рассылки всем клиентам.
func (c *Client) listenRead() {
...
for {
select {
// receive done request
case <-c.doneCh:
c.server.Del(c)
c.doneCh <- true // for listenWrite method
return
// read data from websocket connection
default:
var msg Message
err := websocket.JSON.Receive(c.ws, &msg)
if err == io.EOF {
c.doneCh <- true
} else if err != nil {
c.server.Err(err)
} else {
c.server.SendAll(&msg)
}
}
}
}
Server
Теперь разберемся к сервером. Он определен структурой и имеет строку для определения пути, по которому будет работать сервер pattern string, массив для хранения сообщений пользователей messages []Message, карту для хранения клиентов по id клиента clients map[int]Client, каналы для добавления нового клиента в список клиентов addCh chan Client и для удаления клиента из списка клиентов delCh chan Client, канал для отправки всех сообщений sendAllCh chan *Message, каналы для завершения doneCh chan bool и для ошибок errCh chan error
type Server struct {
pattern string
messages []*Message
clients map[int]*Client
addCh chan *Client
delCh chan *Client
sendAllCh chan *Message
doneCh chan bool
errCh chan error
}
Самый интересный метод в сервере — это метод Listen, остальное я думаю более чем понятно, так что давайте разберемся с ним.
В начале реализуется анонимная функция, которая будет вызвана при обращении к нашему серверу по протоколу ws по пути, содержащимся в s.pattern. При вызове этой функции мы создаем нового клиента, добавляем его на сервер и говорим клиенту слушать… client:= NewClient (ws, s)… s.Add (client)… client.Listen ()
func (s *Server) Listen() {
...
onConnected := func(ws *websocket.Conn) {
defer func() {
err := ws.Close()
if err != nil {
s.errCh <- err
}
}()
client := NewClient(ws, s)
s.Add(client)
client.Listen()
}
http.Handle(s.pattern, websocket.Handler(onConnected))
...
}
Во второй части метода, запускается бесконечный цикл, в котором опрашиваются каналы.
В принципе, здесь все интуитивно понятно, но давайте пройдем:
- Прилетает в канал s.addCh = добавим прилетевшего клиента в карту s.clients по id клиента s.clients[c.id] = c и отправим новому клиенту все сообщения s.sendPastMessages©
- в канал s.delCh = удалим клиента из карты s.clients по id клиента delete (s.clients, c.id)
- в канал s.sendAllCh = добавим прилетевшее сообщение в массив сообщений s.messages s.messages = append (s.messages, msg) и скажем серверу разослать сообщение всем клиентам s.sendAll (msg)
- в канал s.errCh = выводим ошибку
- в канал s.doneCh = завершаем бесконечный цикл и горутину
func (s *Server) Listen() {
...
for {
select {
// Add new a client
case c := <-s.addCh:
s.clients[c.id] = c
s.sendPastMessages(c)
// del a client
case c := <-s.delCh:
delete(s.clients, c.id)
// broadcast message for all clients
case msg := <-s.sendAllCh:
s.messages = append(s.messages, msg)
s.sendAll(msg)
case err := <-s.errCh:
log.Println("Error:", err.Error())
case <-s.doneCh:
return
}
}
}
Итак, имеем достаточно хороший каркас для начала разработки.
Давайте определим основные кейсы для нашего чата:
- Login/logout
- Найти пользователя по логину
- Начать приватный чат с 1 пользователем
- Начать конференцию сразу с 2 и более пользователями (1й кейс)
- Пригласить в существующий приватный чат 1 или более пользователя (начать конференцию 2й кейс)
- Просмотр списка приватных чатов и конференций
- Выйти из конференции
- Просмотреть или редактировать свой профиль
- Просмотреть профиль другого пользователя
Основные кейсы определены, можно начинать разработку.
Процесс разработки будет освещен в следующих частях. В результате мы получим работающий прототип, который можно будет развивать по своему усмотрению.