1 August 2016

Чат на Go (часть 1)

Go
Sandbox

Начинаем разработку чата на 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
        }
    }
}

Итак, имеем достаточно хороший каркас для начала разработки.


Давайте определим основные кейсы для нашего чата:


  1. Login/logout
  2. Найти пользователя по логину
  3. Начать приватный чат с 1 пользователем
  4. Начать конференцию сразу с 2 и более пользователями (1й кейс)
  5. Пригласить в существующий приватный чат 1 или более пользователя (начать конференцию 2й кейс)
  6. Просмотр списка приватных чатов и конференций
  7. Выйти из конференции
  8. Просмотреть или редактировать свой профиль
  9. Просмотреть профиль другого пользователя

Основные кейсы определены, можно начинать разработку.


Процесс разработки будет освещен в следующих частях. В результате мы получим работающий прототип, который можно будет развивать по своему усмотрению.

Tags:goчат
Hubs: Go
+23
30.1k 114
Comments 7
Top of the last 24 hours