I’ve been hearing a lot about websockets lately and how they can accomplish real time communication between applications and servers. They act as a compliment and possible alternative to RESTful APIs that have been around for significantly longer. With websockets you can do real time messaging for things like chat, communication with IoT, gaming, and a whole lot of other things that need instant communication between clients and the server.
A while back I had played around with websockets and Node.js using a library called Socket.io, but since I’ve been really getting into Golang I wanted to explore websockets using the Go programming language.
We’re going to check out how to create a chat application where the client is an Angular application and the server is a Golang application.
There are many moving pieces in this application so there are a few requirements that must be met in order to be successful. The requirements are as follows:
The chat server which will orchestrate all the messages and clients will be written in the Go programming language. The client front-end will be written in Angular which has a dependency of the Node Package Manager (NPM) which ships with Node.js.
We’re going to start by developing the server component of our application which has a few external dependencies that must be downloaded prior.
From the Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:
go get github.com/gorilla/websocket
go get github.com/satori/go.uuid
The websocket library was created by the same people who made the very popular Mux routing library. We need a UUID library so we can assign each chat client a unique id value.
Create a new project at your $GOPATH which will represent our chat server. My project will be found in the $GOPATH/src/github.com/nraboy/realtime-chat/main.go file.
Before going forward, it is important to note that I obtained a lot of the Golang code from other sources. To avoid being an offender of plagiarism, I obtained portions of the code from Dinosaurs Code and the Gorilla websocket chat example. However, I’ve included many of my own unique changes to the project as well.
The application will have three custom data structures:
type ClientManager struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
type Client struct {
id string
socket *websocket.Conn
send chan []byte
}
type Message struct {
Sender string `json:"sender,omitempty"`
Recipient string `json:"recipient,omitempty"`
Content string `json:"content,omitempty"`
}
The ClientManager
will keep track of all the connected clients, clients that are trying to become registered, clients that have become destroyed and are waiting to be removed, and messages that are to be broadcasted to and from all connected clients.
Each Client
has a unique id, a socket connection, and a message waiting to be sent.
To add complexity to the data being passed around, it will be in JSON format. Instead of passing around a string of data which cannot easily be tracked we are passing around JSON data. With JSON we can have meta and other useful things. Each of our messages will contain information regarding who sent the message, who is receiving the message and the actual content of the message.
Let’s spin up a global ClientManager
for our application to use:
var manager = ClientManager{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
The server will use three goroutines, one for managing the clients, one for reading websocket data, and one for writing websocket data. The catch here is that the read and write goroutines will get a new instance for every client that connects. All goroutines will run on a loop until they are no longer needed.
Starting with the server goroutine we have the following:
func (manager *ClientManager) start() {
for {
select {
case conn := <-manager.register:
manager.clients[conn] = true
jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected."})
manager.send(jsonMessage, conn)
case conn := <-manager.unregister:
if _, ok := manager.clients[conn]; ok {
close(conn.send)
delete(manager.clients, conn)
jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected."})
manager.send(jsonMessage, conn)
}
case message := <-manager.broadcast:
for conn := range manager.clients {
select {
case conn.send <- message:
default:
close(conn.send)
delete(manager.clients, conn)
}
}
}
}
}
Every time the manager.register
channel has data, the client will be added to the map of available clients managed by the client manager. After adding the client, a JSON message is sent to all other clients, not including the one that just connected.
If a client disconnects for any reason, the manager.unregister
channel will have data. The channel data in the disconnected client will be closed and the client will be removed from the client manager. A message announcing the disappearance of a socket will be sent to all remaining connections.
If the manager.broadcast
channel has data it means that we’re trying to send and receive messages. We want to loop through each managed client sending the message to each of them. If for some reason the channel is clogged or the message can’t be sent, we assume the client has disconnected and we remove them instead.
To save repetitive code, a manager.send
method was created to loop through each of the clients:
func (manager *ClientManager) send(message []byte, ignore *Client) {
for conn := range manager.clients {
if conn != ignore {
conn.send <- message
}
}
}
How the data is sent, with conn.send
will be explored later in this guide.
Now we can explore the goroutine for reading websocket data sent from the clients. The point of this goroutine is to read the socket data and add it to the manager.broadcast
for further orchestration.
func (c *Client) read() {
defer func() {
manager.unregister <- c
c.socket.Close()
}()
for {
_, message, err := c.socket.ReadMessage()
if err != nil {
manager.unregister <- c
c.socket.Close()
break
}
jsonMessage, _ := json.Marshal(&Message{Sender: c.id, Content: string(message)})
manager.broadcast <- jsonMessage
}
}
If there was an error reading the websocket data it probably means the client has disconnected. If that is the case we need to unregister the client from our server.
Remember the conn.send
that we saw previously? This is handled in the third goroutine for writing data:
func (c *Client) write() {
defer func() {
c.socket.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
c.socket.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.socket.WriteMessage(websocket.TextMessage, message)
}
}
}
If the c.send
channel has data we try to send the message. If for some reason the channel is not alright, we will send a disconnect message to the client.
So how do we start each of these goroutines? The server goroutine will be started when we start our server and each of the other goroutines will start when someone connects.
For example, check out the main
function:
func main() {
fmt.Println("Starting application...")
go manager.start()
http.HandleFunc("/ws", wsPage)
http.ListenAndServe(":12345", nil)
}
We start the server on port 12345 and it has a single endpoint which is only accessible via a websocket connection. This endpoint method called wsPage
looks like the following:
func wsPage(res http.ResponseWriter, req *http.Request) {
conn, error := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(res, req, nil)
if error != nil {
http.NotFound(res, req)
return
}
client := &Client{id: uuid.NewV4().String(), socket: conn, send: make(chan []byte)}
manager.register <- client
go client.read()
go client.write()
}
The HTTP request is upgraded to a websocket request using the websocket library. By adding a CheckOrigin
we can accept requests from outside domains eliminating cross origin resource sharing (CORS) errors.
When a connection is made, a client is created and a unique id is generated. This client is registered to the server as seen previously. After client registration, the read and write goroutines are triggered.
At this point the Golang application can be run with the following:
go run *.go
You cannot test it in your web browser, but a websocket connection can be established at ws://localhost:12345/ws.
Now we need to create a client facing application where we can send and receive the messages. Assuming you’ve already installed the Angular CLI, execute the following to create a fresh project:
ng new SocketExample
This will be a single page application and what we hope to accomplish can be seen in the animated image found below.
The JavaScript websocket management will happen from within an Angular provider class. Using the Angular CLI, create a provider by executing the following:
ng g service socket
The above command should create src/app/socket.service.ts and src/app/socket.service.spec.ts within your project. The spec file is for unit testing, something we won’t explore in this particular example. Open the src/app/socket.service.ts file and include the following TypeScript code:
import { Injectable, EventEmitter } from '@angular/core';
@Injectable()
export class SocketService {
private socket: WebSocket;
private listener: EventEmitter<any> = new EventEmitter();
public constructor() {
this.socket = new WebSocket("ws://localhost:12345/ws");
this.socket.onopen = event => {
this.listener.emit({"type": "open", "data": event});
}
this.socket.onclose = event => {
this.listener.emit({"type": "close", "data": event});
}
this.socket.onmessage = event => {
this.listener.emit({"type": "message", "data": JSON.parse(event.data)});
}
}
public send(data: string) {
this.socket.send(data);
}
public close() {
this.socket.close();
}
public getEventListener() {
return this.listener;
}
}
This provider will be injectable and emit data based on certain events. In the constructor
method a websocket connection to the Golang application is established and three event listeners are created. One event listener for each socket creation and destruction as well as a listener for when messages come in.
The send
method will allow us to send messages to the Golang application and the close
method will allow us to tell the Golang application that we are no longer connected.
The provider is created, but it cannot yet be used within each page of our application. To do this we need to add it to the project’s @NgModule
block found in the project’s src/app/app.module.ts file. Open it and include the following:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { SocketService } from "./socket.service";
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [SocketService],
bootstrap: [AppComponent]
})
export class AppModule { }
Notice we’ve imported the provider and added it to the providers
array of the @NgModule
block.
Now we can focus on the page logic for our application. Open the project’s src/app/app.component.ts file and include the following TypeScript code:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocketService } from "./socket.service";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
public messages: Array<any>;
public chatBox: string;
public constructor(private socket: SocketService) {
this.messages = [];
this.chatBox = "";
}
public ngOnInit() {
this.socket.getEventListener().subscribe(event => {
if(event.type == "message") {
let data = event.data.content;
if(event.data.sender) {
data = event.data.sender + ": " + data;
}
this.messages.push(data);
}
if(event.type == "close") {
this.messages.push("/The socket connection has been closed");
}
if(event.type == "open") {
this.messages.push("/The socket connection has been established");
}
});
}
public ngOnDestroy() {
this.socket.close();
}
public send() {
if(this.chatBox) {
this.socket.send(this.chatBox);
this.chatBox = "";
}
}
public isSystemMessage(message: string) {
return message.startsWith("/") ? "<strong>" + message.substring(1) + "</strong>" : message;
}
}
In the constructor
method of the above AppComponent
class we inject our provider and initialize our public variables that are bound to the UI. It is never a good idea to load or subscribe to events within the constructor
method so instead we use the ngOnInit
method.
public ngOnInit() {
this.socket.getEventListener().subscribe(event => {
if(event.type == "message") {
let data = event.data.content;
if(event.data.sender) {
data = event.data.sender + ": " + data;
}
this.messages.push(data);
}
if(event.type == "close") {
this.messages.push("/The socket connection has been closed");
}
if(event.type == "open") {
this.messages.push("/The socket connection has been established");
}
});
}
In the above method we are subscribing to the event listener we had created in the provider class. In it we check to see what kind of event we found. If the event is a message then we check to see if there was a sender and prepend it to the message.
You’ll probably notice that some messages are starting with a slash. I’m using this to represent system messages which we’ll later bold.
Upon destruction, the close event is sent to the server and if the chatbox is sent, the message is sent to the server.
Before we look at the HTML, let’s add some CSS so it looks like a more legitimate chat application. Open the project’s src/styles.css file and include the following:
/* You can add global styles to this file, and also import other style files */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
Now let’s have a look at the HTML markup. Open the project’s src/app/app.component.html file and include the following:
<ul id="messages">
<li *ngFor="let message of messages">
<span [innerHTML]="isSystemMessage(message)"></span>
</li>
</ul>
<form action="">
<input [(ngModel)]="chatBox" [ngModelOptions]="{standalone: true}" autocomplete="off" />
<button (click)="send()">Send</button>
</form>
We simply loop through the messages
array and display them on the screen. Any message that starts with a slash will be bolded. The form is bound to a public variable and when the send button is pressed, it will be sent to the Golang server.
You just saw how to create a websocket real time chat application using Golang and Angular. While we aren’t storing a history of the chats in this particular example, the logic can be applied to much more complicated projects that include gaming, IoT, and plenty of other use cases.