WebSocket server

Introduction

Setup Server

Using GoLang, it is easy to set up a WebSocket server listening at localhost port 8888 by writing.

func main() {
    http.Handle("/api/v3/", websocket.Handler(ccyyHandler))

    if err := http.ListenAndServe(":8888", nil); err != nil {
        glog.Errorf("%s", err.Error())
    }
}

Pull from connection

Each request from the client-side will be forwarded to a "ccyyHandler" function for processing. The handler will build a "PlayerConn" struct to store the connection. Recall WebSocket connection is a long and duplex connection. This means we will reuse this "ws" multiple times instead of creating it whenever we need to send data back to the server. Therefore we need to bind the "Connection" and "PlayerName" together for future use.

"PullFromClient" will constantly try to pull content from the receiving WebSocket. Each content pulled will start a new thread to process content. in GoLang the keyword "go" is used to start a new thread.

type PlayerConn struct {
    PlayerName string
    Connection *websocket.Conn
}

func ccyyHandler(ws *websocket.Conn) {
    NetDataConnTmp := &PlayerConn{
        Connection: ws,
        PlayerName: "",
    }
    NetDataConnTmp.PullFromClient()
}

func (c *PlayerConn) PullFromClient() {
    for {
        var content string
        if err := websocket.Message.Receive(c.Connection, &content); err != nil {
            break
        }
        if len(content) == 0 {
            break
        }
        go c.ProcessContent(content)
    }
}

Unserialise json content

Here we try to unmarshal JSON bytes to a map object with string as key and interface{} as value, interface{} here basically means a pointer to some memory space, which we could not know its type right now. When using WebSocket, it is our coder's responsibility to define the protocol and, based on the protocol, to encode and decode the content. Here, we extract "Proto" and "Proto1" to learn what protocol we are using. Hence we could decode content accordingly

type RequestBody struct {
    req string
}

func (r *RequestBody) JsonString2Map() (result map[string]interface{}, err error) {
    if err = json.Unmarshal([]byte(r.req), &result); err != nil {
        return nil, err
    }
    return result, nil
}

func (c *PlayerConn) ProcessContent(content string) {
    glog.Infof("receive content: %s\n", content)

    var r RequestBody
    r.req = content
    if data, err := r.JsonString2Map(); err != nil {
        glog.Errorf("failed to process content %s\n", err.Error())
    } else {
        c.HandleProtocol(data["Proto"], data["Proto1"], data)
    }
}

Define protocol

Before decoding content based on the protocol, let us define our protocol first XD. For a large project like this, usually, a one-layer protocol is not enough. Here we use a two-layer protocol structure with "Proto" as the primary protocol, defining the main class of the protocol and "Proto1" as the sub proto, defining the behavior under each main class. For example, "Proto" determines if the protocol is used for the Game server or DB server. "Proto1" under Game server defines the protocol between client-server communications like logging in, choosing a room, and uploading paint. For each protocol defined, we will also designate a structure (data) to send or receive. Right now, please focus on the paint protocols and structs as we have the login functionality implemented in firebase. And I will explain why it is named "GameServer" later.

const (
	INIT_PROTO    = iota
	Game_Proto    // 1, Main Protocol
	Game_DB_Proto // 2, DB Protocol
)

const (
	INIT_PROTO1 = iota

	C2S_PlayerLogin_Proto1 // 1   C2S = Client to Server
	S2C_PlayerLogin_Proto1 // 2   S2C = Server to Client

	C2S_PlayerChooseRoom_Proto1 // 3
	S2C_PlayerChooseRoom_Proto1 // 4

	C2S_PlayerUploadPaint_Proto1 // 5
	S2C_PlayerUploadPaint_Proto1 // 6
	S2C_PlayerUploadPaintOther_Proto1 // 7
)

type PlayerSt struct {
	UID         int
	PlayerName  string
	OpenID      string
}

type C2S_PlayerLogin struct {
	Proto       int
	Proto1      int
	IType       int      // 1,login 2,register
	Code        string
}

type S2C_PlayerLogin struct {
	Proto       int
	Proto1      int
	Player      PlayerSt
}

type C2S_PlayerUploadPaint struct {
	Proto       int
	Proto1      int
	PlayerName  string
	Img         string
	PlayerId    string
}

type S2C_PlayerUploadPaint struct {
	Proto       int
	Proto1      int
	PlayerName  string
	PlayerId    string
	Probability float32
	Prediction  int
	Category    string
}

type S2C_PlayerUploadPaintOther struct {
	Proto       int
	Proto1      int
	PlayerName  string
	PlayerId    string
}

Handle protocol

Now let us dispatch our data based on the protocol. We build two switches to pass our data to the correct function according to our protocol. If the data sent from the client has "Proto = Game_Proto" and "Proto1 = C2S_PlayerUplaodPaint_Proto1", then this is a message to our Upload function.

func (c *PlayerConn) HandleProtocol(protocol interface{}, protocol1 interface{}, data map[string]interface{}) {

    switch protocol {
    case float64(proto.Game_Proto):
        c.HandleProtocol1(protocol1, data)
    case float64(proto.Game_DB_Proto):
    default:
        glog.Errorln("failed to handle main protocol")
    }
}


func (c *PlayerConn) HandleProtocol1(protocol1 interface{}, data map[string]interface{}) {
    switch protocol1 {
    case float64(proto1.C2S_PlayerLogin_Proto1):
        c.Login(data)
    case float64(proto1.C2S_PlayerChooseRoom_Proto1):
    case float64(proto1.C2S_PlayerUploadPaint_Proto1):
        c.UploadPaintAndPredict(data)
    case float64(proto1.S2C_PlayerUploadPaint_Proto1):
    default:
        glog.Errorln("failed to handle sub protocol 1")
    }
}

Upload and Predict

Finally, we are going to start implementing our core logic. The first is to extract "PlayerName", "PlayerId", "Img" from the data using reflection. "PlayerName" is the email registered on firebase. "PlayerId" is the UID generated by firebase when creating the user. These two values are helpful for pairing and starting a new chat with each other. The third is the "Img", which is the UUID of the user drawings uploaded with HTTP server [check HTTP Server section]. Then, we called predict to get the probability, index, and category [check AI Serving section]. Lastly, we start a new thread for pairing based on the prediction.

func (c *PlayerConn) UploadPaintAndPredict(data map[string]interface{}) {
    if data["PlayerName"] == nil || data["PlayerId"] == nil || data["Img"] == nil {
        return
    }
    playerName := data["PlayerName"].(string)
    playerId := data["PlayerId"].(string)
    image := data["Img"].(string)

    c.PlayerName = playerName
    G_PlayerData[playerName] = c

    glog.Infoln("user upload paint: ", playerName, playerId, image)

    probability, index, category := paint(image)
    glog.Infoln("predict upload paint: ", probability, index, category)

    go c.Pair(playerName, playerId, index, c.Connection, probability, category)
}

Pair and Notify

We can treat our user paring functionality as a game paring functionality we experience every day (If you play multi-player games every day like me). We send people to rooms and let them start playing together. In our case, a pairing is simply a room of two people. We first create 345 rooms (channels), since we have 345 possible prediction categories. Each room (category) is uniquely labeled with a number. Then simply add users to the rooms. Here we have two scenarios.

  1. If the room is empty, add me in.
  2. If the room is not empty, pickup the person from the room, and lets chat (send back peer's info through websocket).

Rooms are implemented using channels, since GoLang recommands CSP model for concurrency, and context are used for timeous.

type PlayerInRoom struct {
    PlayerName string
    PlayerId string
}

var G_Rooms [345]chan PlayerInRoom

func InitRooms() {
    for i := range G_Rooms {
        G_Rooms[i] = make(chan PlayerInRoom, 2) 
    }
}

func (c *PlayerConn) Pair (playerName string, playerId string, roomNumber int, probability float32, category string) {
    timeoutCtx, cancel := context.WithTimeout(G_BaseCtx, 2*time.Second)
    defer cancel()
    ticker := time.NewTicker(500 * time.Millisecond)
    round := 0
    for range ticker.C {
        select {
        //case <-time.After(5 * time.Second):
        case <-timeoutCtx.Done():
            glog.Infoln("time up, add myself %s to the channel", playerName)

            // noone in the room, add me in
            G_Rooms[roomNumber] <- PlayerInRoom{
                PlayerName: playerName,
                PlayerId: playerId,
            }
            return
        case playerInRoom := <-G_Rooms[roomNumber]:
            glog.Infof("get other player %s at room %d, round %d", playerInRoom.PlayerName, roomNumber, round)
            
            // send me your info
            PlayerSendMessageToServer(c.Connection, 
                &proto1.S2C_PlayerUploadPaint{
                    Proto: proto.Game_Proto,
                    Proto1: proto1.S2C_PlayerUploadPaint_Proto1,
                    PlayerName : playerInRoom.PlayerName,
                    PlayerId: playerInRoom.PlayerId,
                    Probability: probability,
                    Prediction: roomNumber,
                    Category: category,
                })
            
            // send you my info
            PlayerSendMessageToServer(G_PlayerData[playerInRoom.PlayerName].Connection, 
                &proto1.S2C_PlayerUploadPaintOther{
                    Proto: proto.Game_Proto,
                    Proto1: proto1.S2C_PlayerUploadPaintOther_Proto1,
                    PlayerName : playerName,
                    PlayerId : playerId,
                })
            return
        default:
            glog.Infof("no one responding now at round %d", round)
        }
        round++
    }
}

func PlayerSendMessageToServer(conn *websocket.Conn, data interface{}) {
    dataSend, err := json.Marshal(data)
    if err != nil {
        glog.Errorln("data marchal failed", err.Error())
    }
    glog.Infof("dataSend: %s\n", string(dataSend[:]))
    if err := websocket.JSON.Send(conn, dataSend); err != nil {
        glog.Errorln("send failed", err.Error())
    }

}