美文网首页
从零实现直播:聊天室

从零实现直播:聊天室

作者: Jiangyouhua | 来源:发表于2021-08-25 13:52 被阅读0次

Hi,大家好,我是姜友华。
这一节我们将使用WebSocket实现一个简单的聊天室功能。记得我最早开发的聊天室使用是ASP语言,在ASP语言里混合使用VBScript和Javascript,调用com组件定时轮询来实现。真是long long ago的事情了。

今天,我们将稍微改动一下Go的WebSocket示例来实现。示例代码是Go官方的,非常简单好用。改动的地方在,原来是基于HTTP建立连接,现在改为基于HTTPS建立连接。
你可以只看官方的。

主要内容

  • 服务端WebSocket实现。
  • 客户端WebSocket实现。

服务端WebSocket实现

一、入口的处理。

  • 首先,我们在server目录下添加socket文件夹,在这里我们将添加socket的处理。
  • 然后,我们更改一下main.go:
/// main.go
const (
   keyFile = "/Users/jiangyouhua/code/system/live/server/server.key"
   certFile = "/Users/jiangyouhua/code/system/live/server/server.crt"
)

func main() {
   // 分开处理socket、site。
   http.HandleFunc("/ws", webSocket)
   http.HandleFunc("/", webSite)

   if err := http.ListenAndServeTLS(":443", certFile, keyFile, nil); err != nil {
       log.Fatalln(err)
   }
}

func webSite(w http.ResponseWriter, r * http.Request) {
   p := "." + r.URL.Path
   if p == "./" {
       p = "./site/index.html"
   }
   http.ServeFile(w, r, p)
}

func webSocket(w http.ResponseWriter, r * http.Request) {

}

上面是改过后代码,我们来看看这段代码。

  1. 我们把SSL证书中的两个文件路径作为常量,并拿到main外。没有理由,只是想让main方法成为单纯的路由。
  2. 使用两个HandleFunc,分别处理socket和site。
  3. 在site里,我们处理默认文件的加载,使用其为index.html。
  • 最后,我们将Go的WebSocket示例代码拿过来,将hub.go和client.go放在socket文件夹里。并更改包名为socket;将home.html替换site里的index.html,并改名为index.html;还有更改index.html里的Javascript代码,改ws://wss://,表示使用SSL。
    再次更改main.go
/// main.go
...
var (
    hub = socket.NewHub()
)

func main() {
    go hub.Run()
    ......
}

......

func socket(w http.ResponseWriter, r * http.Request) {
    socket.ServeWs(hub, w, r)
}

上面代码,主要表现在这里:

  1. 首先,我们添加了一个hub的全局变量, 并通过go hug.Run启用了一个新的线程。
  2. 然后,我们在socket方法里调用了ServeWs(hub, w, r)
    这两处的意义是:socket协议需要通过http建立连接(即ServeWs(hub, w, r)),剩下的就可以自己处理了(即hub.Run)。

二、hub.go的实现。

由于改了包名,所以需要将外部可调用的方法名首个字母改大写。
hub.go的主要作用是收发信息。

/// hub.go
type Hub struct {
    clients map[*Client]bool
    broadcast chan []byte
    register chan *Client
    unregister chan *Client
}
......
func (h *Hub) Run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true
        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
        case message := <-h.broadcast:
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

上面代码中,Hub结构体定义了四个属性:

  1. clients,用户列表。
  2. 其余的三个是信息的三种形式,都是Channel。
    Run方法分二部分:
  3. 维护客户端列表。
  4. 收到的信息发到各客户端。如果需要分聊天室,就在这里处理。

三、 client.go的实现。

同样需要将外部可调用方法的首个字母大写。
client.go

/// client.go
const (
    writeWait = 10 * time.Second
    pongWait = 60 * time.Second
    pingPeriod = (pongWait * 9) / 10
    maxMessageSize = 512
)

var (
    newline = []byte{'\n'}
    space   = []byte{' '}
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

type Client struct {
    hub *Hub
    conn *websocket.Conn
    ssend chan []byte
}

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    c.conn.SetReadLimit(maxMessageSize)
    c.conn.SetReadDeadline(time.Now().Add(pongWait))
    c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("error: %v", err)
            }
            break
        }
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        c.hub.broadcast <- message
    }
}

func (c *Client) writePump() {
    ticker := time.NewTicker(pingPeriod)
    defer func() {
        ticker.Stop()
        c.conn.Close()
    }()
    for {
        select {
        case message, ok := <-c.send:
            c.conn.SetWriteDeadline(time.Now().Add(writeWait))
            if !ok {
                // The hub closed the channel.
                c.conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }

            w, err := c.conn.NextWriter(websocket.TextMessage)
            if err != nil {
                return
            }
            w.Write(message)

            // Add queued chat messages to the current websocket message.
            n := len(c.send)
            for i := 0; i < n; i++ {
                w.Write(newline)
                w.Write(<-c.send)
            }

            if err := w.Close(); err != nil {
                return
            }
        case <-ticker.C:
            c.conn.SetWriteDeadline(time.Now().Add(writeWait))
            if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}

// serveWs handles websocket requests from the peer.
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
    client.hub.register <- client

    // Allow collection of memory referenced by the caller by doing all work in
    // new goroutines.
    go client.writePump()
    go client.readPump()
}

client.go主要做以下几个工作:

  1. 通过ServeWs建立与客户端的连接。ServeWs连接的建立需要通过HTTP或HTTPS来实现,比如本案使用的是HTTPS,WebSocket通过main.go里的WebSocket方法来建立连接。
  2. readPump,负责从连接中获取信息并发送到hub里。hub接收后分发到各client。
  3. writePump,接收hub分发的信息,通过已建立的连接发送给各客户端。

hub与client之间的通信主要是Channel,关于Go Channel 知识请查看官网的A Tour of Go,当然你也可以从中了解Go的其它的知识。

四、index.html实现。

index.html布局了一个提交信息的input和一个显示信息的div组件。接收到的信息将被分行逐条显示。建立webSocket连接并实现收发信息都是通过JavaScript实现,代码如下:

/// index.html
window.onload = function () {
    ......
    document.getElementById("form").onsubmit = function () {
        ......
        conn.send(msg.value);
        msg.value = "";
        return false;
    };
    if (window["WebSocket"]) {
        conn = new WebSocket("wss://" + document.location.host + "/ws");
        conn.onclose = function (evt) {
            var item = document.createElement("div");
            item.innerHTML = "<b>Connection closed.</b>";
            appendLog(item);
        };
        conn.onmessage = function (evt) {
            var messages = evt.data.split('\n');
            for (var i = 0; i < messages.length; i++) {
                var item = document.createElement("div");
                item.innerText = messages[i];
                appendLog(item);
            }
        };
    } else {
        var item = document.createElement("div");
        item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
        appendLog(item);
    }
};

上面代码主要实现了建立连接与收发信息。

  1. 建立连接:使用一个新WebSocket来建立连接,本案建立的是SSL连接,所以用的是wss://
  2. 接收信息:使用已建立的连接,通过它的onmessage事件来接收信息并输出。
  3. 发送信息:使用已建立的连接,通过它的send方法发送用户输入的信息。
    整个实现非常简单明了。

我们来试试:

  1. 启动服务器,在跳出的对话框中确认允许监听443接口。
$ go run main.go
  1. 打开多个标签,分别成地址栏输入:https://localhost/
    现在你就可以跟自己聊天了。
  • 注意,你会看不到各标签加载完成前的信息。

好,就到这里。我是姜友华,下一次,再见。

相关文章

网友评论

      本文标题:从零实现直播:聊天室

      本文链接:https://www.haomeiwen.com/subject/scdeiltx.html