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()
// ...
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()
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 fromAccept()
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 anio.Writer
(which we now know thatnet.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
}
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)
}
}
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)
}
}
Top comments (0)