TCP, QUIC and UDP

TCP

TCP is a highly reliable, connection-oriented protocol. It provides orderly transmission of data, automatically correcting errors.

The main features of TCP are:

  • Reliability: acknowledgements and resending of lost packets.

  • Orderliness: transmit data in the order in which it was sent.

  • Overload control: Prevent network collapse by controlling the data transfer rate.

Go has a package net for creating TCP servers and clients. This package has several functions that allow you to manage network connections.

To initialize a listening socket function is used net.Listenwhich takes a network type and an address. Example call: listener, err := net.Listen("tcp", "localhost:8080"). The function returns a Listener object that will listen for incoming connections on the specified port.

Once the listener is created, you can accept incoming connections in a loop using listener.Accept(). The method blocks until a new incoming connection arrives. Each new connection can be handled in a separate goroutine for asynchronous processing.

Using the obtained Conn object, you can read data via conn.Read() and send data via conn.Write().

To create a TCP client, use the function net.Dialwhich establishes a connection to the server. Example: conn, err := net.Dial("tcp", "localhost:8080").

Similar to the server, you can send and receive data through the Conn object.

Example

We implement a simple messaging system between the server and the client.

The server will listen for incoming TCP connections, accept messages from clients, and send a simple acknowledgement of message receipt:

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

func main() {
    // определяем порт для прослушивания
    PORT := ":9090"
    listener, err := net.Listen("tcp", PORT)
    if err != nil {
        fmt.Println("Error listening:", err.Error())
        os.Exit(1)
    }
    // закрываем listener при завершении программы
    defer listener.Close()
    fmt.Println("Server is listening on " + PORT)

    for {
        // принимаем входящее подключение
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting:", err.Error())
            os.Exit(1)
        }
        fmt.Println("Connected with", conn.RemoteAddr().String())
        // обрабатываем подключение в отдельной горутине
        go handleRequest(conn)
    }
}

func handleRequest(conn net.Conn) {
    defer conn.Close()
    // читаем данные от клиента
    scanner := bufio.NewScanner(conn)
    for scanner.Scan() {
        clientMessage := scanner.Text()
        fmt.Printf("Received from client: %s\n", clientMessage)
        // отправляем ответ клиенту
        conn.Write([]byte("Message received.\n"))
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("Error reading:", err.Error())
    }
}

The client will connect to the server, send messages and receive responses from the server:

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

func main() {
    // соединяемся с сервером
    conn, err := net.Dial("tcp", "localhost:9090")
    if err != nil {
        fmt.Println("Error connecting:", err.Error())
        os.Exit(1)
    }
    defer conn.Close()

    // читаем сообщения с консоли и отправляем их серверу
    consoleScanner := bufio.NewScanner(os.Stdin)
    fmt.Println("Enter text to send:")
    for consoleScanner.Scan() {
        text := consoleScanner.Text()
        conn.Write([]byte(text + "\n"))

        // получаем ответ от сервера
        response, err := bufio.NewReader(conn).ReadString('\n')
        if err != nil {
            fmt.Println("Error reading:", err.Error())
            os.Exit(1)
        }
        fmt.Print("Server says: " + response)
        fmt.Println("Enter more text to send:")
    }

    if err := consoleScanner.Err(); err != nil {
        fmt.Println("Error reading from console:", err.Error())
    }
}

UDP

UDP is a simple connectionless protocol that does not guarantee delivery, order, or integrity of data. However, it does provide minimal latency.

Main features of UDP:

  • The absence of a connection establishment process reduces latency.

  • Less overhead, more productivity.

To create a UDP server, use the function net.ListenPacket() or net.ListenUDP(). They allow you to bind a server to a specific address and port. The server will listen for incoming UDP packets and can respond to them without establishing a permanent connection, which is typical for UDP.

Example

Server example:

package main

import (
    "fmt"
    "net"
)

func main() {
    conn, err := net.ListenPacket("udp", ":8080")
    if err != nil {
        fmt.Println("Error creating socket:", err)
        return
    }
    defer conn.Close()

    fmt.Println("Listening on :8080...")

    buf := make([]byte, 1024)

    for {
        n, addr, err := conn.ReadFrom(buf)
        if err != nil {
            fmt.Println("Error reading datagram:", err)
            continue
        }

        if _, err := conn.WriteTo(buf[:n], addr); err != nil {
            fmt.Println("Error writing datagram:", err)
        }
    }
}

A UDP client in Go is created using the function net.DialUDP() or net.Dial("udp", address)which returns an object net.Connwith methodsRead And Write to send and receive data.

Customer example:

package main

import (
    "fmt"
    "net"
)

func main() {
    addr, err := net.ResolveUDPAddr("udp", "localhost:8080")
    if err != nil {
        fmt.Println("Error resolving address:", err)
        return
    }

    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        fmt.Println("Error creating socket:", err)
        return
    }
    defer conn.Close()

    data := "Hello, server!"
    if _, err := conn.Write([]byte(data)); err != nil {
        fmt.Println("Error sending datagram:", err)
        return
    }

    buf := make([]byte, len(data))
    if _, err := conn.Read(buf); err != nil {
        fmt.Println("Error reading datagram:", err)
        return
    }

    fmt.Println("Received from server:", string(buf))
}

UDP is connectionless, making it faster than TCP for projects where data loss is acceptable.

Functions ReadFrom() And WriteTo() are used to exchange data without the need to establish a permanent connection.

There is no need for a listening object of type Listeneras required by TCP, since UDP operates on a datagram basis rather than a data stream.

QUIC

QUIC is a modern protocol developed by Google and standardized IETFwhich aims to improve the performance of connections provided by TCP while adding security features similar to TLS/SSL. QUIC runs on top of UDP and is designed to reduce connection latency, support multiplexing of streams without deadlock, and manage packet loss better than TCP.

Main features of QUIC:

  • Reducing delays:reduces connection latency by using 0-RTT and 1-RTT handshakes.

  • Safety: Enables built-in connection-level encryption.

  • Multiplexing: Allows multiple data streams to exchange data within a single connection without interlocking.

Working with the QUIC protocol in Go is done using the library quic-gowhich is a full-fledged implementation of QUIC. This library supports many standards, including HTTP/3.

IN quic-go you can initialize the transport connection using quic.Transportwhich allows multiplexing of multiple connections from one UDP socket.

To establish a connection, you can use the functions quic.Dial or quic.DialAddrwhich do not require preliminary initialization quic.Transport. These features allow you to quickly connect to a server with specified TLS and QUIC configurations.

To create a sample QUIC server and client in Go, you can use the library quic-gowhich provides a complete implementation of the QUIC protocol. Here's how you can create a basic QUIC server and client using this library.

Example

The server will listen on a specific port and respond to incoming messages from the client:

package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "io"
    "log"

    "github.com/lucas-clemente/quic-go"
)

func main() {
    listener, err := quic.ListenAddr("localhost:4242", generateTLSConfig(), nil)
    if err != nil {
        log.Fatal("Failed to listen:", err)
    }

    for {
        sess, err := listener.Accept(context.Background())
        if err != nil {
            log.Fatal("Failed to accept session:", err)
        }

        go func() {
            for {
                stream, err := sess.AcceptStream(context.Background())
                if err != nil {
                    log.Fatal("Failed to accept stream:", err)
                }

                // эхо полученных данных обратно клиенту
                _, err = io.Copy(stream, stream)
                if err != nil {
                    log.Fatal("Failed to echo data:", err)
                }
            }
        }()
    }
}

func generateTLSConfig() *tls.Config {
    key, cert := generateKeys() // Допустим, что функция generateKeys генерирует TLS ключ и сертификат
    return &tls.Config{
        Certificates: []tls.Certificate{cert},
        NextProtos:   []string{"quic-echo-example"},
    }
}

The client will connect to the server, send messages and receive responses:

package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "io"
    "log"
    "os"

    "github.com/lucas-clemente/quic-go"
)

func main() {
    session, err := quic.DialAddr("localhost:4242", &tls.Config{InsecureSkipVerify: true}, nil)
    if err != nil {
        log.Fatal("Failed to dial:", err)
    }

    stream, err := session.OpenStreamSync(context.Background())
    if err != nil {
        log.Fatal("Failed to open stream:", err)
    }

    fmt.Fprintf(stream, "Hello, QUIC Server!\n")
    buf := make([]byte, 1024)
    n, err := io.ReadFull(stream, buf)
    if err != nil {
        log.Fatal("Failed to read from stream:", err)
    }

    fmt.Printf("Server says: %s", string(buf[:n]))
}

The material was prepared as part of the launch online course “Go (Golang) Developer Basic”.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *