Go language advanced network programming

In-depth discussion of network programming in Go language

img

Introduction

Network programming in Go (Golang) is easy, powerful, and fun. This guide delves into the intricacies of network programming, covering protocols, TCP/UDP sockets, concurrency, and more, with detailed notes.

Key concepts

1. Network protocol

  • TCP (Transmission Control Protocol): Ensures reliable data transmission.
  • UDP (User Datagram Protocol): Faster, but data delivery is not guaranteed.

2. Socket

  • TCP sockets: used for connection-oriented communication.
  • UDP socket: used for connectionless communication.

3. Concurrency

  • Goroutines: Allow parallel processing in code.
  • Channels: used for communication between coroutines.

Example

Example 1: TCP server and client

The TCP server and client examples demonstrate the basics of TCP communication.

Server

package main

import (
 "net"
 "fmt"
)

func main() {<!-- -->
 // Listen on TCP port 8080 on all available unicast and
 // any unicast IP addresses.
 listen, err := net.Listen("tcp", ":8080")
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 defer listen.Close()

 // Infinite loop to handle incoming connections
 for {<!-- -->
  conn, err := listen.Accept()
  if err != nil {<!-- -->
   fmt.Println(err)
   continue
  }
  // Launch a new goroutine to handle the connection
  go handleConnection(conn)
 }
}

func handleConnection(conn net.Conn) {<!-- -->
 defer conn.Close()
 buffer := make([]byte, 1024)
 // Read the incoming connection into the buffer.
 _, err := conn.Read(buffer)
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 // Send a response back to the client.
 conn.Write([]byte("Received: " + string(buffer)))
}

Client

package main

import (
 "net"
 "fmt"
)

func main() {<!-- -->
 // Connect to the server at localhost on port 8080.
 conn, err := net.Dial("tcp", "localhost:8080")
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 defer conn.Close()

 // Send a message to the server.
 conn.Write([]byte("Hello, server!"))
 buffer := make([]byte, 1024)
 // Read the response from the server.
 conn.Read(buffer)
 fmt.Println(string(buffer))
}

The server waits for a connection on port 8080, reads incoming messages and sends responses. The client connects to the server, sends a message and prints the server’s response.

Example 2: UDP server and client

Unlike TCP, UDP is connectionless. Below is the implementation of UDP server and client.

Server

package main

import (
 "net"
 "fmt"
)

func main() {<!-- -->
 // Listen for incoming UDP packets on port 8080.
 conn, err := net.ListenPacket("udp", ":8080")
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 defer conn.Close()

 buffer := make([]byte, 1024)
 // Read the incoming packet data into the buffer.
 n, addr, err := conn.ReadFrom(buffer)
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 fmt.Println("Received: ", string(buffer[:n]))
 // Write a response to the client's address.
 conn.WriteTo([]byte("Message received!"), addr)
}

Client

package main

import (
 "net"
 "fmt"
)

func main() {<!-- -->
 // Resolve the server's address.
 addr, err := net.ResolveUDPAddr("udp", "localhost:8080")
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }

 // Dial a connection to the resolved address.
 conn, err := net.DialUDP("udp", nil, addr)
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 defer conn.Close()

 // Write a message to the server.
 conn.Write([]byte("Hello, server!"))
 buffer := make([]byte, 1024)
 // Read the response from the server.
 conn.Read(buffer)
 fmt.Println(string(buffer))
}

The server reads messages from any client and sends responses. The client sends the message and waits for a response.

Example 3: Concurrent TCP servers

Concurrency allows multiple clients to be processed simultaneously.

package main

import (
 "net"
 "fmt"
)

func main() {<!-- -->
 // Listen on TCP port 8080.
 listener, err := net.Listen("tcp", ":8080")
 if err != nil {<!-- -->
  fmt.Println(err)
  return
 }
 defer listener.Close()

 for {<!-- -->
  //Accept a connection.
  conn, err := listener.Accept()
  if err != nil {<!-- -->
   fmt.Println(err)
   continue
  }
  // Handle the connection in a new goroutine.
  go handleConnection(conn)
 }
}

func handleConnection(conn net.Conn) {<!-- -->
 defer conn.Close()
 buffer := make([]byte, 1024)
 // Read the incoming connection.
 conn.Read(buffer)
 fmt.Println("Received:", string(buffer))
 // Respond to the client.
 conn.Write([]byte("Message received!"))
}

By using a new goroutine for each connection, multiple clients can connect simultaneously.

Example 4: HTTP Server with Gorilla Mux

The Gorilla Mux library simplifies HTTP request routing.

package main

import (
 "fmt"
 "github.com/gorilla/mux"
 "net/http"
)

func main() {<!-- -->
 // Create a new router.
 r := mux.NewRouter()
 // Register a handler function for the root path.
 r.HandleFunc("/", homeHandler)
 http.ListenAndServe(":8080", r)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {<!-- -->
 // Respond with a welcome message.
 fmt.Fprint(w, "Welcome to Home!")
}

This code sets up an HTTP server and defines a handler function for the root path.

Example 5: HTTPS server

Implementing an HTTPS server ensures secure communication.

package main

import (
 "net/http"
 "log"
)

func main() {<!-- -->
 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {<!-- -->
  // Respond with a message.
  w.Write([]byte("Hello, this is an HTTPS server!"))
 })
 // Use the cert.pem and key.pem files to secure the server.
 log.Fatal(http.ListenAndServeTLS(":8080", "cert.pem", "key.pem", nil))
}

The server uses TLS (Transport Layer Security) to encrypt communications.

Example 6: Custom TCP protocol

Customized TCP protocols can be used for specialized communication.

package main

import (
 "net"
 "strings"
)

func main() {<!-- -->
 // Listen on TCP port 8080.
 listener, err := net.Listen("tcp", ":8080")
 if err != nil {<!-- -->
  panic(err)
 }
 defer listener.Close()

 for {<!-- -->
  //Accept a connection.
  conn, err := listener.Accept()
  if err != nil {<!-- -->
   panic(err)
  }
  // Handle the connection in a new goroutine.
  go handleConnection(conn)
 }
}

func handleConnection(conn net.Conn) {<!-- -->
 defer conn.Close()
 buffer := make([]byte, 1024)
 // Read the incoming connection.
 conn.Read(buffer)
 // Process custom protocol command.
 cmd := strings.TrimSpace(string(buffer))
 if cmd == "TIME" {<!-- -->
  conn.Write([]byte("The current time is: " + time.Now().String()))
 } else {<!-- -->
  conn.Write([]byte("Unknown command"))
 }
}

This code implements a simple custom protocol that replies with the current time when the client sends the command “TIME”.

Example 7: WebSockets using Gorilla WebSocket

WebSockets provide real-time, full-duplex communication over a single connection.

package main

import (
 "github.com/gorilla/websocket"
 "net/http"
)

var upgrader = websocket.Upgrader{<!-- -->
 ReadBufferSize: 1024,
 WriteBufferSize: 1024,
}

func handler(w http.ResponseWriter, r *http.Request) {<!-- -->
 conn, err := upgrader.Upgrade(w, r, nil)
 if err != nil {<!-- -->
  http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
  return
 }
 defer conn.Close()

 for {<!-- -->
  messageType, p, err := conn.ReadMessage()
  if err != nil {<!-- -->
   return
  }
  // Echo the message back to the client.
  conn.WriteMessage(messageType, p)
 }
}

func main() {<!-- -->
 http.HandleFunc("/", handler)
 http.ListenAndServe(":8080", nil)
}

The WebSocket server will pass the message back to the client.

Example 8: Connection timeout

Connection timeouts can be managed using the context package.

package main

import (
 "context"
 "fmt"
 "net"
 "time"
)

func main() {<!-- -->
 // Create a context with a timeout of 2 seconds
 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 defer cancel()

 // Dialer using the context
 dialer := net.Dialer{<!-- -->}
 conn, err := dialer.DialContext(ctx, "tcp", "localhost:8080")
 if err != nil {<!-- -->
  panic(err)
 }

 buffer := make([]byte, 1024)
 _, err = conn.Read(buffer)
 if err == nil {<!-- -->
  fmt.Println("Received:", string(buffer))
 } else {<!-- -->
  fmt.Println("Connection error:", err)
 }
}

This code sets a two-second deadline for reading data from the connection.

Example 9: Rate limiting using golang.org/x/time/rate

Rate limiting controls the rate of requests.

package main

import (
 "golang.org/x/time/rate"
 "net/http"
 "time"
)

// Define a rate limiter allowing two requests per second with a burst capacity of five.
var limiter = rate.NewLimiter(2, 5)

func handler(w http.ResponseWriter, r *http.Request) {<!-- -->
 // Check if request is allowed by the rate limiter.
 if !limiter.Allow() {<!-- -->
  http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
  return
 }
 w.Write([]byte("Welcome!"))
}

func main() {<!-- -->
 http.HandleFunc("/", handler)
 http.ListenAndServe(":8080", nil)
}

This example uses a rate limiter to limit the request rate to two requests per second with a burst capacity of five.