Сервер часов использует по одной горутине на соединение. В этом разделе мы создадим эхо-сервер, который для
каждого подключения использует несколько горутин. Большинство эхо-серверов просто записывают все, что
считывают, — что можно выполнить с помощью следующей тривиальной версии handleConn
:
func handleConn(c net.Conn) {
io.Copy(c, c) // Примечание: ингорируем ошибки
c.Close()
}
Более интересный эхо-сервер может имитировать реверберацию обычного эха, сначала отвечая громко ("HELLO! "), затем,
после задержки, — умеренно ("Hello! "), а потом — совсем тихо ("hello! "). Это умеет делать следующая
версия handleConn
(см. reverb1.go):
func echo(c io.Writer, shout string, delay time.Duration) {
fmt.Fprintln(c, "\t", strings.ToUpper(shout))
time.Sleep(delay)
fmt.Fprintln(c, "\t", shout)
time.Sleep(delay)
fmt.Fprintln(c, "\t", strings.ToLower(shout))
}
func handleConn(c io.ReadWriteCloser) {
input := bufio.NewScanner(c)
for input.Scan() {
echo(c, input.Text(), 1*time.Second)
}
// Примечание: игнорируем потенциальные ошибки input.Err()
c.Close()
}
Нашу клиентскую программу необходимо обновить так, чтобы она отправляла входные данные на сервер и в то же время копировала ответ сервера на выход, что предоставляет еще одну возможность использования параллелизма (см. netcat2.go):
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
go mustCopy(os.Stdout, conn)
mustCopy(conn, os.Stdin)
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
В то время как главная горутина (main
) считывает стандартный ввод и отправляет его на сервер, вторая горутина
считывает и выводит ответ сервера. Когда главная горутина встречает конец входных данных, например после того,
как пользователь нажмет в терминале клавиши <Ctrl+D> (или <Ctrl+Z> в Microsoft Windows), программа останавливается, даже
если другие горутины все еще работают. (Познакомившись с каналами в разделе 8.4.1, мы узнаем, как заставить
программу ждать завершения с обеих сторон соединения.)
В приведенной ниже сессии входные данные клиента приведены слева, а ответы сервера — справа. Клиент три раза "кричит" эхо-серверу и слушает ответы.
$ go build /reverbl $ ./reverb1 &
$ go build /netcat2
$ ./netcat2
Hello?
HELLO?
Hello?
hello?
Is there anybody there?
IS THERE ANYBODY THERE?
Yooohooo!
Is there anybody there? is there anybody there?
YOOOHOOO!
Yooohooo!
yooohooo!
^D
$ killall reverb1
Обратите внимание, что третий “крик” от клиента не рассматривается до тех пор, пока второй крик не обработан
полностью, — что, конечно, не очень реалистично. Реальное эхо будет состоять из трех независимых криков. Чтобы
имитировать его, нам понадобится больше горутин. И вновь все, что нам нужно сделать, — это добавить ключевое
слово go
, на этот раз к вызову echo
(см. reverb2.go):
func handleConn(conn io.ReadWriteCloser) {
input := bufio.NewScanner(conn)
for input.Scan() {
go echo(conn, input.Text(), 1*time.Second)
}
}
Аргументы функции, запускаемой с помощью go
, вычисляются при выполнении самой инструкции go
; таким
образом, input.Text()
вычисляется в главной горутине.
Теперь все эхо работают параллельно и перекрываются во времени:
$ go build /reverb2
$ ./reverb2 &
$ ./netcat2
Is there anybody there?
IS THERE ANYBODY THERE?
Yooohooo!
Is there anybody there?
YOOOHOOO!
is there anybody there?
Yooohooo!
yooohooo!
^D
$ killall reverb2
Все, что требовалось для того, чтобы сделать сервер использующим параллелизм, причем не только для обработки соединений
от нескольких клиентов, но даже в рамках одного соединения, — это вставить два ключевых слова go
.
Однако, добавляя эти ключевые слова, мы должны были убедиться, что этот одновременный вызов методов net.Conn
безопасен, что для большинства типов не выполняется. Мы обсудим решающее значение концепции безопасности параллелизма в
следующей главе.
- Горутины в Golang позволяют выполнять параллельные вычисления в одном соединении;
- Добавление ключевого слова go перед вызовом функции echo позволяет горутинам работать параллельно и обрабатывать запросы одновременно;
- Использование горутин позволяет параллельно отправлять сообщения на сервер и выводить ответ сервера на экран;
go mustCopy(os.Stdout, conn) mustCopy(conn, os.Stdin)
- Важно убедиться, что одновременные вызовы методов объектов языка Golang безопасны, для чего необходимо обеспечить безопасность параллелизма.