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.
- If the room is empty, add me in.
- 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())
}
}