Skip to content

Latest commit

 

History

History

lesson3

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

8.3 Пример: параллельный эхо-сервер

Сервер часов использует по одной горутине на соединение. В этом разделе мы создадим эхо-сервер, который для каждого подключения использует несколько горутин. Большинство эхо-серверов просто записывают все, что считывают, — что можно выполнить с помощью следующей тривиальной версии 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 безопасны, для чего необходимо обеспечить безопасность параллелизма.