A few months back I wrote about using websockets in a Golang application for communication with an Angular client web application. While very useful and simplistic, in many cases websockets won’t be the means for real-time communication between applications. It is often easier or better to use standard TCP network sockets as an alternative. For example, if you’re developing an online video game, it will likely communicate to the server using TCP sockets rather than websockets.
We’re going to see how to create a basic chat application using the Go programming language. This chat application will have a server for listening and routing client communications and a client for sending and receiving messages from the server.
Let’s figure out at a high level what we’re going to be doing in this application. When we start the server a management process will be started. This management service will keep track of connected clients and queue messages to each of the clients. For the server, we will listen for connections and when established, the management process will keep track of them in addition to a new send and receive process being started. The server will have one management process and then one send and receive process for every connection.
To be clear, when I say process I mean goroutine in Golang.
When starting the application as a client, things are handled a bit differently. There will be no management process because the client should not be managing connections. Instead a process for receiving data will be started and the application will allow for sending of data.
Not too bad of a plan right?
Before we get into the heavy lifting, let’s get a project going with the bare essentials. This will act as a foundation to the two components that come next.
Somewhere in your $GOPATH create a project with a main.go file. This file should contain the following:
package main
import (
"bufio"
"flag"
"fmt"
"net"
"os"
"strings"
)
type ClientManager struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
type Client struct {
socket net.Conn
data chan []byte
}
func startServerMode() { }
func startClientMode() { }
func main() {
flagMode := flag.String("mode", "server", "start in client or server mode")
flag.Parse()
if strings.ToLower(*flagMode) == "server" {
startServerMode()
} else {
startClientMode()
}
}
In the above code we define our imports and data structures as well as a router for starting either a client or server based upon what came in via the command line.
The Client
structure will hold information about the socket connection and data to be sent. The ClientManager
structure will hold all of the available clients, received data, and potential incoming or terminating clients. Anything that uses chan
represents a channel for use with our goroutines to prevent locking or other problems that might exist in a concurrent application.
For more information on building concurrent applications with Go, check out a previous tutorial I wrote called, Concurrent Golang Applications with Goroutines and Channels.
We’re going to start with developing the server component of our application since it is the more complex. If you haven’t already seen my previous example on websockets, it is worth checking out as things will be similar.
Let’s start by creating the management goroutine function. Within the main.go file, add the following function:
func (manager *ClientManager) start() {
for {
select {
case connection := <-manager.register:
manager.clients[connection] = true
fmt.Println("Added new connection!")
case connection := <-manager.unregister:
if _, ok := manager.clients[connection]; ok {
close(connection.data)
delete(manager.clients, connection)
fmt.Println("A connection has terminated!")
}
case message := <-manager.broadcast:
for connection := range manager.clients {
select {
case connection.data <- message:
default:
close(connection.data)
delete(manager.clients, connection)
}
}
}
}
}
This goroutine will run for the lifespan of the server. If it reads data from the register
channel, the connection will be stored and a status will be printed in the logs. If the unregister
channel has data and that data which represents a connection, exists in our managed clients map, then the data channel for that connection will be closed and the connection will be removed from the list.
This brings us to messaging. If the broadcast
channel has data it means we’ve received a message. This message should be sent to every connection we’re watching so this is done by looping through the available connections. If we can’t send the message to a client, that client is closed and removed from the list of managed clients.
So what does receiving look like for the server? We need to make a receive
function that looks like the following:
func (manager *ClientManager) receive(client *Client) {
for {
message := make([]byte, 4096)
length, err := client.socket.Read(message)
if err != nil {
manager.unregister <- client
client.socket.Close()
break
}
if length > 0 {
fmt.Println("RECEIVED: " + string(message))
manager.broadcast <- message
}
}
}
Remember, the above function will be a goroutine, which will exist for every connection that is established. For as long as the goroutine is available, it will be receiving data from a particular client. If there was an error, for example the connection broke, the client will be unregistered and formally closed. If everything went well and the message received wasn’t empty, it will be added to the broadcast
channel to be distributed to all clients by the manager.
When it comes to distributing these messages, they are do so via a send
function:
func (manager *ClientManager) send(client *Client) {
defer client.socket.Close()
for {
select {
case message, ok := <-client.data:
if !ok {
return
}
client.socket.Write(message)
}
}
}
If the client has data to be sent and there are no errors, that data will be sent to the client in question. If there is an error and the loop breaks, the connection to the socket will end.
Now we can bring all these server functions together via the startServerMode
function:
func startServerMode() {
fmt.Println("Starting server...")
listener, error := net.Listen("tcp", ":12345")
if error != nil {
fmt.Println(error)
}
manager := ClientManager{
clients: make(map[*Client]bool),
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
}
go manager.start()
for {
connection, _ := listener.Accept()
if error != nil {
fmt.Println(error)
}
client := &Client{socket: connection, data: make(chan []byte)}
manager.register <- client
go manager.receive(client)
go manager.send(client)
}
}
In the above function we define where we are planning to listen for connections. We then initialize our manager and start the manager goroutine. While the manager is doing its thing, we do a continuous loop listening for connections. If a connection is accepted, it will be registered and prepared for sending and receiving of data.
Now we can focus on the client side of things.
This is actually the easy part of the application, although, the server side wasn’t particularly complex assuming you understand goroutines.
The client will have a continuously running goroutine for receiving data. The receive
function will look like this:
func (client *Client) receive() {
for {
message := make([]byte, 4096)
length, err := client.socket.Read(message)
if err != nil {
client.socket.Close()
break
}
if length > 0 {
fmt.Println("RECEIVED: " + string(message))
}
}
}
The above function is similar to what we saw for the server with the exception that we aren’t taking our received messages to a central processor. After messages are received, they are printed, end of story. If there was a read error, maybe there was an issue with the server, the connection will close.
This takes us to the startClientMode
function:
func startClientMode() {
fmt.Println("Starting client...")
connection, error := net.Dial("tcp", "localhost:12345")
if error != nil {
fmt.Println(error)
}
client := &Client{socket: connection}
go client.receive()
for {
reader := bufio.NewReader(os.Stdin)
message, _ := reader.ReadString('\n')
connection.Write([]byte(strings.TrimRight(message, "\n")))
}
}
Instead of listening on a particular port, we are dialing it. If the connection to the server was successful, the client is created and we start attempting to receive messages. While messages are continuously receiving, we will be continuously allowing user input for message sending. When the enter key is pressed, text will be sent and the cycle will continue.
We covered a lot of ground, so it might be easier to look at the application as a whole. Somewhere in your $GOPATH you’ll want a main.go file like previously mentioned. It should contain the following:
package main
import (
"bufio"
"flag"
"fmt"
"net"
"os"
"strings"
)
type ClientManager struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
type Client struct {
socket net.Conn
data chan []byte
}
func (manager *ClientManager) start() {
for {
select {
case connection := <-manager.register:
manager.clients[connection] = true
fmt.Println("Added new connection!")
case connection := <-manager.unregister:
if _, ok := manager.clients[connection]; ok {
close(connection.data)
delete(manager.clients, connection)
fmt.Println("A connection has terminated!")
}
case message := <-manager.broadcast:
for connection := range manager.clients {
select {
case connection.data <- message:
default:
close(connection.data)
delete(manager.clients, connection)
}
}
}
}
}
func (manager *ClientManager) receive(client *Client) {
for {
message := make([]byte, 4096)
length, err := client.socket.Read(message)
if err != nil {
manager.unregister <- client
client.socket.Close()
break
}
if length > 0 {
fmt.Println("RECEIVED: " + string(message))
manager.broadcast <- message
}
}
}
func (client *Client) receive() {
for {
message := make([]byte, 4096)
length, err := client.socket.Read(message)
if err != nil {
client.socket.Close()
break
}
if length > 0 {
fmt.Println("RECEIVED: " + string(message))
}
}
}
func (manager *ClientManager) send(client *Client) {
defer client.socket.Close()
for {
select {
case message, ok := <-client.data:
if !ok {
return
}
client.socket.Write(message)
}
}
}
func startServerMode() {
fmt.Println("Starting server...")
listener, error := net.Listen("tcp", ":12345")
if error != nil {
fmt.Println(error)
}
manager := ClientManager{
clients: make(map[*Client]bool),
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
}
go manager.start()
for {
connection, _ := listener.Accept()
if error != nil {
fmt.Println(error)
}
client := &Client{socket: connection, data: make(chan []byte)}
manager.register <- client
go manager.receive(client)
go manager.send(client)
}
}
func startClientMode() {
fmt.Println("Starting client...")
connection, error := net.Dial("tcp", "localhost:12345")
if error != nil {
fmt.Println(error)
}
client := &Client{socket: connection}
go client.receive()
for {
reader := bufio.NewReader(os.Stdin)
message, _ := reader.ReadString('\n')
connection.Write([]byte(strings.TrimRight(message, "\n")))
}
}
func main() {
flagMode := flag.String("mode", "server", "start in client or server mode")
flag.Parse()
if strings.ToLower(*flagMode) == "server" {
startServerMode()
} else {
startClientMode()
}
}
If you want to see this application in action, you can execute the following commands:
go run *.go --mode server
go run *.go --mode client
Of course the above two commands should be executed from separate Terminal or Command Prompt windows.
You just saw how to create a socket client and server written with the Go programming language. This was the next level to my previous article titled, Create a Real Time Chat App with Golang, Angular, and Websockets. Having familiarity with network sockets is great because they are fast, real-time, and great for many things such as games and Internet of Things (IoT).
If you’re having trouble wrapping your head around goroutines and channels, check out my previous article on the topic.