Here’s a brief introduction to Websocket, and really just a brief introduction.

1. Application scenarios

In some scenarios, such as the transaction K line, we need the front end to poll the back end to continuously obtain or update the resource status. The polling problem is no clunky, because each HTTP request has three handshakes and four waves in addition to its own resource information transfer. An alternative to polling is to reuse an HTTP connection to more accurately reuse the same TCP connection. This can be HTTP long connection or websocket.

2. Differences between WebSocket and HTTP long connections

First, Websocket and HTTP are completely different protocols, even though the underlying protocol is TCP/IP. HTTP long connections also belong to HTTP. The biggest difference between HTTP and Websocket is that HTTP is based on request/ Response mode, while the client and server of Websocket can initiate data push at will. For example, websocket is suitable for sending messages from the server to the APP. (In this scenario, HTTP long connection can also be used. The client periodically sends messages to the server, such as HeatBeat. The message to be pushed by the server is then returned to the client as a response.

Gist.github.com/legendtkl/1 here… I’ll write a github GIST snippet to give you a taste of it.

Golang Best Practices

Here to define the use of our scenario: a lot of transaction data, such as K line, such as port data are regularly refreshed, here can use websocket to do. To put it simply, the front end requests specific data, such as K line data, the front end establishes websocket connection with the back end, and the back end continuously returns information to the front end.

Before we write the WebSocket interface, we need to think a little bit about how to abstract and design our WebSocket framework so that our code is extensible.

3.1 the Hub

First of all, what is a hub? The following figure is the result found by Google Image. For a simple analogy, the USB 3.0 ports in the picture (blue) are equivalent to TCP connections, and the interfaces summarized above are the upstream data sources of our hub.


The first time I wanted to define the granularity of the hub was to use the controller, the requested router. However, I later thought that the design was too complicated, because a router has many parameters, and different parameters may correspond to different data.

So how do you define it? Not in terms of functionality, but in terms of data sources. Let’s just take a quick look at how many classes of constantly updated data we need to provide, each of which corresponds to a hub.

3.2 Broadcast

In 3.1 we defined the hub, and the next thing to consider is how to broadcast.

The easiest way to iterate over all conn’s on a hub is to conn.write (). This approach is very simple and crude, and the problem is obvious: each conn.write () is a network IO, so we are processing multiple network IO serially, which is inefficient.

Serial to parallel. We’ll iterate over all the conns on the hub and then do a goroutine for each conn.write (), which is IO multiplexing.

Think about whether there is a problem with the above approach. There is: scalability. If the websocket interface has a lot of parameters and we want to return different results for different conn’s based on the parameters, what should we do? It is also easy to wrap conn above into a struct. As I mentioned in a long time ago article on function extensibility, designing function parameters as structs is a good way to extend functions.

3.3 Hub data awareness

Then 3.2, how to get the data of broadcast? Pull it from the information source or push it by others? The simplest way to do this is to construct a producer-consumer model, and implementing the producer-consumer model in Golang is particularly simple. In our case, we only need to specify a channel in the hub.

As I understand it, the survival of the data to be broadcast should all be business logic and should not be coupled to the infrastructure.

4. talk is cheap, show me the code

The code uses the following two packages as examples:

  1. Github.com/astaxie/bee…
  2. Github.com/gorilla/web…

The Controller.

type WsController struct { beego.Controller } var upgrader = websocket.Upgrader{ ReadBufferSize: maxMessageSize, WriteBufferSize: maxMessageSize, } func (this *WsController) WSTest() { defer this.ServeJSON() ws, Err := upgrader.Upgrade(this.ctx.responsewriter, this.ctx.request, nil) Net.conn is websocket encapsulation if err! WsClient := &wsclient {WsConn: = nil {this.Data["json"] = "fail" return} // WsClient := &wsclient {WsConn: ws, WsSend: make(chan []byte, maxMessageSize), HttpRequest: This.ctx. Request, // Record Request parameters} service.serveWsexample (wsClient)}Copy the code

WsClient structure.

type WsClient struct {
    WsConn  *websocket.Conn
    WsSend  chan []byte
    HttpRequest http.Request 
}
Copy the code

WsClient has two basic methods: processing data sent from the client and processing data sent from the server. This uses functions as parameters, also to achieve maximum flexibility, but the function parameter design is not necessarily the most appropriate, if you have more appropriate, welcome to comment.

func (client *WsClient) ReadMsg(fn func(c *WsClient, s string)) { for { _, msg, err := client.WsConn.ReadMessage() if err ! = nil { break } fn(client, string(msg)) } } func (client *WsClient) WriteMsg(fn func(s string) error) { for { select { case msg, ok := <-client.WsSend: if ! ok { client.WsConn.WriteMessage(websocket.CloseMessage, []byte{}) return } if err := fn(string(msg)); err ! = nil { return } } } }Copy the code

The Hub.

type WsHub struct { Clients map[*WsClient]bool // clients to be broadcast Broadcast chan string Register chan *WsClient UnRegister chan *WsClient LastMsg string // Last broadcast. If we broadcast once a minute and a new request has not reached the time of broadcast, return the contents of the last broadcast.Copy the code

The Hub includes an export Run method and a private broadCast() method.

func (hub *WsHub) Run() { for { select { case c := <-hub.Register: hub.Clients[c] = true c.WsSend <- []byte(hub.LastMsg) break case c := <-hub.UnRegister: _, ok := hub.Clients[c] if ok { delete(hub.Clients, c) close(c.WsSend) } break case msg := <-hub.Broadcast: hub.LastMsg = msg hub.broadCast() break } } } func (hub *WsHub) broadCast() { for c := range hub.Clients { select { case  c.WsSend <- []byte(hub.LastMsg): break default: close(c.WsSend) delete(hub.Clients, c) } } }Copy the code

We now string together the client and hub, which is service.serveWsexample (wsClient) in the first example.

Func initWs() {WsHubs = make(map[string]* util.wshub) hubList := []string{"hub1", "hub2", "hub2"} for _, hub := range hubList { WsHubs[hub] = &WsHub { Clients: make(map[*util.WsClient]bool), Broadcast: make(chan string), Register: make(chan *util.WsClient), UnRegister: make(chan *util.WsClient), //Identity: hub.String(), } go mockBroadCast(WsHubs[hub].Broadcast) go WsHubs[hub].Run() } } func mockBroadCast(broadCast chan string) { for { BroadCast <- "Hello world" time.sleep (time.second * 10)}} // Controller requests to route to the corresponding ServeWsExample function func ServeWsExample(c *util.WsClient, pair string) { defer func() { WsHubs[pair].UnRegister <- c c.WsConn.Close() }() WsHubs[pair].Register <- c go c.WriteMsg(func(string) error {}) c.ReadMsg(func(*WsClient, string){}) }Copy the code

One more thing to note is that we don’t write producers (that is, processes that send data to the Hub) because producers are more flexible, so we’ll write a simple one here.

//init func init() {go Producer()} {for {// generate MSG MSG := "hello, I am legendtkl" // select the proper hub to send the msg WsHubs["hub1"].Broadcast <- msg } }Copy the code

5. Write at the end

One of the questions I’ve been thinking about since I started working on it is, how do you measure code scalability and how do you write code that is highly scalable? Welcome to exchange.

If the demo code above is needed, I will package it and upload it to Github.