这是我参与「第五届青训营 」笔记创作活动的第9天
做一个IM系统的,websocket是必不可少的,这里就主要介绍websocket以及怎么用websocket做一个即时聊天
websocket介绍
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
websocket优点
主要有以下优点:
- 网络请求更快,SDK ready 耗时减少23%;
- 消息收发效率更高,其中300ms以内的耗时占比提升35%;
- SDK 使用推(Server Push)、拉(SDK Pull)结合的方式消息收发,在充分发挥 WebSocket 的优点,降低消息传输延时的同时,也保证了消息的可靠性;
- 大大减少了前端网络请求量,对小程序更友好;
- 更迅速地感知用户状态变更,当客户关闭浏览器 Tab页或者强杀浏览器进程后,IM 状态系统可以非常迅速地感知到(如果客户端发出了 FIN 或 RST 报文,则实时感知,否则最迟60s感知)。
建立连接
客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
- 客户端:申请协议升级首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法。
GET / HTTP/1.1 Host: localhost:8080 Origin: http://127.0.0.1:3000 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重点请求首部意义如下:Connection: Upgrade:表示要升级协议Upgrade: websocket:表示要升级到websocket协议。Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
-
服务器:响应协议升级服务端返回内容如下,状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=复制代码
-
Sec-WebSocket-Accept的计算Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。计算公式为:将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。通过SHA1计算出摘要,并转成base64字符串。
维持连接
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
- 发送方->接收方:
ping - 接收方->发送方:
pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)
实战
这是是参考github上面的一个案例,本人测试,能正常使用
Server
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var clients = make(map[*websocket.Conn]bool) // connected clients
var broadcast = make(chan Message) // broadcast channel
// Configure the upgrader
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// Define our message object
type Message struct {
Email string `json:"email"`
Username string `json:"username"`
Message string `json:"message"`
}
func main() {
// Create a simple file server
fs := http.FileServer(http.Dir("../public"))
http.Handle("/", fs)
// Configure websocket route
http.HandleFunc("/ws", handleConnections)
// Start listening for incoming chat messages
go handleMessages()
// Start the server on localhost port 8000 and log any errors
log.Println("http server started on :8000")
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func handleConnections(w http.ResponseWriter, r *http.Request) {
// Upgrade initial GET request to a websocket
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}
// Make sure we close the connection when the function returns
defer ws.Close()
// Register our new client
clients[ws] = true
for {
var msg Message
// Read in a new message as JSON and map it to a Message object
err := ws.ReadJSON(&msg)
if err != nil {
log.Printf("error: %v", err)
delete(clients, ws)
break
}
// Send the newly received message to the broadcast channel
broadcast <- msg
}
}
func handleMessages() {
for {
// Grab the next message from the broadcast channel
msg := <-broadcast
// Send it out to every client that is currently connected
for client := range clients {
err := client.WriteJSON(msg)
if err != nil {
log.Printf("error: %v", err)
client.Close()
delete(clients, client)
}
}
}
}
客户端
new Vue({
el: '#app',
data: {
ws: null, // Our websocket
newMsg: '', // Holds new messages to be sent to the server
chatContent: '', // A running list of chat messages displayed on the screen
email: null, // Email address used for grabbing an avatar
username: null, // Our username
joined: false // True if email and username have been filled in
},
created: function() {
var self = this;
this.ws = new WebSocket('ws://' + window.location.host + '/ws');
this.ws.addEventListener('message', function(e) {
var msg = JSON.parse(e.data);
self.chatContent += '<div class="chip">'
+ '<img src="' + self.gravatarURL(msg.email) + '">' // Avatar
+ msg.username
+ '</div>'
+ emojione.toImage(msg.message) + '<br/>'; // Parse emojis
var element = document.getElementById('chat-messages');
element.scrollTop = element.scrollHeight; // Auto scroll to the bottom
});
},
methods: {
send: function () {
if (this.newMsg != '') {
this.ws.send(
JSON.stringify({
email: this.email,
username: this.username,
message: $('<p>').html(this.newMsg).text() // Strip out html
}
));
this.newMsg = ''; // Reset newMsg
}
},
join: function () {
if (!this.email) {
Materialize.toast('You must enter an email', 2000);
return
}
if (!this.username) {
Materialize.toast('You must choose a username', 2000);
return
}
this.email = $('<p>').html(this.email).text();
this.username = $('<p>').html(this.username).text();
this.joined = true;
},
gravatarURL: function(email) {
return 'http://www.gravatar.com/avatar/' + CryptoJS.MD5(email);
}
}
});