Начинаем разработку чата на 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й кейс)
- Просмотр списка приватных чатов и конференций
- Выйти из конференции
- Просмотреть или редактировать свой профиль
- Просмотреть профиль другого пользователя
Основные кейсы определены, можно начинать разработку.
Процесс разработки будет освещен в следующих частях. В результате мы получим работающий прототип, который можно будет развивать по своему усмотрению.