Лучшая практика для создания сервера - корутины или poll / select
коллеги. Некоторое время писал разное прикладное клиент-серверное ПО на C++. Моя обычная практика (в общих чертах):
- создать "серверный" сокет для приема входящих подключений
- тем или иным образом "отметить" его как "серверный"
- написать фрагмент кода/функцию приема входящих подключений (1)
- написать фрагмент кода/функцию обслуживающий принятые подключения (клиентов) (2)
- написать фрагмент кода/функцию обслуживающий таймаут ожидания (3)
- завести массив / вектор для файл-дескрипторов
- запустить select / poll для этого массива
- при наступлении какого либо события (1),(2) или (3) - дернуть соответствующую функцию.
В принципе, для не сильно нагруженных приложений всегда хватало такого подхода. Корутины C++ для этого не использовал в основном потому, что они в этом языке неудобные (мнение субъективное). Однако прочитал я про замечательные примеры корутин в Go и, соответственно, примеры реализации клиент-серверных приложений на этих корутинах (ибо там они очень удобно выглядят). Как я понял, в общих чертах подход следующий:
- создать "серверный" сокет для приема входящих подключений
- написать функцию-корутину приема входящих подключений (1)
- написать функцию-корутину обслуживающую принятые подключения (клиентов) (2)
- на каждого из принятых клиентов заводить "свою" корутину
- корутины "сами дергаются" при наступлении события
Пытаюсь для себя вывести соображения, какой подход лучше и почему. Первый подход, кажется, более платформозависимым, в отличие от второго. При первом подходе мне проще отслеживать состояние сокета: готовность на чтение, запись (иногда полезно знать, что сокет именно готов), отключение корреспондирующей стороны. На первый подход хорошо ложится "машина состояний".
Со вторым подходом я знаком сильно хуже, но все же. Второй подход условно более расширяемый, в том плане, что в корутины закатать можно и какие-то другие операции. Пожалуй, асинхронно удобнее писать в сокет большие объемы данных. Большего придумать не сумел.
Предлагаю высказать свои соображения по этому поводу или собственный опыт.
P.S. Если для обсуждения данного вопроса выбрал неправильную площадку, то прошу простить, а пост снести
Ответы (1 шт):
В Го, безусловно, для работы с сетью предполагается использовать только горутины. Вообще горутины - фундаментальная парадигма для асинхронных операций. Более того, в интерфейсе стандартной библиотеки в принципе нет сокетов, даже в полустандартной библиотеке golang.org/x/net их нет. Соответственно, нет и прямого доступа к системным вызовам
select/epoll. Нет, конечно, если очень надо, то всё это можно сделать через пакет syscall, но не нужно.
Идиома для обработки сетевых соединений исчерпывающе описана в примере к типу Listener:
package main
import (
"io"
"log"
"net"
)
func main() {
// Listen on TCP port 2000 on all available unicast and
// anycast IP addresses of the local system.
l, err := net.Listen("tcp", ":2000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
for {
// Wait for a connection.
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
// Handle the connection in a new goroutine.
// The loop then returns to accepting, so that
// multiple connections may be served concurrently.
go func(c net.Conn) {
// Echo all incoming data.
io.Copy(c, c)
// Shut down the connection.
c.Close()
}(conn)
}
}
- Функция
net.Listenсоздаёт слушателя на заданном порту. - Далее в цикле серверная горутина ждёт соединения в вызове
listner.Accept. Этот вызов возвращает объет типаnet.Conn, у которого вместо системных вызововsend/recvв наличии методыWriteиRead. - Для обработки клиентского подключения порождается горутина с новосозданным объектом
connв качестве параметра. Объектconnреализует интерфейсыReaderиWriter, поэтому поверх него можно накрутить любой вид стандартного ввода-вывода --io.ReadAll,io.Copy,bufioи т.д. и т.п. Завершаясь, горутина должна закрыть соединение.
Для незначительного ускорения процесса вы можете не порождать горутины, а использовать какой-нибудь пул (или написать его сами), но с архитектурной точки зрения разница невелика.
На самом деле глубоко в кишках рантайма таки происходит epoll. Реализация пакета net вместо того, чтобы заблокироваться на сокете в системном вызове accept или recv, регистрирует соответствующие события в epoll и передаёт управление планировщику. Планировщик помечает горутину как ждущую и подбирает следующую горутину для исполнения. Переключение между горутинами дёшево, планировщик Го очень старается минимизировать простои, раскидывает горутины по разным потокам (thread-ам) и разным ядрам, поэтому (ИМХО) реализация сокет-сервера на горутинах весьма эффективна. Едва ли получится переиграть планировщик ручным асинхронным вводом-выводом.
Так что ответ на ваш вопрос - только горутины.