When designing products, we usually encounter message notification, such as the successful payment of the user‘s order, for example, the user can notify in real time when he has an on-site letter. HTTP is one-way. The client requests and the server returns. This request has ended. Websocket can maintain the connection and realize the long connection. In case of notification, websocket is often used to achieve the purpose that the server actively sends messages to the client.
Our goal is to enable the server to actively send messages to a user. So there are four steps to do.
- Establish connection (keep connection)
- Disconnect (delete connection)
- Maintenance connection (heartbeat detection)
- receive messages
- send message
We use GitHub COM / gorilla / websocket package.
preparation
The first step is to define the structure of a client connection. The connection can only be saved after having the structure of the connection. The ID is the ID of a client connection, and the socket is the real client connection
//Client connection information
type Client struct {
ID string // connection ID
Accountid string // account ID. an account may have multiple connections
Socket *websocket. Conn // connect
Heartbeattime Int64 // previous heartbeat time
}
Then define a client management to manage all client connections and instantiate it as a global variable.
//Message type
const (
Messagetypeheartbeat = "heartbeat" // heartbeat
Messagetyperegister = "register" // register
Heartbeatchecktime = 9 // heartbeat is detected every few seconds
Heartbeattime = 20 // the maximum time between the heartbeat and the last time
Chanbufferregister = 100 // register Chan buffer
Chanbufferunregister = 100 // unregister Chan size
)
//Client management
type ClientManager struct {
Clients map [string] * client // save connection
Accounts map [string] [] string // account and connection relationship. The key of the map is the account ID, that is, accountid. This mainly considers one account and multiple connections
mu *sync.Mutex
}
//Define a management manager
var Manager = ClientManager{
Clients: make (map [string] * client), // users participating in the connection need to set the maximum number of connections for performance reasons
Accounts: make (map [string] [] string), // account and connection relationship
mu: new(sync.Mutex),
}
var (
Registerchan = make (Chan * client, chanbufferregister) // register
Unregisterchan = make (Chan * client, chanbufferunregister) // log off
)
Here, we also need to encapsulate the format of the message sent by the server to the client, so that the server can reply to the message to the client after the client connection is successful
//Encapsulate reply message
type ServiceMessage struct {
Type string ` JSON: "type" ` // type
Content ServiceMessageContent `json:"content"`
}
type ServiceMessageContent struct {
Body string ` JSON: "body" ` // main data
Metadata string ` JSON: "meta_data" ` // extended data
}
func CreateReplyMsg(t string, content ServiceMessageContent) []byte {
replyMsg := ServiceMessage{
Type: t,
Content: content,
}
msg, _ := json.Marshal(replyMsg)
return msg
}
Manage Connections
Establish and disconnect
Connect the clients and accounts in the manager. Clients is used to save each connection that communicates with the client. The binding relationship between the account holder’s connection ID and the connection category.
//Registration cancellation
func register() {
for {
select {
case conn := 0 {
for k, clientId := range Manager.Accounts[c.AccountId] {
If ClientID = = c.id {// binding client ID found
Manager.Accounts[c.AccountId] = append(Manager.Accounts[c.AccountId][:k], Manager.Accounts[c.AccountId][k+1:]...)
}
}
}
}
Maintenance connection (heartbeat detection)
Every once in a while, the heartbeat is detected. If the last heartbeat time exceeds the heartbeat time, it is deemed to have been disconnected.
//Maintain heartbeat
func heartbeat() {
for {
//Get all clients
Manager.mu.Lock()
clients := make([]*Client, len(Manager.Clients))
for _, c := range Manager.Clients {
clients = append(clients, c)
}
Manager.mu.Unlock()
for _, c := range clients {
if time.Now().Unix()-c.HeartbeatTime > HeartbeatTime {
unAccountBind(c)
}
}
time.Sleep(time.Second * HeartbeatCheckTime)
}
}
Manage Connections
//Manage Connections
func Start() {
//Check heartbeat
go func() {
defer func() {
if r := recover(); r != nil {
log.Println(r)
}
}()
heartbeat()
}()
//Registration cancellation
go func() {
defer func() {
if r := recover(); r != nil {
log.Println(r)
}
}()
register()
}()
}
Send and receive messages
Get connection according to account
//Get connection according to account
func GetClient (accountId string) []*Client{
clients := make([]*Client,0)
Manager.mu.Lock()
defer Manager.mu.Unlock()
if len(Manager.Accounts[accountId]) > 0 {
for _,clientId := range Manager.Accounts[accountId] {
if c,ok := Manager.Clients[clientId]; ok {
clients = append(clients,c)
}
}
}
return clients
}
Read messages from clients
We only use the heartbeat, so just judge that the client is a heartbeat message, and then reply.
//Read the message, i.e. receive the message
func (c *Client) Read() {
defer func() {
_ = c.Socket.Close()
}()
for {
//Read message
_, body, err := c.Socket.ReadMessage()
if err != nil {
break
}
var msg struct {
Type string `json:"type"`
}
err = json.Unmarshal(body, &msg)
if err != nil {
log.Println(err)
continue
}
if msg. Type = = messagetypeheartbeat {// maintain heartbeat message
//Refresh connection time
c.HeartbeatTime = time.Now().Unix()
//Reply heartbeat
replyMsg := CreateReplyMsg(MessageTypeHeartbeat, ServiceMessageContent{})
err = c.Socket.WriteMessage(websocket.TextMessage, replyMsg)
if err != nil {
log.Println(err)
}
continue
}
}
}
Send messages to clients
Just find the connection and send a message to the connection.
//Send message
func Send(accounts []string,message ServiceMessage) error{
msg,err := json.Marshal(message)
if err != nil {
return err
}
for _,accountId := range accounts{
//Get connection ID
clients := GetClient(accountId)
//Send message
for _,c := range clients {
_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
}
}
return nil
}
Request call
Here, the HTTP request is upgraded to websocket, and then a separate goroutine is established to maintain the connection. The following calls are similar, but many details such as authentication and log are not perfect. It just provides an idea.
package wesocket
import (
websocket2 "demo/websocket"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/rs/xid"
"log"
"net/http"
"time"
)
type MessageNotifyRequest struct {
UserId string `form:"user_id"`
}
func MessageNotify(ctx *gin.Context) {
//Get parameters
var params MessageNotifyRequest
if err := ctx.ShouldBindQuery(¶ms); err != nil {
log.Println(err)
return
}
//Todo: authentication
//Upgrade HTTP to websocket
conn, err := (&websocket.Upgrader{
// 1. Solve cross domain problems
CheckOrigin: func(r *http.Request) bool {
return true
},
}). Upgrade (CTX. Writer, CTX. Request, Nil) // upgrade
if err != nil {
log.Println(err)
http.NotFound(ctx.Writer, ctx.Request)
return
}
//Create an instance connection
ConnId := xid.New().String()
client := &websocket2.Client{
ID: connid, // connection ID
AccountId: fmt.Sprintf("%s", params.UserId),
HeartbeatTime: time.Now().Unix(),
Socket: conn,
}
//User registration to user connection management
websocket2.RegisterChan
summary
Using websocket for message notification is mainly for the back endBinding connectionandManage Connections, the binding connection is to establish a binding relationship between the user ID and the websocket connection, while the management connection is to store the connection, delete the connection and maintain the health of the connection (heartbeat detection). The second is to define the format of data received and sent by the server. Generally speaking, it is such an idea.