Writing a Websocket Chat in Go

October 29th, 2016 by

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.

Go Websocket Chat Implementation

Go Websocket Chat Implementation

 

Writing the Chat Server

Websocket Chat in Action

Websocket Chat in Action

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

Websocket Chat Login Screen

Websocket Chat Login Screen

Websocket Chat in Action

Websocket Chat in Action

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:

Tags: , , , , ,

Leave a Reply

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 116,048 bad guys.

Search
Categories