Чат на 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
        }
    }
}

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


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


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

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


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

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    0
    ssmmxx, почему именно чат?
    +2
    Минут 10 тупил — зачем два объявления одного метода и как это может работать.

    func (s *Server) SendAll(msg *Message) {
    	s.sendAllCh <- msg
    }
    

    и

    func (s *Server) sendAll(msg *Message) {
    	for _, c := range s.clients {
    		c.Write(msg)
    	}
    }
    

    Это так принято — давать одинаковые названия с разницей только в регистре?
      +1
      Названия функций регистрозависимы. Плюс SendAll() это public, а sendAll() protected.
        +1
        Переменные, функции, методы:
        с Заглавной буквы — экспортируемые.
        со строчной — ограничены пакетом.

      Only users with full accounts can post comments. Log in, please.