Create A Real Time Chat App With Golang, Angular 2, And Websockets

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 2 application and the server is a Golang application.

The Requirements

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 2 which has a dependency of the Node Package Manager (NPM) which ships with Node.js.

Building a Golang Chat Server

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.

Building an Angular 2 Chat Client

Now we need to create a client facing application where we can send and receive the messages. Assuming you’ve already installed the Angular 2 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.

Golang Angular 2 Chat Example

The JavaScript websocket management will happen from within an Angular 2 provider class. Using the Angular 2 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.

Conclusion

You just saw how to create a websocket real time chat application using Golang and Angular 2. 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.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

Subscribe

Subscribe to my newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

Subscribe on YouTube

Support This Site