🦫 Самоучитель по Go для начинающих. Часть 17. Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
В этой части самоучителя мы погрузимся в мир сетевого программирования, изучим его основные концепции и инструменты. Начнем с рассмотрения принципов работы компьютерных сетей и их архитектуры, познакомимся с протоколами TCP и IP, лежащими в основе стека TCP/IP. Затем детально изучим сокеты и их роль в сетевом взаимодействии. Особое внимание будет уделено пакету net, который предоставляет удобные инструменты для реализации сетевых приложений.
Основы сетевого взаимодействия
Перед написанием сетевых приложений разберемся с некоторыми базовыми понятиями. Во-первых, определимся: что же такое сеть? В этой статье под сетью будем понимать совокупность компьютеров, объединенных таким образом, что они способны взаимодействовать между собой посредством сетевых протоколов с целью обмена данными.
Данные передаются по сети в виде небольших сообщений, называемых пакетами. Каждый из них содержит заголовок с адресом назначения, который используют сетевые устройства (например, маршрутизаторы и коммутаторы) для последующего перенаправления пакета.
Что такое сетевой протокол?
Сетевой протокол – это свод правил и стандартов для описания взаимодействия двух и более устройств сети. Поддержка протоколов закладывается на аппаратном и/или программном уровнях.
Сетевые протоколы описывают взаимодействие на различных уровнях: физическом (радиочастоты, сигналы), сетевом (маршрутизация пакетов, логическая адресация) и других. Уровни объединяют в сетевые модели или стеки протоколов. В основе работы интернета лежит стек TCP/IP, описывающий взаимодействие устройств в глобальной сети. Он состоит из двух протоколов – TCP и IP, первый из которых работает поверх второго.
Что такое CP (Transmission Control Protocol)?
TCP (Transmission Control Protocol) — это протокол управления передачей, который обеспечивает доставку и сборку данных в заданном порядке. TCP гарантирует целостность и упорядоченность данных, однако из-за дополнительных проверок его скорость может быть ниже по сравнению с другими протоколами.
Работу TCP можно разделить на три этапа:
- Установка сеанса связи с помощью так называемого трехстороннего рукопожатия.
- Разбиение данных на сегменты, их нумерация и отправка получателю.
- Завершение соединения путем обмена флагами FIN и ACK.
Что такое IP (Internet Protocol)?
IP (Internet Protocol) — это межсетевой протокол, отвечающий за адресацию: доставку определенных данных до целевого компьютера. Именно IP логически объединил локальные компьютерные сети в глобальную, больше известную как Интернет.
Таким образом, протокол IP отвечает за передачу данных, а TCP – за гарантию их доставки.
Стек TCP/IP состоит из четырех уровней:
Название уровня | Описание | Примеры протоколов |
Прикладной | Обеспечивает работу сетевых приложений | HTTP, DNS, FTP, SMTP |
Транспортный | Контролирует доставку сообщений и гарантирует корректную последовательность прихода данных | TCP, UDP, RSVP |
Межсетевой | Отвечает за взаимодействие между отдельными сетями, соединяет локальные сети с глобальной | IPv4, IPv6, ICMP |
Канальный | Отвечает за физический процесс передачи информации, её кодирование, разделение на пакеты и отправку по каналам | Ethernet, MAC, WLAN, Wi-Fi |
После прохождения транспортного уровня пакет попадает в управление операционной системы (ОС), которая подбирает подходящее сетевое приложение с помощью комбинации IP-адреса и сетевого порта.
Что такое IP-адрес?
IP-адрес – это уникальный адрес, идентифицирующий устройство в глобальной или локальной сети. Он представляется 32 или 128-битным числом для версий IPv4 и IPv6 соответственно.
Что такое сетевой порт?
Сетевой порт – это числовой идентификатор, который включается в заголовки транспортного уровня и используется для определения получателя пакета в пределах хоста. В заголовках протоколов TCP и UDP (о нем мы поговорим далее) порты хранятся в виде полей размером 16 бит и разделены на три диапазона: общеизвестные (0–1023), пользовательские (1024–49151) и частные (49152–65535).
Комбинация из IP-адреса и порта называется сокетом. Он представляет собой программный интерфейс для создания каналов взаимодействия между устройствами с целью обмена информацией. Сокеты используются во многих сетевых приложениях: веб-серверах, чатах, электронной почте и других.
Можно сказать, что сокет – это канал общения между программистом и ОС. Вместо того, чтобы разбираться в тонкостях работы протоколов, IP-адресов и портов, программисту достаточно создать в программе объект сокета для отправки и получения сообщений. При этом все задачи по передаче данных возлагаются на ОС.
В следующей части статьи изучим сетевое взаимодействие в Go и реализуем два вида сокетов: TCP и UDP. Второй из них основан на еще одном важном протоколе Интернета – UDP (User Datagram Protocol). Он обеспечивает передачу данных без необходимости установки сеансов связи и получения подтверждений, поэтому работает быстрее TCP, но не гарантирует целостность и упорядоченность информации. Из-за этой особенности UDP применяется там, где скорость работы важнее, чем потеря части пакетов, например, в стриминговых сервисах, онлайн-играх и голосовой связи.
Сетевое взаимодействие в Go
Перейдем к реализации сетевого взаимодействия средствами языка Go. Мы будем работать с клиент-серверной моделью, состоящей из клиента и сервера и работающей по следующим принципам:
- Клиент отправляет запрос на определенный сетевой порт и получает от него ответ.
- Сервер привязывает сетевые службы к портам, ожидает запросы от клиента и отсылает ему ответы.
Основным пакетом для сетевого программирования в Go является net. Он предоставляет интерфейсы сетевого ввода / вывода, разрешения доменных имен, работы с сокетами, протоколами TCP, IP и UDP.
Пакет net предоставляет доступ к низкоуровневым сетевым инструментам, но для большинства задач хватает нескольких базовых примитивов: функций Dial, Listen, Accept и интерфейсов Conn и Listener, включая их вариации для протоколов TCP, UDP и IP.
Детально рассмотрим устройство интерфейсов Conn и Listener, так как они содержат часто используемые методы для работы с соединениями. В листингах приведено лишь короткое описание методов, более подробную информацию о них можно найти в документации пакета net.
Интерфейс Conn:
type Conn interface { // Read считывает данные из соединения. Read(b []byte) (n int, err error) // Write записывает данные в соединение. Write(b []byte) (n int, err error) // Close закрывает соединение. Close() error // LocalAddr возвращает адрес локальной сети, если он известен. LocalAddr() Addr // RemoteAddr возвращает адрес удаленной сети, если он известен. RemoteAddr() Addr // SetDeadline устанавливает таймаут для чтения и записи. Эквивалентен // вызову обоих методов SetReadDeadline и SetWriteDeadline. SetDeadline(t time.Time) error // SetReadDeadline устанавливает таймаут для будущих // и заблокированных на данный момент вызовов Read. SetReadDeadline(t time.Time) error // SetWriteDeadline устанавливает таймаут для будущих // и заблокированных на данный момент вызовов Write. SetWriteDeadline(t time.Time) error }
Можно заметить, что интерфейс net.Conn реализует интерфейсы io.Reader и io.Writer, поэтому обладает совместимостью со многими объектами пакетов io и bufio. Эта особенность упрощает реализацию ввода-вывода в сетевых соединениях и открывает возможность настройки буферизации и десериализации данных. Для полноценного изучения пакетов io и bufio рекомендуем обратиться к статье: Самоучитель по Go для начинающих. Часть 14. Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os.
Интерфейс Listener:
type Listener interface { // Accept ожидает следующее соединение и возвращает его прослушивателю. Accept() (Conn, error) // Close закрывает прослушивателя. Close() error // Addr возвращает адрес сети прослушивателя. Addr() Addr }
Для принятия входящих соединений используются функции Dial и Listen, возвращающие типы Conn и Listener соответственно:
// Dial подключается к адресу в указанной сети. func Dial(network, address string) (Conn, error) // Listen прослушивает подключения по адресу локальной сети. // Возможные значения network: "tcp", "tcp4", "tcp6", "unix", "unixpacket". func Listen(network, address string) (Listener, error)
Параметры network и address задействованы во многих объектах пакета net, поэтому разберем их детальнее:
- параметр
network
может принимать значения"tcp"
,"tcp4"
(для IPv4),"tcp6"
(для IPv6), аналогичные названия для UDP и IP, а также"unix"
,"unixgram"
и"unixpacket"
. - параметр
address
для протоколов TCP и UDP должен иметь вид"хост:порт"
, для IP –"хост"
. Если значение хоста пусто, например, имеет вид":80"
или"::"
, то будет использован локальный хост (localhost).
Самое время применить на практике теоретические знания об устройстве сетей и основных компонентов пакета net. Напишем несложный код для отправки HTTP-запроса по протоколу TCP с использованием функции Dial
:
func main() { // Заголовок HTTP-запроса для получения корневой страницы сайта go.dev request := "GET / HTTP/1.1\\nHost: go.dev\\n\\n" // Установка TCP-соединения по хосту go.dev и стандартному HTTP-порту 80 conn, err := net.Dial("tcp", "go.dev:80") if err != nil { log.Fatal(err) } defer conn.Close() // Отложенное закрытие соединения // Отправка HTTP-запроса в соединение и обработка ошибки if _, err = conn.Write([]byte(request)); err != nil { log.Fatal(err) } // Копирование данных в стандартный поток вывода из соединения io.Copy(os.Stdout, conn) }
Обратите внимание: после открытия соединения необходимо обязательно предусмотреть, как оно будет закрыто. Чаще всего для отложенного закрытия используется конструкция defer conn.Close()
.
В результате выполнения кода в консоль будет выведен стандартный HTTP-ответ от сервера, содержащий справочную информацию:
HTTP/1.1 302 Found Location: <https://go.dev/> X-Cloud-Trace-Context: eb1e804740ac2ae3dc5ef2058662f1e1 Date: Sun, 06 Oct 2024 13:15:59 GMT Content-Type: text/html Server: Google Frontend Content-Length: 0
TCP-сервер
Рассмотрим реализацию TCP-сервера, который открывает сетевой порт на прослушивание, принимает соединения от клиента и обрабатывает его запросы в отдельной горутине. Чтобы сократить код и сконцентрироваться на структуре TCP-сервера, опустим обработку ошибок, оставив написание этого функционала в качестве полезного упражнения:
const ( network = "tcp" port = ":8080" ) func main() { // Преобразование сети и порта в TCP-адрес tcpAddr, _ := net.ResolveTCPAddr(network, port) // Открытие сокета-прослушивателя listener, _ := net.ListenTCP(network, tcpAddr) defer listener.Close() log.Printf("Прослушивание порта %s...\\n", port) for { // Принятие TCP-соединения от клиента и создание нового соединения conn, err := listener.Accept() if err != nil { log.Fatal(err) } // Обработка запросов клиента в отдельной горутине go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() log.Printf("Подключен клиент %s\\n", conn.RemoteAddr().String()) connReader := bufio.NewReader(conn) for { // Чтение данных из соединения data, err := connReader.ReadString('\\n') if err != nil { break } // Обработка сообщения от клиента message := strings.TrimSpace(string(data)) log.Printf("Сообщение от %s: %s\\n", conn.RemoteAddr().String(), message) if message == "STOP" { break } // Отправка данных в соединение conn.Write([]byte(data)) } log.Printf("Отключен клиент %s\\n", conn.RemoteAddr().String()) }
TCP-клиент
Теперь приведем код TCP-клиента, который считывает из стандартного потока ввода сообщение, отправляет его серверу и получает от него ответ:
const ( network = "tcp" port = ":8080" ) func main() { // Преобразование сети и порта в TCP-адрес tcpAddr, _ := net.ResolveTCPAddr(network, port) // Создание соединения с сервером по TCP-адресу conn, _ := net.DialTCP(network, nil, tcpAddr) defer conn.Close() reader := bufio.NewReader(os.Stdin) connReader := bufio.NewReader(conn) for { fmt.Print("Сообщение серверу (или 'STOP' для завершения): ") // Считывание сообщения из стандартного потока ввода data, _ := reader.ReadString('\\n') if data == "STOP\\n" { break } // Отправка данных в соединение conn.Write([]byte(data)) // Чтение данных из соединения message, _ := connReader.ReadString('\\n') fmt.Print("Ответ от сервера: " + message) } }
Для тестирования кода TCP-клиента сначала следует в отдельном окне запустить TCP-сервер на открытом сетевом порту. Примерный вариант взаимодействия между клиентом и сервером может выглядеть так:
2024/10/08 22:43:44 Прослушивание порта :8080... 2024/10/08 22:43:45 Подключен клиент 127.0.0.1:37708 2024/10/08 22:43:49 Сообщение от 127.0.0.1:37708: message1 2024/10/08 22:43:53 Отключен клиент 127.0.0.1:37708
Вывод результата выполнения кода TCP-клиента:
Сообщение серверу (или 'STOP' для завершения): message1 Ответ от сервера: message1 Сообщение серверу (или 'STOP' для завершения): STOP
UDP-сервер
Основное отличие UDP-сервера от TCP-сервера заключается в том, что первый из них не устанавливает отдельного соединения с клиентом, а просто прослушивает определенный порт и обрабатывает входящие запросы по мере их поступления. Поэтому функция net.ListenUDP
возвращает указатель на тип UDPConn, в то время как функция net.ListenTCP
– указатель на тип TCPListener
, что избавляет UDP-сервер от необходимости использовать метод Accept
для принятия соединений:
const ( network = "udp" port = ":8080" ) func main() { // Преобразование сети и порта в UDP-адрес addr, _ := net.ResolveUDPAddr(network, port) // Создание UDP-прослушивателя (открытие соединения) conn, _ := net.ListenUDP(network, addr) defer conn.Close() log.Printf("Прослушивание порта %s...\\n", port) buffer := make([]byte, 1024) for { // Чтение данных из UDP-соединения n, remoteAddr, _ := conn.ReadFromUDP(buffer) // Обработка сообщения от клиента message := strings.TrimSpace(string(buffer[:n])) log.Printf("Сообщение от %s: %s\\n", remoteAddr.String(), message) if message == "STOP" { break } // Отправка данных в соединение conn.WriteToUDP(buffer[:n], remoteAddr) } }
UDP-клиент
Коды TCP- и UDP-клиентов практически не отличаются, за исключением того, что для чтения данных из UDP-соединения используется отдельный буфер и метод ReadFromUDP
:
const ( network = "udp" port = ":8080" ) func main() { // Преобразование сети и порта в UDP-адрес сервера udpAddr, _ := net.ResolveUDPAddr(network, port) // Создание UDP-соединения conn, _ := net.DialUDP(network, nil, udpAddr) defer conn.Close() reader := bufio.NewReader(os.Stdin) for { fmt.Print("Сообщение серверу (или 'STOP' для завершения): ") // Считывание сообщения из стандартного потока ввода data, _ := reader.ReadString('\\n') if data == "STOP\\n" { break } // Отправка данных в соединение conn.Write([]byte(data)) // Чтение данных из соединения buffer := make([]byte, 1024) n, _, _ := conn.ReadFromUDP(buffer) fmt.Printf("Ответ от сервера: %s", string(buffer[:n])) } }
Заключение
В этой части самоучителя мы познакомились с основами компьютерных сетей: изучили основные протоколы, на которых функционирует Интернет, а также узнали о сетевых моделях, стеках, портах и сокетах. Затем мы рассмотрели принципы сетевого программирования в Go и устройство пакета net, после чего закрепили знания на практических примерах.
В следующей части самоучителя познакомимся с еще одним важным протоколом Интернета – HTTP, который лежит в основе обмена данными. Детально рассмотрим пакет net/http и с помощью его компонентов реализуем клиент-серверное взаимодействие по протоколу HTTP.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
- Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http