├── LICENSE ├── README.md ├── client.go ├── documentation.md ├── go.mod ├── go.sum ├── mesh_server.go ├── message.go ├── message_test.go ├── projectstructure.md └── room_manager.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dhruvik Donga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimplySocket 2 | 3 | SimplySocket is a Golang package designed for pushing messages to clients and managing rooms and events. This package is a part of the Words Battle game project. 4 | 5 | Checkout documentation :- [here](https://github.com/DhruvikDonga/simplysocket/wiki) 6 | 7 | Checkout [wordsbattle multiplayer game](https://github.com/DhruvikDonga/wordsbattle) which uses simplysocket 8 | 9 | - [SimplySocket](#simplysocket) 10 | - [Introduction](#introduction) 11 | - [Features](#features) 12 | - [Project Architecture](#project-architecture) 13 | - [Contributing](#contributing) 14 | - [License](#license) 15 | - [Documentation](#documentation) 16 | 17 | ## Introduction 18 | 19 | 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. 20 | 21 | 22 | ### Features 23 | 24 | - **Message Broadcasting:** Efficiently push messages to multiple clients. 25 | - **Room Management:** Create, delete, and manage rooms for organized communication. 26 | - **Event Handling:** Trigger and handle various events seamlessly. 27 | 28 | ### Project Architecture 29 | check the [Project Architecure](projectstructure.md) file. 30 | 31 | ### Contributing 32 | 33 | We welcome contributions! Please feel free to submit issues, pull requests, or suggestions. 34 | 35 | ### License 36 | 37 | This project is licensed under the MIT License - see the LICENSE file for details. 38 | 39 | 40 | ## Documentation 41 | 42 | For more detailed information, check the [Documentation](documentation.md) file. 43 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package simplysocket 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 15 | const charintset = "0123456789" 16 | 17 | var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) 18 | 19 | const ( 20 | // Max wait time when writing message to peer 21 | writeWait = 10 * time.Second 22 | 23 | // Max time till next pong from peer 24 | pongWait = 60 * time.Second 25 | 26 | // Send ping interval, must be less then pong wait time 27 | pingPeriod = (pongWait * 9) / 10 28 | 29 | // Maximum message size allowed from peer. 30 | maxMessageSize = 10000 31 | ) 32 | 33 | var upgrader = websocket.Upgrader{ 34 | ReadBufferSize: 4096, 35 | WriteBufferSize: 4096, 36 | } 37 | 38 | var ( 39 | newline = []byte{'\n'} 40 | space = []byte{' '} 41 | ) 42 | 43 | type client struct { 44 | slug string 45 | 46 | authMetadata []string //client can have a list of authorization metadata for secure rooms 47 | conn *websocket.Conn 48 | meshServer *meshServer //keep reference of webserver to every client 49 | send chan []byte 50 | } 51 | 52 | // newClient initialize new websocket client like App server in routes.go 53 | func newClient(wscon *websocket.Conn, m *meshServer, name string) *client { 54 | //random name 55 | a := make([]byte, 5) 56 | for i := range a { 57 | a[i] = charset[seededRand.Intn(len(charset))] 58 | } 59 | b := make([]byte, 3) 60 | for i := range b { 61 | b[i] = charintset[seededRand.Intn(len(charintset))] 62 | } 63 | randomname := string(a) + string(b) 64 | c := &client{ 65 | slug: randomname, 66 | conn: wscon, 67 | meshServer: m, 68 | send: make(chan []byte, 256), //needs to be buffered cause it should not block when channel is not receiving from broadcast 69 | } 70 | m.clientConnect <- c //we are registering the client 71 | return c 72 | } 73 | 74 | // readPump Goroutine, the client will read new messages send over the WebSocket connection. It will do so in an endless loop until the client is disconnected. When the connection is closed, the client will call its own disconnect method to clean up. 75 | // upon receiving new messages the client will push them in the LobbyServer broadcast channel. 76 | func (client *client) readPump() { 77 | defer func() { 78 | client.disconnect() 79 | }() 80 | 81 | client.conn.SetReadLimit(maxMessageSize) 82 | // Frontend client will give a pong message to the routine we have to handle it 83 | client.conn.SetReadDeadline(time.Now().Add(pongWait)) 84 | client.conn.SetPongHandler(func(appData string) error { client.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 85 | 86 | // Start endless read loop, waiting for message from client 87 | for { 88 | _, jsonMessage, err := client.conn.ReadMessage() 89 | if err != nil { 90 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 91 | log.Printf("\n ReadPump unexepected close error: %v", err) 92 | break 93 | } 94 | break 95 | } 96 | 97 | var message Message 98 | if err := json.Unmarshal(jsonMessage, &message); err != nil { 99 | log.Printf("ReadPump Error on unmarshal JSON message %s", err) 100 | } 101 | message.Sender = client.slug 102 | client.meshServer.mu.Lock() 103 | roomtosend := client.meshServer.rooms[message.Target] 104 | client.meshServer.mu.Unlock() 105 | 106 | select { 107 | case roomtosend.consumeMessage <- &message: 108 | default: 109 | log.Println("Failed to send to Room name ", roomtosend.slug) 110 | 111 | } 112 | } 113 | } 114 | 115 | // writePump goroutine handles sending the messages to the connected client. It runs in an endless loop waiting for new messages in the client.send channel. When receiving new messages it writes them to the client, if there are multiple messages available they will be combined in one write. 116 | // writePump is also responsible for keeping the connection alive by sending ping messages to the client with the interval given in pingPeriod. If the client does not respond with a pong, the connection is closed. 117 | func (client *client) writePump() { 118 | ticker := time.NewTicker(pingPeriod) 119 | defer func() { 120 | ticker.Stop() 121 | client.conn.Close() 122 | }() 123 | 124 | for { 125 | select { 126 | case message, ok := <-client.send: 127 | client.conn.SetWriteDeadline(time.Now().Add(writeWait)) 128 | if !ok { // not ok means send channel has been closed caused by disconnect() in readPump() 129 | client.conn.WriteMessage(websocket.CloseMessage, []byte{}) 130 | return 131 | } 132 | w, err := client.conn.NextWriter(websocket.TextMessage) 133 | if err != nil { 134 | return 135 | } 136 | w.Write(message) 137 | 138 | // Attach queued chat messages to the current websocket message. 139 | n := len(client.send) 140 | for i := 0; i < n; i++ { 141 | w.Write(newline) 142 | w.Write(<-client.send) 143 | } 144 | 145 | if err := w.Close(); err != nil { 146 | return 147 | } 148 | 149 | case <-ticker.C: //make a ping request 150 | client.conn.SetWriteDeadline(time.Now().Add(writeWait)) 151 | if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 152 | return 153 | } 154 | } 155 | } 156 | } 157 | 158 | func (client *client) disconnect() { 159 | m := client.meshServer 160 | m.clientDisconnect <- client 161 | close(client.send) //close the sending channel 162 | client.conn.Close() //close the client connection 163 | } 164 | 165 | // ServeWs handles websocket requests from clients requests. 166 | func ServeWs(meshserv *meshServer, w http.ResponseWriter, r *http.Request) { 167 | 168 | upgrader.CheckOrigin = func(r *http.Request) bool { 169 | return true 170 | } 171 | conn, err := upgrader.Upgrade(w, r, nil) 172 | if err != nil { 173 | log.Println("Failed to intiazlize websocket connection:-", err) 174 | return 175 | } 176 | 177 | name := r.URL.Query().Get("name") // ws://url?name=dumm_name 178 | roles := r.Header.Get("Role") 179 | if len(name) < 1 { 180 | name = "Guest" 181 | } 182 | 183 | client := newClient(conn, meshserv, name) 184 | client.authMetadata = strings.Split(roles, ",") 185 | 186 | go client.readPump() 187 | go client.writePump() 188 | 189 | } 190 | -------------------------------------------------------------------------------- /documentation.md: -------------------------------------------------------------------------------- 1 | 2 | # SimplySocket Documentation 3 | - [SimplySocket Documentation](#simplysocket-documentation) 4 | - [Installation](#installation) 5 | - [Understanding basics of Server , Room, Message and Client](#understanding-basics-of-server--room-message-and-client) 6 | - [Client](#client) 7 | - [Server](#server) 8 | - [Rooms](#rooms) 9 | - [Messages](#messages) 10 | 11 | ## Installation 12 | 13 | ```go get github.com/DhruvikDonga/simplysocket``` 14 | 15 | ## Understanding basics of Server , Room, Message and Client 16 | 17 | 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 . 18 | 19 | ### Client 20 | 21 | Client is a end user which has connected to our system via a User interface can be a web app, mobile application etc . 22 | 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 . 23 | 24 | ```mermaid 25 | flowchart LR 26 | C[Server] -.- B 27 | C -.- D 28 | A[Client
ws://localhost:8080/ws
Browser,Mobile] --> B(Recieve Message
Go Func) 29 | D[Write Message
Go Func] -->A 30 | ``` 31 | 32 | ### Server 33 | 34 | 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 . 35 | 36 | ```mermaid 37 | flowchart TD 38 | C[Server] -.- A[Client 1] 39 | C -.- B[Client 2] 40 | C -.- D[Client 3] 41 | A -->|Send Message
Go Func| C 42 | C -->|Recieve Messages
Go Func| A 43 | ``` 44 | 45 | ### Rooms 46 | 47 | 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 . 48 | 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 . 49 | 50 | ```mermaid 51 | flowchart TD 52 | C[Server] -.- A[Client 1] 53 | C -.- B[Client 2] 54 | C -.- D[Client 3] 55 | A -->|Send Message
Go Func| E 56 | E -->|Recieve Messages
Go Func| A 57 | B -->|Send Message
Go Func| E 58 | E -->|Recieve Messages
Go Func| B 59 | D -->|Send Message
Go Func| F[Room 2] 60 | F -->|Recieve Messages
Go Func| D 61 | C -.- E[Room 1] 62 | C -.- F 63 | ``` 64 | 65 | ### Messages 66 | 67 | 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 . 68 | 69 | type Message struct { 70 | Action string `json:"action"` //action 71 | MessageBody map[string]interface{} `json:"message_body"` //message 72 | IsTargetClient bool //not imported if its true then the Target string is a client which is one 73 | Target string `json:"target"` //target the room 74 | Sender string `json:"sender"` //whose readpump is used 75 | } 76 | Above one is a snippet from SimplySocket Message which is passed through client and across the system . 77 | 78 | - **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. 79 | - **Message Body**:- interface based so can be custom struct . Obvious one will contain main message . 80 | - **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 display `Client xyz joined a room` . So a simple flag and in Target part add client slug will redirect message to that client only . 81 | - **Target** :- Target is a room or a client where client wants to push the message. 82 | - **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 . 83 | 84 | 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DhruvikDonga/simplysocket 2 | 3 | go 1.21.3 4 | 5 | require github.com/gorilla/websocket v1.5.1 6 | 7 | require golang.org/x/net v0.17.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 2 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 3 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 4 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 5 | -------------------------------------------------------------------------------- /mesh_server.go: -------------------------------------------------------------------------------- 1 | package simplysocket 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | ) 7 | 8 | const ( 9 | MeshGlobalRoom = "mesh-global" //MeshGlobalRoom is a room where a client gets joined when he connects to a websocket . MeshGlobalRoom facilitates creation of rooms 10 | ) 11 | 12 | type MeshServerConfig struct { 13 | DirectBroadCast bool 14 | } 15 | 16 | type MeshServer interface { 17 | GetClients() map[string]*client 18 | GetClientAuthMetadata(clientslug string) []string 19 | GetRooms() []string 20 | GetGameName() string 21 | GetClientsInRoom() map[string]map[string]*client 22 | DeleteRoom(name string) 23 | JoinClientRoom(roomname string, clientname string, rd RoomData) 24 | RemoveClientRoom(roomname string, clientname string) 25 | //PushMessage is to push message from the code not from the UI thats broadcast 26 | //returns a send only channel 27 | PushMessage() chan<- *Message 28 | //ReceiveMessage is to receive message from readpumps of the clients this can be used to manipulate 29 | //returns a receive only channel 30 | RecieveMessage() <-chan *Message 31 | //EventTriggers Track 32 | //Get the updates on the clients in room changes and act accordingly 33 | //Returns receive only channel []string length of 3 [0]-->event type [1]-->roomname [1]-->clientslug 34 | //event types :- client-joined-room , client-left-room 35 | EventTriggers() <-chan []string 36 | } 37 | 38 | // meshServer runs like workers which are light weight instead of using rooms approach this reduces weight on rooms side 39 | // this helps for a user to connect simultaneously multiple rooms in a single go 40 | type meshServer struct { 41 | mu sync.RWMutex 42 | 43 | gamename string 44 | isbroadcaston bool 45 | clients map[string]*client 46 | rooms map[string]*room 47 | clientsinroom map[string]map[string]*client 48 | roomcnt int 49 | clientConnect chan *client 50 | clientDisconnect chan *client 51 | 52 | roomCreate chan []string //[clientid,roomid] who created this room to save it as a first player in that room 53 | roomDelete chan string 54 | 55 | clientJoinedRoom chan []interface{} //[0]-->roomslug [1]-->clientslug [2]--> RoomData 56 | clientLeftRoom chan []string //[0]-->roomslug [1]-->clientslugs 57 | 58 | processMessage chan *Message 59 | clientInRoomEvent chan []string //[event type,room name, client slug] , client-joined-room, client-left-room 60 | 61 | roomdata RoomData 62 | } 63 | 64 | // NewMeshServer initialize new websocket server 65 | func NewMeshServer(name string, meshconf *MeshServerConfig, rd RoomData) *meshServer { 66 | server := &meshServer{ 67 | mu: sync.RWMutex{}, 68 | gamename: name, 69 | roomcnt: 0, 70 | isbroadcaston: meshconf.DirectBroadCast, 71 | clients: make(map[string]*client), 72 | rooms: make(map[string]*room), 73 | clientsinroom: make(map[string]map[string]*client), 74 | 75 | clientConnect: make(chan *client, 1), 76 | clientDisconnect: make(chan *client, 1), 77 | 78 | roomCreate: make(chan []string, 1), 79 | roomDelete: make(chan string, 1), 80 | 81 | clientJoinedRoom: make(chan []interface{}, 1), 82 | clientLeftRoom: make(chan []string, 1), 83 | 84 | processMessage: make(chan *Message, 1), //unbuffered channel unlike of send of client cause it will recieve only when readpump sends in it else it will block 85 | clientInRoomEvent: make(chan []string, 1), //view into the maps is your room affected by client changes 86 | 87 | roomdata: rd, 88 | } 89 | r := &room{ 90 | id: server.roomcnt, 91 | slug: MeshGlobalRoom, 92 | createdby: "Gawd", 93 | stopped: make(chan struct{}), 94 | roomdata: rd, 95 | server: server, 96 | consumeMessage: make(chan *Message, 1), 97 | clientInRoomEvent: make(chan []string, 1), 98 | } 99 | server.roomcnt += 1 100 | 101 | server.rooms[MeshGlobalRoom] = r 102 | go func() { 103 | server.roomdata.HandleRoomData(r, server) 104 | }() 105 | go func() { 106 | server.RunMeshServer() 107 | }() 108 | 109 | return server 110 | } 111 | 112 | // Run mesh server accepting various requests 113 | func (server *meshServer) RunMeshServer() { 114 | for { 115 | select { 116 | case client := <-server.clientConnect: 117 | server.connectClient(client) //add the client 118 | 119 | case client := <-server.clientDisconnect: 120 | server.disconnectClient(client) //remove the client 121 | 122 | case roomcreate := <-server.roomCreate: 123 | server.createRoom(roomcreate[0], roomcreate[1], server.roomdata) //add the client 124 | 125 | case roomname := <-server.roomDelete: 126 | server.DeleteRoom(roomname) //remove the client 127 | 128 | case message := <-server.processMessage: //this broadcaster will broadcast to all clients 129 | roomtosend := server.rooms[message.Target] 130 | select { 131 | case roomtosend.consumeMessage <- message: 132 | default: 133 | log.Println("Failed to send to Room name ", roomtosend.slug) 134 | 135 | } 136 | } 137 | } 138 | } 139 | 140 | func (server *meshServer) GetClients() map[string]*client { 141 | return server.clients 142 | } 143 | 144 | func (server *meshServer) GetClientAuthMetadata(clientslug string) []string { 145 | return server.clients[clientslug].authMetadata 146 | } 147 | 148 | func (server *meshServer) GetGameName() string { 149 | return server.gamename 150 | } 151 | 152 | func (server *meshServer) GetRooms() []string { 153 | roomslist := []string{} 154 | 155 | server.mu.Lock() 156 | defer server.mu.Unlock() 157 | for room := range server.rooms { 158 | roomslist = append(roomslist, room) 159 | } 160 | return roomslist 161 | } 162 | 163 | func (server *meshServer) GetClientsInRoom() map[string]map[string]*client { 164 | server.mu.Lock() 165 | res := server.clientsinroom 166 | server.mu.Unlock() 167 | return res 168 | } 169 | 170 | func (server *meshServer) PushMessage() chan<- *Message { 171 | return server.processMessage 172 | } 173 | 174 | func (server *meshServer) RecieveMessage() <-chan *Message { 175 | return server.processMessage 176 | } 177 | 178 | func (server *meshServer) EventTriggers() <-chan []string { 179 | return server.clientInRoomEvent 180 | } 181 | 182 | func (server *meshServer) connectClient(client *client) { 183 | server.mu.Lock() 184 | server.clients[client.slug] = client 185 | server.mu.Unlock() 186 | server.JoinClientRoom(MeshGlobalRoom, client.slug, server.roomdata) //join this default to a room this is a global room kind of main lobby 187 | } 188 | 189 | func (server *meshServer) disconnectClient(client *client) { 190 | server.mu.Lock() 191 | defer server.mu.Unlock() 192 | for roomname, clientsmap := range server.clientsinroom { 193 | if _, ok := clientsmap[client.slug]; ok { 194 | delete(clientsmap, client.slug) 195 | delete(server.rooms[roomname].clientsinroom, client.slug) 196 | select { 197 | case server.rooms[roomname].clientInRoomEvent <- []string{"client-left-room", roomname, client.slug}: 198 | default: 199 | log.Println("Failed to trigger left room trigger for client ", client.slug, " in room", roomname) 200 | } 201 | if roomname != MeshGlobalRoom { 202 | if len(clientsmap) == 0 && roomname != MeshGlobalRoom { 203 | delete(server.clientsinroom, roomname) 204 | //server.DeleteRoom(roomname) 205 | if r, ok := server.rooms[roomname]; ok { 206 | close(r.stopped) 207 | delete(server.rooms, roomname) 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | delete(server.clients, client.slug) 215 | 216 | } 217 | 218 | func (server *meshServer) createRoom(name string, client string, rd RoomData) { 219 | 220 | room := NewRoom(name, client, rd, server) 221 | 222 | server.mu.Lock() 223 | server.rooms[room.slug] = room //add it to server list of rooms 224 | server.mu.Unlock() 225 | 226 | } 227 | 228 | func (server *meshServer) DeleteRoom(name string) { 229 | server.mu.Lock() 230 | defer server.mu.Unlock() 231 | if r, ok := server.rooms[name]; ok { 232 | close(r.stopped) 233 | delete(server.rooms, name) 234 | } 235 | 236 | } 237 | 238 | func (server *meshServer) JoinClientRoom(roomname string, clientname string, rd RoomData) { 239 | noroom := false 240 | server.mu.RLock() 241 | if _, ok := server.rooms[roomname]; !ok { 242 | noroom = true 243 | 244 | } 245 | server.mu.RUnlock() 246 | if noroom { 247 | server.createRoom(roomname, clientname, rd) 248 | } 249 | server.mu.Lock() 250 | for roomkey := range server.rooms { 251 | if roomkey == roomname { 252 | if clientinroom, ok := server.clientsinroom[roomkey]; ok { 253 | clientinroom[clientname] = server.clients[clientname] 254 | } else { 255 | server.clientsinroom[roomkey] = map[string]*client{} 256 | server.clientsinroom[roomkey][clientname] = server.clients[clientname] 257 | } 258 | //copy it to the room and keep it updated 259 | server.rooms[roomname].clientsinroom = server.clientsinroom[roomname] 260 | select { 261 | case server.rooms[roomname].clientInRoomEvent <- []string{"client-joined-room", roomname, clientname}: 262 | default: 263 | log.Println("Failed to trigger join room trigger for client ", clientname, " in room", roomname) 264 | } 265 | 266 | server.mu.Unlock() 267 | return 268 | 269 | } 270 | } 271 | 272 | } 273 | 274 | func (server *meshServer) RemoveClientRoom(roomname string, clientname string) { 275 | server.mu.Lock() 276 | defer server.mu.Unlock() 277 | if clientsmap, ok := server.clientsinroom[roomname]; ok { 278 | delete(clientsmap, clientname) 279 | delete(server.rooms[roomname].clientsinroom, clientname) 280 | server.clientInRoomEvent <- []string{"client-left-room", roomname, clientname} 281 | select { 282 | case server.rooms[roomname].clientInRoomEvent <- []string{"client-left-room", roomname, clientname}: 283 | default: 284 | log.Println("Failed to trigger left room trigger for client ", clientname, " in room", roomname) 285 | } 286 | if len(clientsmap) == 0 && roomname != MeshGlobalRoom { 287 | delete(server.clientsinroom, roomname) 288 | //server.DeleteRoom(roomname) 289 | if r, ok := server.rooms[roomname]; ok { 290 | close(r.stopped) 291 | delete(server.rooms, roomname) 292 | } 293 | } 294 | } 295 | 296 | } 297 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package simplysocket 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | // Message struct is the structure of the message which is send in mesh server 9 | type Message struct { 10 | Action string `json:"action"` //action 11 | MessageBody map[string]interface{} `json:"message_body"` //message 12 | IsTargetClient bool //not imported if its true then the Target string is a client which is one 13 | Target string `json:"target"` //target the room 14 | Sender string `json:"sender"` //whose readpump is used 15 | } 16 | 17 | func (message *Message) Encode() []byte { 18 | json, err := json.Marshal(message) 19 | if err != nil { 20 | log.Println(err) 21 | } 22 | 23 | return json 24 | } 25 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package simplysocket 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMessage_Encode(t *testing.T) { 8 | message := &Message{ 9 | Action: "test_action", 10 | MessageBody: map[string]interface{}{ 11 | "key": "value", 12 | }, 13 | IsTargetClient: true, 14 | Target: "target_client", 15 | Sender: "sender_client", 16 | } 17 | 18 | encoded := message.Encode() 19 | expected := `{"action":"test_action","message_body":{"key":"value"},"IsTargetClient":true,"target":"target_client","sender":"sender_client"}` 20 | 21 | if string(encoded) != expected { 22 | t.Errorf("Expected %s, got %s", expected, string(encoded)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projectstructure.md: -------------------------------------------------------------------------------- 1 | ## Architecture 2 | 3 | ```mermaid 4 | graph LR 5 | node1(Message Processor) 6 | node4(Observer Lobby Server) 7 | node5(wsclient1) 8 | node2(Message Reciever 1) 9 | node3(Message Sender 1) 10 | node6(Algorithm and room state watcher) 11 | node7(wsclient2) 12 | node8(Message Reciever 2) 13 | node9(Message Sender 2) 14 | node10(wsclient3) 15 | node11(Message Reciever 3) 16 | node12(Message Sender 3) 17 | node13(wsclient4) 18 | node14(Message Reciever 4) 19 | node15(Message Sender 4) 20 | node16(Message Processor arch) 21 | node17[clients list] 22 | node18(client1 message sender) 23 | node19(client2 message sender) 24 | node20(client3 message sender) 25 | 26 | node21[[Client connected]] 27 | node22[[Client disconnected]] 28 | node23[[Room created]] 29 | node24[[Room deleted]] 30 | node25[[Client Joined a Room]] 31 | node26[[Client Left a Room]] 32 | node27[[Event Triggers]] 33 | 34 | node28[(Clients Map
cid_key--struct)] 35 | node29[(Rooms Map
roomname_key--struct)] 36 | node30[(Room to client map
roomkey--map-clinetkeys)] 37 | 38 | node31[[Message Send in a processor
1. Action to be done/message type
2. Room address
3. If not 2nd then client address
4. Message body]] 39 | 40 | node31-->node16 41 | node16-->node17 42 | node17-->node18 43 | node17-->node19 44 | node17-->node20 45 | node17-->node30 46 | 47 | 48 | node27-->node21 49 | node27-->node22 50 | node27-->node23 51 | node27-->node24 52 | node27-->node25 53 | node27-->node26 54 | 55 | node21-->node28 56 | node22-->node28 57 | 58 | node23-->node29 59 | node24-->node29 60 | 61 | node25-->node30 62 | node26-->node30 63 | node30-->node28 64 | node28-->node17 65 | 66 | node2-->node1-->node3 67 | node8-->node1-->node9 68 | node11-->node1-->node12 69 | node14-->node1-->node15 70 | 71 | node6-->node1 72 | node5-.->node2 73 | node5-.->node3 74 | 75 | node7-.->node8 76 | node7-.->node9 77 | 78 | node10-.->node11 79 | node10-.->node12 80 | 81 | node13-.->node14 82 | node13-.->node15 83 | 84 | 85 | node4-.->node5 86 | node4-.->node6 87 | node4-.->node1 88 | node4-.->node7 89 | node4-.->node10 90 | node4-.->node13 91 | ``` 92 | -------------------------------------------------------------------------------- /room_manager.go: -------------------------------------------------------------------------------- 1 | package simplysocket 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | ) 7 | 8 | type RoomData interface { 9 | //HandleRoomData use your struct which has all the data related to room and do the changes accordingly 10 | HandleRoomData(room Room, server MeshServer) 11 | } 12 | 13 | type Room interface { 14 | GetRoomSlugInfo() string 15 | GetRoomMakerInfo() string 16 | GetAuthMetadata() []string 17 | // This is to indicate that there are no clients in the room to send the message 18 | // If there are no clients in the room the room gets deleted from the map and this channel is closed. 19 | // The HandleRoomData go routine will be closed if implemented. 20 | RoomStopped() <-chan struct{} 21 | //ConsumeRoomMessage receives the messages it gets directly from the clients. 22 | ConsumeRoomMessage() <-chan *Message 23 | //This are the events such as client-joined-room,client-left-room . 24 | //Consist of list of 3 values :- [event,roomname,clientid] 25 | EventTriggers() <-chan []string 26 | //BroadcastMessage pushes the message to all the clients in the room . 27 | //Use IsTargetClient to true if you have to send the message to a particular client of the room . 28 | BroadcastMessage(message *Message) 29 | } 30 | 31 | type room struct { 32 | mu sync.RWMutex 33 | 34 | id int 35 | server *meshServer 36 | authMetadata []string 37 | slug string 38 | createdby string //client id who created it 39 | stopped chan struct{} 40 | roomdata RoomData 41 | consumeMessage chan *Message 42 | clientInRoomEvent chan []string 43 | clientsinroom map[string]*client 44 | } 45 | 46 | func NewRoom(roomslug string, clientslug string, rd RoomData, srv *meshServer) *room { 47 | srv.roomcnt += 1 48 | 49 | r := &room{ 50 | mu: sync.RWMutex{}, 51 | id: srv.roomcnt, 52 | slug: roomslug, 53 | createdby: clientslug, 54 | stopped: make(chan struct{}, 1), 55 | roomdata: rd, 56 | server: srv, 57 | consumeMessage: make(chan *Message, 1), 58 | clientInRoomEvent: make(chan []string, 1), 59 | clientsinroom: make(map[string]*client), 60 | } 61 | go func() { 62 | r.roomdata.HandleRoomData(r, srv) 63 | }() 64 | log.Println("room created and running", roomslug) 65 | 66 | return r 67 | } 68 | 69 | func (room *room) GetRoomSlugInfo() string { 70 | return room.slug 71 | } 72 | 73 | func (room *room) GetRoomMakerInfo() string { 74 | return room.createdby 75 | } 76 | 77 | func (room *room) GetAuthMetadata() []string { 78 | return room.authMetadata 79 | } 80 | 81 | func (room *room) RoomStopped() <-chan struct{} { 82 | return room.stopped 83 | } 84 | 85 | func (room *room) ConsumeRoomMessage() <-chan *Message { 86 | return room.consumeMessage 87 | } 88 | 89 | func (room *room) EventTriggers() <-chan []string { 90 | return room.clientInRoomEvent 91 | } 92 | 93 | func (room *room) BroadcastMessage(message *Message) { 94 | room.mu.RLock() 95 | defer room.mu.RUnlock() 96 | jsonBytes := message.Encode() 97 | if message.IsTargetClient { 98 | 99 | client := room.clientsinroom[message.Target] 100 | 101 | client.send <- jsonBytes 102 | } else { 103 | clients := room.clientsinroom 104 | for _, c := range clients { 105 | c.send <- jsonBytes 106 | } 107 | } 108 | 109 | } 110 | --------------------------------------------------------------------------------