DEV Community

Cover image for Building A TCP Bridger Server in Go - Part 1
MJ Pooladkhay
MJ Pooladkhay

Posted on

Building A TCP Bridger Server in Go - Part 1

What is going on?!

I was experimenting on how to transfer data between two clients using a tcp connection.
I began by building a server capable of connecting two clients in order to be able to send textual messages back and forth over a TCP connection.
Then tried to transfer voice packets using that connection and realized I can build different clients that can utilize the power of the same server to transfer, well anything!

In this 3 or 4 part series, I'll show you how to build such a server with two clients.

What is TCP?

According to OSI Model, TCP is defined inside Transport Layer which is Layer 4. That's where the notion of ports like 80, 22, etc. is added to the previous layer, Network.
Protocols like HTTP are build on top of TCP. They are basically a set of standards that define how messages should be transferred and interpreted.

What are the Requirements?

  • Intermediate understanding of Go language
  • Some knowledge about networks

What...?!

Let's start by building a tcp server that listens on a specific port, like 4004.

// Start func main()
func main() {
    var port string
    flag.StringVar(&port, "p", "4004", "port number")
    flag.Parse()

    addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%s", port))
    if err != nil {
        log.Fatalln(err)
    }

    l, err := net.ListenTCP("tcp", addr)
    if err != nil {
        log.Fatalln(err)
    }
    defer l.Close()
// ...
Enter fullscreen mode Exit fullscreen mode

ResolveTCPAddr returns an address of TCP end point. Then we provide it to ListenTCP which in turn, gives us a pointer to net.TCPListener.

TCPListener internally consists of a network file descriptor which is basically a file descriptor to a socket bound to that 4004 port and a ListenConfig struct that contains options for listening to an address.

We also defer the Close() function on our newly created listener to make sure it gets closed when the outer function returns.

I'm not gonna discuss how the internals really work for now, but let me know if you're interested in a deep dive.

Accepting Connections and spawning a new go routine for each one:

// ...

fmt.Printf("waiting for clients on port %s...\n", port)
for {
    c, err := l.AcceptTCP()
    if err != nil {
        fmt.Println(err)
        return
    }
    defer c.Close()
    fmt.Printf("client connected: %s\n", c.RemoteAddr().String())

    _, err = c.Write([]byte("Hello, Client :)\nYou deserve a new Go routine!\n"))
    if err != nil {
        fmt.Println("err greeting user: ", err)
        return
    }
    go cmdHandler(c)

}
// End func main()
Enter fullscreen mode Exit fullscreen mode

Inside a for loop, we will wait for clients to reach out. AcceptTCP blocks until a new client is connected. Then it returns a pointer to net.TCPConn.
net.TCPConn contains another network file descriptor that again points to a socket.

It is really important to understand that each net.TCPConn returned from Accept() is unique in the sense that it's source IP and port are different from others.
We will come back to this topic later when we start building a client for our server.

Again, defer the Close() function since we are dealing with a file descriptor like net.TCPListener

Each time you open a file or in general, anything that gives you a file descriptor, you have to close it when you no longer need it.

net.TCPConn has some methods on it. Read() and Write() are two of them. In fact, it implements both io.Reader and io.Writer interfaces.
That's what we expect from a connection, right?
We want to be able to both read from and write to it.

c.RemoteAddr().String() returns a string representation of newly connected user in the form of ip:port. That's the port I was talking about which is unique for each net.TCPConn.

You can navigate to official documentations to see all available methods on net.TCPConn.

So, after a successful connection, we send a message back to user using Write() method which accepts a byte slice and returns number of bytes written and an error.

There are also other ways to send data to clients. For example fmt.Fprint(w io.Writer, a ...interface{}).
It takes an io.Writer (which we now know that net.TCPConn is one such thing) and writes the provided data to it.

_, err = fmt.Fprint(c, "Hello, Client :)\nYou deserve a new Go routine!\n")
if err != nil {
    fmt.Println("err greeting user: ", err)
    return
}
Enter fullscreen mode Exit fullscreen mode

If everything goes as planned, we reach to the point that we spawn a go routine for each client with it's associated net.TCPConn. As you probably know, this is accomplished by calling go cmdHandler(c).

Now let's see how cmdHandler() function looks like.
Although we have bigger plans as the subject suggests, for now we just read the textual data sent by each client and print it to standard output of the server machine.

func cmdHandler(c *net.TCPConn) {
    defer c.Close()
    for {
        msg, err := bufio.NewReader(c).ReadString('\n')
        if err != nil {
            if err == io.EOF {
                fmt.Printf("client '%s' left the session.\n", c.RemoteAddr().String())
                return
            }
            fmt.Println(err)
            return
        }
        fmt.Printf("%s -> %s", c.RemoteAddr().String(), msg)
    }
}
Enter fullscreen mode Exit fullscreen mode

Well, we start by entering a loop.
bufio.NewReader(c).ReadString('\n') blocks the loop until is reads the provided element from the Reader.
In our case, Reader is a *net.TCPConn and the element we are waiting for to occur is '\n' (LF).

Let's talk a little bit about buffered I/O.
If you ever try to run this program and connect to it using a tool like telnet, you will see when you write something on the client side, as soon as you press enter, the data gets printed on the server side.
You might think the moment you press enter, the data gets sent but that's not always the case.
Data is constantly coming from a client, we want to interpret it as we wish. so we use something called a buffer to store it only print it when it's somehow complete. in our case, when the data contains a line feed ('\n').
But be aware that tools like telnet may themselves have an internal buffer, so they won't send data to the server until you press enter.

So cmdHandler prints the data sent by client to server's standard output.

When the connection is closed manually or by the client, the last message is the EOF which indicates the End-Of-File.

And finally c.RemoteAddr().String() returns the string representation of the remote address.

I think here is a good place to end the part 1.
Let me know your ideas in the comments section and wait for part 2.

Here is the complete code for ctrl+c / ctrl+v lovers like me:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
)

func main() {
    var port string
    flag.StringVar(&port, "p", "4004", "port number")
    flag.Parse()

    addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%s", port))
    if err != nil {
        log.Fatalln(err)
    }

    l, err := net.ListenTCP("tcp", addr)
    if err != nil {
        log.Fatalln(err)
    }
    defer l.Close()

    fmt.Printf("waiting for clients on port %s...\n", port)
    for {
        c, err := l.AcceptTCP()
        if err != nil {
            fmt.Println(err)
            return
        }
        defer c.Close()
        fmt.Printf("client connected: %s\n", c.RemoteAddr().String())

        _, err = c.Write([]byte("Hello, Client!\nYou deserve a new Go routine...\n"))
        if err != nil {
            fmt.Println("err greeting user: ", err)
            return
        }
        go cmdHandler(c)
    }
}

func cmdHandler(c *net.TCPConn) {
    defer c.Close()
    for {
        msg, err := bufio.NewReader(c).ReadString('\n')
        if err != nil {
            if err == io.EOF {
                fmt.Printf("client '%s' left the session.\n", c.RemoteAddr().String())
                return
            }
            fmt.Println(err)
            return
        }
        fmt.Printf("%s -> %s", c.RemoteAddr().String(), msg)
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)