Writing a Websocket Chat in Go
October 29th, 2016 by Micha KopsLearning the Go programming language, I wanted to implement an application that I had written with other languages and frameworks before to get a grip on this language.
That’s why I tried to implement a really simple websocket chat server in Go and described my approach in the following article.
Contents
Writing the Chat Server
This is the implementation of our simple chat server.
Server
Our chat server starts a new HTTP server that servers our static contents like HTML, CSS and JavaScript files from the directory static on port 8080.
In addition, the server initializes four chat rooms.
package chat import ( "log" "net/http" ) // Run starts a new chat server with 4 chat rooms, listening on port 8080 func Run() { fs := http.FileServer(http.Dir("static")) http.Handle("/", fs) for _, name := range []string{"arduino", "java", "go", "scala"} { r := NewRoom(name) http.Handle("/chat/"+name, r) go r.Run() } log.Printf("starting chat server on port 8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal("starting server failed: ", err) } }
Room
Our chat room allows chatters to join, leave or talk in a room and offers convenience methods to create new rooms.
We’re using the Gorilla Websocket library here for our websocket connections to the client.
package chat import ( "log" "net/http" "github.com/gorilla/websocket" ) const ( socketBufferSize = 1024 messageBufferSize = 256 ) // Room represents a single chat room type Room struct { forward chan []byte join chan *Chatter leave chan *Chatter chatters map[*Chatter]bool topic string } var upgrader = &websocket.Upgrader{ ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize, CheckOrigin: func(r *http.Request) bool { return true }, } func (r *Room) ServeHTTP(w http.ResponseWriter, req *http.Request) { socket, err := upgrader.Upgrade(w, req, nil) if err != nil { log.Fatal("serving http failed ", err) return } chatter := &Chatter{ socket: socket, send: make(chan []byte, messageBufferSize), room: r, } r.join <- chatter defer func() { r.leave <- chatter }() go chatter.write() chatter.read() } // NewRoom creates a new chat room func NewRoom(topic string) *Room { return &Room{ forward: make(chan []byte), join: make(chan *Chatter), leave: make(chan *Chatter), chatters: make(map[*Chatter]bool), topic: topic, } } // Run initializes a chat room func (r *Room) Run() { log.Printf("running chat room %v", r.topic) for { select { case chatter := <-r.join: log.Printf("new chatter in room %v", r.topic) r.chatters[chatter] = true case chatter := <-r.leave: log.Printf("chatter leaving room %v", r.topic) delete(r.chatters, chatter) close(chatter.send) case msg := <-r.forward: data := FromJSON(msg) log.Printf("chatter '%v' writing message to room %v, message: %v", data.Sender, r.topic, data.Message) for chatter := range r.chatters { select { case chatter.send <- msg: default: delete(r.chatters, chatter) close(chatter.send) } } } } }
Chatter
Our chatter holds a reference to the selected chat room and delegates his messages to the specific chat room.
package chat import "github.com/gorilla/websocket" // Chatter represents a single Chatter type Chatter struct { socket *websocket.Conn send chan []byte room *Room } func (c *Chatter) read() { for { if _, msg, err := c.socket.ReadMessage(); err == nil { c.room.forward <- msg } else { break } } c.socket.Close() } func (c *Chatter) write() { for msg := range c.send { if err := c.socket.WriteMessage(websocket.TextMessage, msg); err != nil { break } } c.socket.Close() }
Message
This structure is pretty useless as we’re only using it to display some information in our application logs but I wanted to demonstrate the possibilities for marshalling/unmarshalling JSON in Go.
package chat import ( "encoding/json" ) // Message represents a chat message type Message struct { Message string `json:"message"` Sender string `json:"sender"` Created string `json:"created"` } // FromJSON created a new Message struct from given JSON func FromJSON(jsonInput []byte) (message *Message) { json.Unmarshal(jsonInput, &message) return }
Chat User Interface
The user interface running in the browser consists of some simple HTML, CSS and JavaScript.
I’m just recycling the source from my tutorial “Creating a Chat Application using Java EE 7, Websockets and GlassFish 4“ here (but with the websocket port changed to 8080).
Client JavaScript
This simple JavaScript establishes the websocket connection and manages the client communication to the server and updates the user interface.
var wsocket; var serviceLocation = "ws://0.0.0.0:8080/chat/"; var $nickName; var $message; var $chatWindow; var room = ''; function onMessageReceived(evt) { var msg = JSON.parse(evt.data); // native API var $messageLine = $('<tr><td class="received">' + msg.received + '</td><td class="user label label-info">' + msg.sender + '</td><td class="message badge">' + msg.message + '</td></tr>'); $chatWindow.append($messageLine); } function sendMessage() { var msg = '{"message":"' + $message.val() + '", "sender":"' + $nickName.val() + '", "received":""}'; wsocket.send(msg); $message.val('').focus(); } function connectToChatserver() { room = $('#chatroom option:selected').val(); wsocket = new WebSocket(serviceLocation + room); wsocket.onmessage = onMessageReceived; } function leaveRoom() { wsocket.close(); $chatWindow.empty(); $('.chat-wrapper').hide(); $('.chat-signin').show(); $nickName.focus(); } $(document).ready(function() { $nickName = $('#nickname'); $message = $('#message'); $chatWindow = $('#response'); $('.chat-wrapper').hide(); $nickName.focus(); $('#enterRoom').click(function(evt) { evt.preventDefault(); connectToChatserver(); $('.chat-wrapper h2').text('Chat # '+$nickName.val() + "@" + room); $('.chat-signin').hide(); $('.chat-wrapper').show(); $message.focus(); }); $('#do-chat').submit(function(evt) { evt.preventDefault(); sendMessage() }); $('#leave-room').click(function(){ leaveRoom(); }); });
HTML Template
This is just an excerpt, the full template is available at my repository here.
<div class="container chat-signin"> <form class="form-signin"> <h2 class="form-signin-heading">Chat sign in</h2> <label for="nickname">Nickname</label> <input type="text" class="input-block-level" placeholder="Nickname" id="nickname"> <div class="btn-group"> <label for="chatroom">Chatroom</label> <select size="1" id="chatroom"> <option>arduino</option> <option>java</option> <option>go</option> <option>scala</option> </select> </div> <button class="btn btn-large btn-primary" type="submit" id="enterRoom">Sign in</button> </form> </div> <div class="container chat-wrapper"> <form id="do-chat"> <h2 class="alert alert-success"></h2> <table id="response" class="table table-bordered"></table> <fieldset> <legend>Enter your message..</legend> <div class="controls"> <input type="text" class="input-block-level" placeholder="Your message..." id="message" style="height:60px"/> <input type="submit" class="btn btn-large btn-block btn-primary" value="Send message" /> <button class="btn btn-large btn-block" type="button" id="leave-room">Leave room</button> </div> </fieldset> </form> </div>
Running the Chat
We’re now ready to build and run our chat application like this:
$ go get bitbucket.org/hascode/go-websocket-chat/chat $ go run chat.go 2016/10/29 15:03:11 starting chat server on port 8080 2016/10/29 15:03:11 running chat room go 2016/10/29 15:03:11 running chat room scala 2016/10/29 15:03:11 running chat room arduino 2016/10/29 15:03:11 running chat room java 2016/10/29 15:03:56 new chatter in room go 2016/10/29 15:04:42 new chatter in room go 2016/10/29 15:05:09 new chatter in room go 2016/10/29 15:05:23 chatter 'Michelle' writing message to room go, message: Hello there 2016/10/29 15:05:33 chatter 'Tim' writing message to room go, message: Hey, what's up? 2016/10/29 15:08:15 chatter leaving room go 2016/10/29 15:08:18 chatter leaving room go
We may now access our chat in the browser at http://localhost:8080
Tutorial Sources
Please feel free to download the tutorial sources from my Bitbucket repository, fork it there or clone it using Git:
git clone https://bitbucket.org/hascode/go-websocket-chat.git
Resources
Websocket Chat Articles
I have written other tutorials about websocket server and client implementations, please feel to read further:
June 14th, 2018 at 7:05 pm
Hi
thank you very much for your letter
I want to close the chat room.
how can I do that
March 9th, 2020 at 7:09 pm
This example seems almost identical from the book Go: Design Patterns for Real-World Projects. And no credits.