Привет, Хабр!
Сегодня мы кратко рассмотрим то, как реализовать такие протколы, как 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/
Добавить комментарий