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.Listen
which 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.Dial
which 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.Conn
with 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 Listener
as 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-go
which 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.Transport
which allows multiplexing of multiple connections from one UDP socket.
To establish a connection, you can use the functions quic.Dial
or quic.DialAddr
which 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-go
which 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”.