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) {
}
上面是改过后代码,我们来看看这段代码。
- 我们把SSL证书中的两个文件路径作为常量,并拿到main外。没有理由,只是想让main方法成为单纯的路由。
- 使用两个HandleFunc,分别处理socket和site。
- 在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)
}
上面代码,主要表现在这里:
- 首先,我们添加了一个hub的全局变量, 并通过
go hug.Run
启用了一个新的线程。 - 然后,我们在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结构体定义了四个属性:
- clients,用户列表。
- 其余的三个是信息的三种形式,都是Channel。
Run方法分二部分: - 维护客户端列表。
- 收到的信息发到各客户端。如果需要分聊天室,就在这里处理。
三、 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主要做以下几个工作:
- 通过ServeWs建立与客户端的连接。ServeWs连接的建立需要通过HTTP或HTTPS来实现,比如本案使用的是HTTPS,WebSocket通过main.go里的WebSocket方法来建立连接。
- readPump,负责从连接中获取信息并发送到hub里。hub接收后分发到各client。
- 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);
}
};
上面代码主要实现了建立连接与收发信息。
- 建立连接:使用一个新WebSocket来建立连接,本案建立的是SSL连接,所以用的是
wss://
。 - 接收信息:使用已建立的连接,通过它的onmessage事件来接收信息并输出。
- 发送信息:使用已建立的连接,通过它的send方法发送用户输入的信息。
整个实现非常简单明了。
我们来试试:
- 启动服务器,在跳出的对话框中确认允许监听443接口。
$ go run main.go
- 打开多个标签,分别成地址栏输入:
https://localhost/
。
现在你就可以跟自己聊天了。
- 注意,你会看不到各标签加载完成前的信息。
好,就到这里。我是姜友华,下一次,再见。
网友评论