Introduction Link to heading
SimplySocket is built for real-time communication with clients, efficiently managing message broadcasting, room creation, and event handling. It’s designed to scale well for multiplayer games and similar applications that require synchronized messaging across clients.
Features Link to heading
- Message Broadcasting: Efficiently push messages to multiple clients.
- Room Management: Create, delete, and manage rooms for organized communication.
- Event Handling: Trigger and handle various events seamlessly.
Basics Link to heading
A websocket server which is a protocol to do communication between server and client (browser, mobile etc) . Its asynchronized so the usage is in real time systems . Below will understand what are the blocks in the socket system and how SimplySocket manages it .
Client Link to heading
Client is a end user which has connected to our system via a User interface can be a web app, mobile application etc . A client will need 2 concurrent functions one to send the message which it will push from the client side (web,mobile..) and second to receive the message from the server and will get displayed on its side .
flowchart LR C[Server] -.- B C -.- D A[Client
ws://localhost:8080/ws
Browser,Mobile] --> B(Recieve Message
Go Func) D[Write Message
Go Func] -->A
Server Link to heading
A websocket system will have a lots of clients joining it . A basic service which we expect is that a client push the message and all the clients receive it . Server’s job is to maintain the client list and also to push the message to clients via a loop .
flowchart TD C[Server] -.- A[Client 1] C -.- B[Client 2] C -.- D[Client 3] A -->|Send Message
Go Func| C C -->|Recieve Messages
Go Func| A
Rooms Link to heading
Looking a top this 2 logic is enough you can get a chat system ready but yeah you will push it to all the clients and is all open . A notification system but all are the receivers of notification .
A Room is a mid level abstraction kind of thing which will help to club up the clients . Server will have a job to maintain a map[RoomName][]map[Client-Slug]clientprops
. Rooms can also hold specific properties it wants for example total number of client your room can have , add some security layer over that room etc .
flowchart TD C[Server] -.- A[Client 1] C -.- B[Client 2] C -.- D[Client 3] A -->|Send Message
Go Func| E E -->|Recieve Messages
Go Func| A B -->|Send Message
Go Func| E E -->|Recieve Messages
Go Func| B D -->|Send Message
Go Func| F[Room 2] F -->|Recieve Messages
Go Func| D C -.- E[Room 1] C -.- F
Messages Link to heading
Given the concept of rooms above and thinking of benefits it provides one obvious question should be how come a message a client will send to backend will be processed to send to a particular room and rest part will get handled by room . Here comes the concept for a message which we expect from client .
type Message struct {
Action string `json:"action"` //action
MessageBody map[string]interface{} `json:"message_body"` //message
IsTargetClient bool //not imported if its true then the Target string is a client which is one
Target string `json:"target"` //target the room
Sender string `json:"sender"` //whose readpump is used
}
Above one is a snippet from SimplySocket Message which is passed through client and across the system .
- Action :- This is kind of a task which a client is expecting to do for example in a chat system Action :- “send-message” means to broadcast the message . In a game system :- “start-the-game” might mean to trigger certain functions to start the game.
- Message Body:- interface based so can be custom struct . Obvious one will contain main message .
- IsTargetClient :- This one is more oriented towards the backend Server system for example when a client joins a room you want to push a message like
Welcome client xyz
where as to all other you want to displayClient xyz joined a room
. So a simple flag and in Target part add client slug will redirect message to that client only . - Target :- Target is a room or a client where client wants to push the message.
- Sender :- The client slug /id who has initiated the message if its from the server then it will have its name or a client slug .
Example Link to heading
This is the example from wordsbattle codebase which runs miniwordgames.com
NewMeshServer Link to heading
Setup the server where it will have all clients and rooms information , roomdata is your information through which a room global will be created and holds the information which you can access interface .
roomdata := &game.RoomData{RandomRooms: []string{}}
ms := simplysocket.NewMeshServer("cowgame", &simplysocket.MeshServerConfig{DirectBroadCast: false}, roomdata)
// initialize websocket link cowgame connection clash of words
r.HandleFunc("/wsmesh", func(w http.ResponseWriter, r *http.Request) {
simplysocket.ServeWs(ms, w, r)
})
HandleRoomData Link to heading
Implement HandleRoomData function in your codebase . This function is the go routine which runs when a newmeshserver is created . The objects will have the property which you wanted to give and can be accessed . In below funciton RoomData is the struct of the object of newMeshServer created .
func (r *RoomData) HandleRoomData(room simplysocket.Room, server simplysocket.MeshServer) {
roomname := room.GetRoomSlugInfo()
r.Slug = roomname
log.Println("Handeling data Server for ", roomname, server.GetGameName())
// ticker := time.NewTicker(5 * time.Second)
// defer ticker.Stop()
for {
select {
case message, ok := <-room.ConsumeRoomMessage():
//log.Println(server.GetRooms())
if !ok {
log.Println("Channel closed. Exiting HandleRoomData for", roomname)
}
log.Println("Room data ", message)
if message.Target == roomname {
r.handleServermessages(room, server, message)
}
case clientevent := <-room.EventTriggers():
log.Println("Event triggered", clientevent[0], clientevent[1], clientevent[2], room.GetRoomSlugInfo())
// case <-ticker.C:
// log.Println("Room activity server", room.GetRoomSlugInfo(), r.Slug)
case <-room.RoomStopped():
log.Println("Room is stopped so stop the handler")
return
}
}
}
room.GetRoomSlugInfo() Link to heading
room.GetRoomSlugInfo()
will give you the room id
room.GetRoomMakerInfo() Link to heading
room.GetRoomMakerInfo()
will give you the client id which created this room
room.GetClientsInRoom() Link to heading
room.GetClientsInRoom()
will return map[string]map[string]*client
roomids->clientids check example
room.ConsumeMessage() Link to heading
room.ConsumeMessage()
will send messages from the client side to the server . It can be removed if you have a service which push the message .
room.EventTriggers() Link to heading
room.EventTriggers()
will send events it is string list where 1st is the event name which can be client-joined-room , client-left-room , 2nd part is the roomname , 3rd part is clientuniqid . This will help you to implement further logics like here in wordsbattle
room.RoomStopped() Link to heading
room.RoomStopped()
will notify room is stopped this will happen when no client is there in the room . Implement this in custom funciton for better go routine management .
simplysocket.Message Link to heading
This is the standard Message struct which simplysocket process and push it . Keep the same struct pattern in client side as well Action: as same as a Title of message Target: Its a room id or client id MessageBody: map interface of message to be send isTargetClient: flag to send message to a client only
res := &simplysocket.Message{
Action: "room-setting-applied",
Target: message.Target,
MessageBody: map[string]interface{}{"message": "Room settings applied successfully Player Limit:- " + message.MessageBody["player_limit"].(string) + " Time duration :-" + message.MessageBody["game_duration"].(string)},
Sender: "bot-of-the-room",
IsTargetClient: false,
}
room.BroadcastMessage(message) Link to heading
room.BroadcastMessage(message)
will broadcast the message . room will have the client list in it . This will push message to all clients in it but if it needs to be send to a particular client set IsTargetClient to true
in message.
func (r *RoomData) FailToJoinRoomNotify(reason string, clientsinroom []string, room simplysocket.Room, server simplysocket.MeshServer) {
reasonmsg := ""
log.Println("Client removed", clientsinroom[2])
if reason == "room-full" {
reasonmsg = "Failed to join the room its occupied"
}
message := &simplysocket.Message{
Action: "fail-join-room-notify",
Target: clientsinroom[2],
MessageBody: map[string]interface{}{
"message": reasonmsg,
},
Sender: "bot-of-the-room",
IsTargetClient: true,
}
room.BroadcastMessage(message)
}
server.JoinClientRoom(roomname string, clientname string, rd RoomData) Link to heading
server.JoinClientRoom(roomname string, clientname string, rd RoomData)
:- This function takes roomname uniq id , client uniq slug id and a object of the struct which you have created . So the approach is simplysocket checks whether a room is already there a roomname which you added if not then it will use object and create a room and it will add your client . SimplySocket self initializes a room mesh-global
where a client is automatically added . It contains list of all clients in it. This mechanism will help you to create different rooms with different properties to play with .
A room created with a object of particular struct must need to implement HandleRoomData
. Incase of wordsbattle there are 2 type of rooms one which is mesh-global has its implementation) and a game room implementation .
case "join-room":
//needed only if new room is needed
rd := GameRoomData{
IsRandomGame: false,
PlayerLimit: int(message.MessageBody["playerlimit"].(float64)),
ClientProperties: make(map[string]*ClientProps),
GameEnded: make(chan bool),
Wordslist: make(map[string]bool),
Endtime: 1 * 60,
Rounds: 0,
TurnAttempted: make(chan []string),
HasGameStarted: false,
HasGameEnded: false,
ClientTurnList: []*ClientProps{},
}
log.Println("JoinRoomAction ", message.Sender, message.MessageBody, room.GetRoomSlugInfo())
roomname := message.MessageBody["roomname"].(string)
server.JoinClientRoom(roomname, message.Sender, &rd)
log.Println("request send to join a room")
Contributing Link to heading
We welcome contributions! Please feel free to submit issues, pull requests, or suggestions.