├── 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 |
--------------------------------------------------------------------------------