如果服务器会定期地更新消息,但是具体的时间间隔并不确定,客户端为了能够及时拿取最新的消息,就得不断的发送 HTTP 请求来轮询服务器,也就是说为了能够及时获取最新资讯,由于 HTTP 本身的特点,服务请求的发起方只能是客户端,服务器本身并没有消息推送的功能,这样就导致了资源浪费的问题,很多客户端发送的请求都是无效的,当客户端和服务器一对一交流时倒还好,如果很多客户端向服务器以这样的形式咨询,服务器将会承受非常大的压力了。
所以,为了避免资源浪费,我们来引入 WebSocket 解决上述问题,WebSocket 可以说是 HTTP 协议的一个升级,它同样基于 TCP 之上,但与传输层的 socket 不同,它的 socket 是应用层的概念。要想使用 WebSocket ,客户端和服务器必须同时支持才可以使用。
WebSocket 中比较重要的一个阶段就是握手阶段了,如下图所示,客户端向服务器发送一个协议升级的 HTTP 请求,并且附带上一些其他的元信息用于握手。

1 | Request headers from client |
服务器收到该协议升级的 HTTP 请求后,会返回一个 HTTP 答复,向客户端确认同意建立 WebSocket 连接,从此之后,它们之间就不再用 HTTP 的形式交流了,而是直接改用 WebSocket 格式的报文进行交流,服务器也具备了相应推送消息的能力了。
1 | Response headers from server |
下面我们来看一下使用 "github.com/gorilla/websocket" 时各个步骤对应的代码吧。
客户端代码的风格和使用 TCP 进行编程非常类似,第一步,客户端向服务器发送协议升级请求,对应websocket.DefaultDialer.Dial(u.string(), nil) 。
1 | var addr = flag.String("addr", "localhost:8080", "http service addr") |
在服务端处理 WebSocket 请求与处理 HTTP 请求类似,我们同样需要定义一个 handler ,但与普通处理 HTTP 请求不同的是,此 handler 会对协议进行升级,也就是 upgrader.Upgrade(w, r, nil) ,这段代码相当于服务端向客户端发送一个协议升级的确认消息。在这之后,服务器就可以一直读取客户端发来的请求和向客户端返回答复了(代码的逻辑在 for 循环之中),也就是说服务器只起了一个 handler 来处理这段 session 。
1 | func echo(w http.ResponseWriter, r *http.Request) { |
客户端在握手之后就可以读取和发送数据,在这里,由于在 WebSocket 中服务器可以主动向客户端推送数据,所以我们起一个 goroutine 专门用于接受服务器主动推送的数据( c.ReadMessage() )。
1 | done := make(chan struct{}) |
下面是客户端发送数据的逻辑,这里我们每隔一秒钟向服务器发送当前的时间,c.WriteMessage(websocket.TextMessage, []byte(t.string())) ,这里我们注意到,WebSocket 中的消息是有类型的,这个函数的第一个参数就是指明该消息的类型,这里我们使用 websocket.TextMessage 。
那么如何让这段 session 结束呢,结束会话一般来说都是客户端主动发起的,客户端主要发送一个结束对话的消息类型就可以了,c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) ,这里注意该结束对话的消息类型为 websocket.CloseMessage 。
1 | ticker := time.NewTicker(time.Second) |