Learning 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.
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 GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/go-websocket-chat.git
Websocket Chat Articles
I have written other tutorials about websocket server and client implementations, please feel to read further: