DEV Community

William Spader
William Spader

Posted on

Go Channels: Creating the Simplest TCP Chat only w/ STD

I strongly believe that the best way to learn a new programming language is by doing side projects.

In this article we will write the simplest TCP Chat only using STD in Go, after that you can improve and add features as you want.

We can start talking about the imports needed for this project

imports, types and structs

import (
    "log"
    "net"
    "time"
)

type MessageType int

type Message struct {
    Conn net.Conn
    Type MessageType
    Text []byte
}

const (
    NewClient MessageType = iota
    DisconnectedClient
    NewTextClient
)
Enter fullscreen mode Exit fullscreen mode

log: add time information for each printed log and writes to os.Stderr by default.
net: will help us to listen to TCP protocol and to stablish a connnection.
time: for throttling implementation, don't need to worry right now.

main()

func main() {
    ln := startServer()

    channel := make(chan Message)
    go Chat(channel)

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println("Could not accept connection. ", err)
        }

        channel <- NewMessage(conn, NewClient, nil)
        log.Println("connection accepted. ", conn.RemoteAddr())

        go HandleConnection(conn, channel)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can talk about each line to better understand and adding code as needed.
The line ln := startServer() calls a method that returns a TCP listener.

startServer()

func startServer() net.Listener {
    ln, err := net.Listen("tcp", ":9595")
    if err != nil {
        log.Fatalf("Could not listen on port %s. Shutting down ...\n", Port)
    }

    log.Printf("Listening on port %s\n", Port)
    return ln
}
Enter fullscreen mode Exit fullscreen mode

We call net.Listen("tcp", ":9595") to create a TCP listener on port 9595. Then, if something goes wrong there isn't much we can do, so we log and exit the app.
log.Fatalf() writes to stderr and exit the app.
If the listener worked, we return to main().

go Chat(channel)

Our application will have 1 go routine for each connected user, so we need a channel to communicate between go routines. When a user sends a message, we need to send that message to all users.

func Chat(broadcastChan chan Message) {
    clients := make(map[string]net.Conn)
    lastNewTextClient := make(map[string]time.Time)
    for {
        msg := <-broadcastChan
        if msg.Type == NewClient {
            clients[msg.Conn.RemoteAddr().String()] = msg.Conn
            log.Println("New client = ", msg.Conn.RemoteAddr().String())
        } else if msg.Type == DisconnectedClient {
            delete(clients, msg.Conn.RemoteAddr().String())
            msg.Conn.Close()
            log.Println("Client disconnected. Connection closed.")
        } else if msg.Type == NewTextClient {
            lastTime := lastNewTextClient[msg.Conn.RemoteAddr().String()]
            if !lastTime.IsZero() && lastTime.After(time.Now().Add(-time.Second*5)) {
                msg.Conn.Write([]byte("The time elapse between messages is 5 seconds."))
            } else {
                lastNewTextClient[msg.Conn.RemoteAddr().String()] = time.Now()
                for _, conn := range clients {
                    if conn.RemoteAddr().String() == msg.Conn.RemoteAddr().String() {
                        continue
                    }
                    conn.Write(msg.Text)
                }
            }
        } else {
            log.Println("Unknown message type received = ", msg.Type)
        }
    }

Enter fullscreen mode Exit fullscreen mode

This function has another infinite for-loop, so we can keep the connection alive with the user.
We create a map of users to add and remove users from the app as needed.
We also create a map to keep track of the last message from a user, so each user can only send a new message after 5 seconds.
The line msg := <-broadcastChan await for the next message from the channel.
If it is a NewClient, then add this client to the map of users.
If it is a DisconnectedClient, then remove this client from the map of users and close the connection.
If it is a NewTextClient, then we iterate over the users and send the message to all other users except the one who sent it.

infinite for-loop

We open a infinite for-loop so the server stay alive indefinitely. Inside the for-loop we call ln.Accept(), this function blocks the routine until a new connection arrives and return this connection to us i.e. the conn variable

channel <- NewMessage(conn, NewClient, nil)

If the ln.Accept() worked, we send a message to the channel to inform that a new user has arrived.
Now, the NewMessage function is defined as

func NewMessage(conn net.Conn, msgType MessageType, buffer []byte) Message {
    if msgType == NewClient {
        return Message{Conn: conn, Type: NewClient}
    } else if msgType == DisconnectedClient {
        return Message{Conn: conn, Type: DisconnectedClient}
    } else if msgType == NewTextClient {
        return Message{Conn: conn, Type: NewTextClient, Text: buffer}
    } else {
        return Message{Conn: conn}
    }
}
Enter fullscreen mode Exit fullscreen mode

go HandleConnection(conn, channel)

Finally, we have the implementation of the last function from main()

func HandleConnection(conn net.Conn, channel chan Message) {
    for {
        buffer := make([]byte, 512)
        _, err := conn.Read(buffer)
        if err != nil {
            channel <- NewMessage(conn, DisconnectedClient, nil)
            break
        }

        channel <- NewMessage(conn, NewTextClient, buffer)
    }
}
Enter fullscreen mode Exit fullscreen mode

If there is any errors to read the user message, we disconnect the client and break the connection after close it.
If we successfully read the message, we send the message to the channel.
Don'f forget, all messages sent to the channel will be handled by the Chat(channel) function, as is the only moment in the app that read from the channel.

Now, you can improve this code and add new features. This app has only one chat for all users, so one idea can be to add users to groups.

Hope this article helps to better understand the usage of channels in practice!

Top comments (0)