Кратко про сетевые протоколы в Golang: TCP, QUIC и UDP

от автора

Привет, Хабр!

Сегодня мы кратко рассмотрим то, как реализовать такие протколы, как TCP, UDP и QUIC в Golang.

Начнем с TCP.

TCP

TCP — это очень надежный, ориентированный на соединение протокол. Он обеспечивает упорядоченную передачу данных, автоматом исправляя ошибки.

Основные черты TCP:

  • Надежность: подтверждения и повторная отправка потерянных пакетов.

  • Упорядоченность: передача данных в том порядке, в котором они были отправлены.

  • Контроль перегрузки: предотвращение коллапса сети за счет контроля скорости передачи данных.

Go имеет пакет net для создания серверов и клиентов TCP. В этом пакете есть несколько функций, которые позволяют управлять сетевыми соединениями.

Для инициализация слушающего сокета используется функция net.Listen, которая принимает тип сети и адрес. Пример вызова: listener, err := net.Listen("tcp", "localhost:8080"). Функция возвращает объект Listener, который будет слушать входящие соединения на указанном порту.

После создания слушателя, можно принимать входящие соединения в цикле, используя listener.Accept(). Метод блокируется до тех пор, пока не поступит новое входящее соединение. Каждое новое соединение можно обрабатывать в отдельной горутине для асинхронной обработки.

С помощью полученного объекта Conn, можно читать данные через conn.Read() и отправлять данные через conn.Write().

Для создания TCP клиента используется функция net.Dial, которая устанавливает соединение с сервером. Пример: conn, err := net.Dial("tcp", "localhost:8080").

Аналогично серверу, через объект Conn можно отправлять и получать данные.

Пример

Реализуем простую систему обмена сообщениями между сервером и клиентом.

Сервер будет слушать входящие TCP подключения, принимать сообщения от клиентов, и отправлять простое подтверждение о получении сообщения:

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())     } } 

Клиент будет подключаться к серверу, отправлять сообщения и получать ответы от сервера:

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 — это простой протокол без установления соединения, который не гарантирует доставку, порядок или интегральность данных. Но зато, он дает минимум задержки.

Основные черты UDP:

  • Отсутствие процесса установления соединения уменьшает задержку.

  • Меньше накладных расходов, больше производительности.

Для создания UDP сервера используется функция net.ListenPacket() или net.ListenUDP(). Они позволяют привязать сервер к определенному адресу и порту. Сервер будет слушать входящие UDP пакеты и может отвечать на них без установления постоянного соединения, что характерно для UDP.

Пример

Пример сервера:

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)         }     } }

Клиент UDP в Go создается с использованием функции net.DialUDP() или net.Dial("udp", address), которая возвращает объект net.Connс методамиRead и Write для отправки и получения данных.

Пример клиента:

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 не использует установление соединения, что делает его быстрее TCP для проектов, где допустима потеря данных.

Функции ReadFrom() и WriteTo() используются для обмена данными без необходимости установления постоянного соединения.

Нет необходимости в слушающем объекте типа Listener, как это требуется в TCP, поскольку UDP оперирует на основе датаграмм, а не потоков данных.

QUIC

QUIC — это уже современный протокол, разработанный Google и стандартизированный IETF, который стремится улучшить производительность соединений, предоставляемых TCP, с добавлением функций безопасности, аналогичных TLS/SSL. QUIC работает поверх UDP и предназначен для снижения задержек соединения, поддерживает мультиплексирование потоков без взаимного блокирования и управляет потерей пакетов более лучше, чем TCP.

Основные черты QUIC:

  • Уменьшение задержек:уменьшает задержку соединения за счет использования 0-RTT и 1-RTT рукопожатий.

  • Безопасность: включает встроенное шифрование на уровне соединений.

  • Мультиплексирование: позволяет нескольким потокам данных обмениваться данными в рамках одного соединения без взаимной блокировки.

Работа с протоколом QUIC в Go проходит с помощью библиотеки quic-go, которая представляет собой полноценную реализацию QUIC. Эта библиотека поддерживает множество стандартов, включая HTTP/3.

В quic-go можно инициализировать транспортное соединение с помощью quic.Transport, которое позволяет мультиплексировать несколько соединений с одного UDP-сокета.

Для установки соединения можно использовать функции quic.Dial или quic.DialAddr, которые не требуют предварительной инициализации quic.Transport. Эти функции позволяют быстро подключиться к серверу с заданными конфигурациями TLS и QUIC.

Чтобы создать пример сервера и клиента на QUIC в Go, можно использовать библиотеку quic-go, которая предоставляет полную реализацию протокола QUIC. Вот как вы можете создать базовый QUIC сервер и клиент с использованием этой библиотеки.

Пример

Сервер будет слушать на определённом порту и отвечать на входящие сообщения от клиента:

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"},     } }

Клиент будет подключаться к серверу, отправлять сообщения и получать ответы:

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])) }

Материал подготовлен в рамках старта онлайн-курса «Go (Golang) Developer Basic».


ссылка на оригинал статьи https://habr.com/ru/articles/830096/