© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
N. TolaramSoftware Development with Gohttps://doi.org/10.1007/978-1-4842-8731-6_9

9. Simple Networking

Nanik Tolaram1  
(1)
Sydney, NSW, Australia
 
In this chapter, you will learn how to write networking code using Go. You will understand how to write client and server code for the TCP and UDP protocols. You will also look at writing a network server that can process requests concurrently using goroutines. By the end of the chapter, you will know how to do the following:
  • Write a network client for TCP and UDP

  • Write a network server for TCP and UDP

  • Use goroutines to process requests

  • Load test a network server

Source Code

The source code for this chapter is available from the https://github.com/Apress/Software-Development-Go repository.

TCP Networking

In this section, you will explore creating TCP applications using the standard Go network library. The code that you will write is both TCP client and server.

TCP Client

Let’s start by writing a TCP client that connects to a particular HTTP server, in this case google.com, and prints out the response from the server. The code can be found inside the chapter9/tcp/simple directory. Run it as follows:
go run main.go
When the code runs, it will try to connect to the google.com server and print out the web page returned to the console, as shown in the output here:
HTTP/1.0 200 OK
Date: Sun, 05 Dec 2021 10:27:46 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
Server: gws
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Set-Cookie: 1P_JAR=2021-12-05-10; expires=Tue, 04-Jan-2022 10:27:46 GMT; path=/; domain=.google.com; Secure
Set-Cookie:
...
Accept-Ranges: none
Vary: Accept-Encoding
<!doctype html>
...
The app uses the net package from the standard library and it uses a TCP connection specified in the following code:
conn, err := net.Dial("tcp", t)
if err != nil {
   panic(err)
}
Here is the code that connects to the server:
package main
...
const (
  host = "google.com"
  port = "80"
)
func main() {
  t := net.JoinHostPort(host, port)
  conn, err := net.Dial("tcp", t)
  if err != nil {
     panic(err)
  }
  ...
}
The code uses the net.Dial(..) function to connect to google.com on port 80 using a TCP connection. Once it successfully connects, it sends the HTTP protocol to the server to tell the server that it is requesting the index page, as shown here:
func main() {
  ...
  req := "GET / Host: google.com Connection: close "
  conn.Write([]byte(req))
  ...
}
Once it receives the response, it prints the output on the console, as shown in this code snippet:
...
func main() {
  ...
  connReader := bufio.NewReader(conn)
  scanner := bufio.NewScanner(connReader)
  for scanner.Scan() {
     fmt.Printf("%s ", scanner.Text())
  }
  if err := scanner.Err(); err != nil {
     fmt.Println("Scanner error", err)
  }
}

Now that you understand how to write a TCP client, in the next section you will learn how to write a TCP server.

TCP Server

In this section, you will write a TCP server that listens to port 3333 on your local machine. The server will print out what it received and send a response back. The code is inside the tcp/server directory, and it can be run as follows:
go run main.go
You will get output as follows:
2022/03/05 22:51:19 Listening on port 3333
Use the nc (network connect) tool to connect to the server, as shown here:
nc localhost 3333
Once connected, enter any text and press Enter. You will get a response back. The following is an example. I typed in This is a test and it came back with a response of Message received of length : 15.
This is a test
Message received of length : 15
Let’s take a look at the code. The first thing you will look at how the code waits and listens on port 3333, as shown in the following code snippet:
func main() {
  t := net.JoinHostPort("localhost", "3333")
  l, err := net.Listen("tcp", t)
  ...
  for {
     conn, err := l.Accept()
     if err != nil {
        log.Println("Error accepting: ", err.Error())
        os.Exit(1)
     }
     go handleRequest(conn)
  }
}

The code uses the Accept function of the Listener object, which is returned when calling the net.Listen(..) function. The Accept function waits until it receives a connection.

When the client is connected successfully, the code proceeds by calling the handleRequest function in a separate goroutine. Having requests processed in a separate goroutine allows the application to process requests concurrently.

The handling of the request and the sending of the response is taken care of inside the handleRequest function, as shown in the following snippet:
func handleRequest(conn net.Conn) {
  ...
  len, err := conn.Read(buf)
  ...
  conn.Write([]byte(fmt.Sprintf("Message received of length : %d", len)))
  conn.Close()
}

The code reads the data sent by the client using the Read(..) function of the connection and writes the response back using the Write(..) function of the same connection.

Because the code uses a goroutine, the TCP server is able to process multiple client requests without any blocking issues.

UDP Networking

In this section, you will look at writing network applications using the UDP protocol.

UDP Client

In this section, you will write a simple UDP application that communicates with a quote-of-the-day(qotd) server that returns a string quote and prints it out to the console. The following link provides more information about the qotd protocol and the available public servers: www.gkbrk.com/wiki/qotd_protocol/. The sample code connects to the server djxms.net that listens on port 17.

The code can be found inside the chapter9/udp/simple directory, and it can be run as follows:
go run main.go
Every time you run the application you will get different quotes. In my case, one was the following:

“Man can climb to the highest summits, but he cannot dwell there long.”

George Bernard Shaw (1856-1950)

Let’s take a look at the different parts of the application and understand what it is doing. The qotd function contains the following snippet. It uses net.ResolveUDPAddr(..) from the standard library to connect to the server and return a UDPAddr struct.
udpAddr, err := net.ResolveUDPAddr("udp", s)
if err != nil {
  println("Error Resolving UDP Address:", err.Error())
  os.Exit(1)
}

The library does a lookup to ensure that the provided domain is valid, and this is done by doing a DNS lookup. On encountering error, it will return a non-nil for the err variable.

Stepping through the net.ResolveUDPAddr function inside the standard library shown in Figure 9-1, you can see that the DNS lookup for the domain returns more than one IP address, but only the first IP address is populated in the returned UDPAddr struct.

Two screenshots. Above is the snippet explaining the connection to the server via I P address. Below are the multiple I Ps from the resolve U D P Addr.

Figure 9-1

Multiple IPs from ResolveUDPAddr

Once udpAddr is successfully populated, it is used when calling net.DialUDP. The function call opens a socket connection to the server using the IP address that is provided inside udpAddr
conn, err := net.DialUDP("udp", nil, udpAddr)

In this section, you learned how to connect a UDP server using the standard library. In the next section, you will learn more on how to write a UDP server.

UDP Server

In this section, you will explore further and write a UDP server using the standard library. The server listens on port 3000 and prints out what is sent by the client. The code can be found inside the chapter9/udp/server directory, and it can be run as follows:
go run main.go
The sample prints out the following on the console:
2022/03/05 23:51:32 Listening [::]:3000
On a terminal window, use the nc command to connect to port 3000.
nc -u localhost 3000
Once the nc tool runs, enter any text and you will see it printed in the server’s terminal. Here is an example of how it looked on my machine:
2022/03/05 23:51:32 Listening [::]:3000
2022/03/05 23:51:36 Received: nanik from [::1]:41518
2022/03/05 23:51:44 Received: this is a long letter from [::1]:41518
Let’s explore how the code works. The following snippet sets up the UDP server using the net.ListenUDP function:
...
func main() {
  conn, err := net.ListenUDP("udp", &net.UDPAddr{
     Port: 3000,
     IP:   net.ParseIP("0.0.0.0"),
  })
  ...
}
The function call returns a UDPConn struct that is used to read and write to the client. After the code successfully creates a UDP server connection, it starts listening to read data from it, as shown here:
...
func main() {
  ...
  for {
     message := make([]byte, 512)
     l, u, err := conn.ReadFromUDP(message[:])
     ...
     log.Printf("Received: %s from %s ", data, u)
  }
}

The code uses the ReadFromUDP(..) function of the UDP connection to read the data that is sent by the client to print it out to the console.

Concurrent Servers

In the previous section, you wrote a UDP server but one of the things that is lacking is its ability to process multiple UDP client requests. Writing a UDP server that can process multiple requests is different from normal TCP. The way to structure the application is to spin off multiple goroutines to listen on the same connection and let each goroutine take care of processing the request. The code can be found inside the udp/concurrent directory. Let’s take a look at what it is doing differently compared to the previous UDP server implementation.

The following snippet shows the code spinning off multiple goroutines to listen to the UDP connection:
...
func main() {
  addr := net.UDPAddr{
     Port: 3333,
  }
  connection, err := net.ListenUDP("udp", &addr)
  ...
  for i := 0; i < runtime.NumCPU(); i++ {
     ...
     go listen(id, connection, quit)
  }
 ...
}
The number of goroutine runs depends on the result returned from runtime.NumCPU(). The goroutine use the listen function, which is shown in the following snippet:
func listen(i int, connection *net.UDPConn, quit chan struct{}) {
  buffer := make([]byte, 1024)
  for {
     _, remote, err := connection.ReadFromUDP(buffer)
     if err != nil {
        break
     }
    ...
  }
  ...
}

Now that the listen function is run as several goroutines, it waits on an incoming UDP request by calling the ReadFromUDP function. When an incoming UDP request is detected, one of the running goroutines processes it.

Load Testing

In this section, you will look at using load testing to test the network server that you wrote in the previous sections. You will be using an open source load testing tool called fortio. which can be downloaded from https://github.com/fortio/fortio; for this book, use version v1.21.1.

Using the load testing tool, you will see the timing difference between code that is designed to handle requests without using goroutines vs. code that is designed to handle requests using goroutines. For this exercise, you will use the UDP server that is inside the chapter9/udp/loadtesting directory. You will compare between the UDP server that uses goroutines inside the chapter9/udp/loadtesting/concurrent directory and the UDP server that does not use goroutines inside chapter9/udp/loadtesting/server.

The only difference between the code that you use for load testing with the code discussed in the previous section is the addition of the time.Sleep(..) function. This is added to simulate or mock a process that is doing something to the request before sending a response back. Here is the code:
func listen(i int, connection *net.UDPConn, quit chan struct{}) {
  ...
  for {
     ...
     //pretend the code is doing some request processing for 10milliseconds
     time.Sleep(10 * time.Millisecond)
     ...
  }
  ...
}
func main() {
  ...
  for {
     ...
     //pretend the code is doing some request processing for 10milliseconds
     time.Sleep(10 * time.Millisecond)
     ...
  }
}
Let’s run the code inside the chapter9/udp/loadtesting/concurrent directory first. Once the UDP server starts up, run the fortio tool as follows:
./fortio load -n 200 udp://0.0.0.0:3333/
The tool makes 200 calls to a server running locally on port 3000. You will see results something like the following:
...
00:00:44 I udprunner.go:223> Starting udp test for udp://0.0.0.0:3333/ with 4 threads at 8.0 qps
Starting at 8 qps with 4 thread(s) [gomax 12] : exactly 200, 50 calls each (total 200 + 0)
...
Aggregated Function Time : count 200 avg 0.011425742 +/- 0.005649 min 0.010250676 max 0.054895756 sum 2.2851485
# range, mid point, percentile, count
>= 0.0102507 <= 0.011 , 0.0106253 , 94.50, 189
> 0.011 <= 0.012 , 0.0115 , 98.00, 7
> 0.045 <= 0.05 , 0.0475 , 99.00, 2
> 0.05 <= 0.0548958 , 0.0524479 , 100.00, 2
# target 50% 0.0106453
# target 75% 0.0108446
# target 90% 0.0109641
# target 99% 0.05
# target 99.9% 0.0544062
Sockets used: 200 (for perfect no error run, would be 4)
Total Bytes sent: 4800, received: 200
udp short read : 200 (100.0 %)
All done 200 calls (plus 0 warmup) 11.426 ms avg, 8.0 qps
The final result is that the average time it takes to process is 11.426 ms. Now let’s compare this with the server code that does not use goroutines, which is inside the chapter9/udp/loadtesting/server directory. Once you run the UDP server, use the same command to run forti. You will see results that looks like the following:
...
00:00:07 I udprunner.go:223> Starting udp test for udp://0.0.0.0:3000/ with 4 threads at 8.0 qps
Starting at 8 qps with 4 thread(s) [gomax 12] : exactly 200, 50 calls each (total 200 + 0)
...
Aggregated Function Time : count 200 avg 0.026354093 +/- 0.01187 min 0.010296825 max 0.054235708 sum 5.27081864
# range, mid point, percentile, count
>= 0.0102968 <= 0.011 , 0.0106484 , 24.50, 49
> 0.011 <= 0.012 , 0.0115 , 25.00, 1
> 0.02 <= 0.025 , 0.0225 , 50.00, 50
> 0.03 <= 0.035 , 0.0325 , 73.50, 47
> 0.035 <= 0.04 , 0.0375 , 74.00, 1
> 0.04 <= 0.045 , 0.0425 , 98.50, 49
> 0.045 <= 0.05 , 0.0475 , 99.00, 1
> 0.05 <= 0.0542357 , 0.0521179 , 100.00, 2
# target 50% 0.025
# target 75% 0.0402041
# target 90% 0.0432653
# target 99% 0.05
# target 99.9% 0.0538121
Sockets used: 200 (for perfect no error run, would be 4)
Total Bytes sent: 4800, received: 200
udp short read : 200 (100.0 %)
All done 200 calls (plus 0 warmup) 26.354 ms avg, 8.0 qps

The average time recorded this time is 26.354ms, which is more than the previous result of 11.426. With this, you can conclude that it is important to remember to use goroutines when writing a network server application to ensure concurrent request processing.

Summary

In this chapter, you learned how to create network applications using TCP and UDP. You learned how to write client and server for both protocols. You learned how to write an application that can process multiple requests concurrently using goroutines.

This is an important step to understand because it is the foundation of how to write network applications that can process huge amounts of traffic. This chapter is a stepping-stone for the upcoming chapter where you will look at different styles of writing network applications in Linux.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.237.154