Привет, Хабр!
Сегодня мы кратко рассмотрим то, как реализовать такие протколы, как 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".
kemm
Это не как реализовать, а как использовать. Как реализовать было бы, если бы вы показали, как поверх протокола предыдущего уровня сделать указанные протоколы (например, для TCP отслеживать sequince number и ack'и, сделать перепосылки, реализовать установку и закрытие соединения, window scaling, etc, etc, etc...)