Книга: Программирование на языке Ruby

18.1.3. Пример: сервер для игры в шахматы по сети

18.1.3. Пример: сервер для игры в шахматы по сети

Не всегда нашей конечной целью является взаимодействие с самим сервером. Иногда сервер — всего лишь средство для соединения клиентов друг с другом. В качестве примера можно привести файлообменные сети, столь популярные в 2001 году. Другой пример — серверы для мгновенной передачи сообщений, например ICQ, и разного рода игровые серверы.

Давайте напишем скелет шахматного сервера. Мы не имеем в виду программу, которая будет играть в шахматы с клиентом. Нет, наша задача — связать клиентов так, чтобы они могли затем играть без вмешательства сервера.

Предупреждаю, что ради простоты показанная ниже программа ничего не знает о шахматах. Логика игры просто заглушена, чтобы можно было сосредоточиться на сетевых аспектах.

Для установления соединения между клиентом и сервером будем использовать протокол TCP. Можно было бы остановиться и на UDP, но этот протокол ненадежен, и нам пришлось бы использовать тайм-ауты, как в одном из примеров выше.

Клиент может передать два поля: свое имя и имя желательного противника. Для идентификации противника условимся записывать его имя в виде user:hostname; мы употребили двоеточие вместо напрашивающегося знака @, чтобы не вызывать ассоциаций с электронным адресом, каковым эта строка не является.

Когда от клиента приходит запрос, сервер сохраняет сведения о клиенте у себя в списке. Если поступили запросы от обоих клиентов, сервер посылает каждому из них сообщение; теперь у каждого клиента достаточно информации для установления связи с противником.

Есть еще вопрос о выборе цвета фигур. Оба партнера должны как-то договориться о том, кто каким цветом будет играть. Для простоты предположим, что цвет назначает сервер. Первый обратившийся клиент будет играть белыми (и, стало быть, ходить первым), второй — черными.

Уточним: компьютеры, которые первоначально были клиентами, начиная с этого момента общаются друг с другом напрямую; следовательно, один из них становится сервером. Но на эту семантическую тонкость я не буду обращать внимания.

Поскольку клиенты посылают запросы и ответы попеременно, причем сеанс связи включает много таких обменов, будем пользоваться протоколом TCP. Следовательно, клиент, который на самом деле играет роль «сервера», создает объект TCPServer, а клиент на другом конце — объект TCPSocket. Будем предполагать, что номер порта для обмена данными заранее известен обоим партнерам (разумеется, У каждого из них свой номер порта).

Мы только что описали простой протокол прикладного уровня. Его можно было бы сделать и более хитроумным.

Сначала рассмотрим код сервера (листинг 18.1). Чтобы его было проще запускать из командной строки, создадим поток, который завершит сервер при нажатии клавиши Enter. Сервер многопоточный — он может одновременно обслуживать нескольких клиентов. Данные о пользователях защищены мьютексом, ведь теоретически несколько потоков могут одновременно попытаться добавить новую запись в список.

Листинг 18.1. Шахматный сервер

require "thread"
require "socket"
PORT = 12000
HOST = "96.97.98.99"              # Заменить этот IP-адрес.
# Выход при нажатии клавиши Enter.
waiter = Thread.new do
 puts "Нажмите Enter для завершения сервера."
 gets
 exit
end
$mutex = Mutex.new
$list = {}
def match?(p1, p2)
 return false if !$list[p1] or !$list[p2]
 if ($list[p1][0] == p2 and $list[p2][0] == p1)
  true
 else
  false
 end
end
def handle_client(sess, msg, addr, port, ipname)
 $mutex.synchronize do
  cmd, player1, player2 = msg.split
  # Примечание: от клиента мы получаем данные в виде user:hostname,
  # но храним их в виде user:address.
  p1short = player1.dup           # Короткие имена
  p2short = player2.split(":")[0] # (то есть не ":address").
  player1 << ":#{addr}"           # Добавить IP-адрес клиента.
  user2, host2 = player2.split(":")
  host2 = ipname if host2 == nil
  player2 = user2 + ":" + IPSocket.getaddress(host2)
  if cmd != "login"
   puts "Ошибка протокола: клиент послал сообщение #{msg}."
  end
  $list[player1] = [player2, addr, port, ipname, sess]
  if match?(player1, player2)
   # Имена теперь переставлены: если мы попали сюда, значит
   # player2 зарегистрировался первым.
   p1 = $list[player1]
   р2 = $list[player2]
   # ID игрока = name:ipname:color
   # Цвет: 0=белый, 1=черный
   p1id = "#{p1short}:#{p1[3]}:1"
   p2id = "#{p2short}:#{p2[3]}:0"
   sess1 = p1[4]
   sess2 = p2[4]
   sess1.puts "#{p2id}"
   sess2.puts "#{p1id}"
   sess1.close
   sess2.close
  end
 end
end
text = nil
$server = TCPServer.new(HOST, PORT)
while session = $server.accept do
 Thread.new(session) do |sess|
  text = sess.gets
  puts "Получено: #{text}" # Чтобы знать, что сервер получил.
  domain, port, ipname, ipaddr = sess.peeraddr
  handle_client sess, text, ipaddr, port, ipname
  sleep 1
 end
end
waiter.join                # Выходим, когда была нажата клавиша Enter.

Метод handle_client сохраняет информацию о клиенте. Если запись о таком клиенте уже существует, то каждому клиенту посылается сообщение о том, где находится другой партнер. Этим обязанности сервера исчерпываются.

Клиент (листинг 18.2) оформлен в виде единственной программы. При первом запуске она становится TCP-сервером, а при втором — TCP-клиентом. Честно говоря, решение о том, что сервер будет играть белыми, совершенно произвольно. Вполне можно было бы реализовать приложение так, чтобы цвет не зависел от подобных деталей.

Листинг 18.2. Шахматный клиент

require "socket"
require "timeout"
ChessServer = '96.97.98.99' # Заменить этот IP-адрес.
ChessServerPort = 12000
PeerPort = 12001
WHITE, BLACK = 0, 1
Colors = %w[White Black]
def draw_board(board)
 puts <<-EOF
+------------------------------+
| Заглушка! Шахматная доска... |
+------------------------------+
 EOF
end
def analyze_move(who, move, num, board)
 # Заглушка - черные всегда выигрывают на четвертом ходу.
 if who == BLACK and num == 4
  move << " Мат!"
 end
 true # Еще одна заглушка - любой ход считается допустимым.
end
def my_move(who, lastmove, num, board, sock)
 ok = false
 until ok do
  print "nВаш ход: "
  move = STDIN.gets.chomp
  ok = analyze_move(who, move, num, board)
  puts "Недопустимый ход" if not ok
 end
  sock.puts move
 move
end
def other_move(who, move, num, board, sock)
 move = sock.gets.chomp
 puts "nПротивник: #{move}"
 move
end
if ARGV[0]
 myself = ARGV[0]
else
 print "Ваше имя? "
 myself = STDIN.gets.chomp
end
if ARGV[1]
 opponent_id = ARGV[1]
else
 print "Ваш противник? "
 opponent_id = STDIN.gets.chomp
end
opponent = opponent_id.split(":")[0] # Удалить имя хоста.
# Обратиться к серверу
socket = TCPSocket.new(ChessServer, ChessServerPort)
response = nil
socket.puts "login # {myself} #{opponent_id}"
socket.flush
response = socket.gets.chomp
name, ipname, color = response.split ":"
color = color.to_i
if color == BLACK   # Цвет фигур другого игрока,
 puts "nУстанавливается соединение..."
 server = TCPServer.new(PeerPort)
 session = server.accept
 str = nil
 begin
  timeout(30) do
   str = session.gets.chomp
   if str != "ready"
    raise "Ошибка протокола: получено сообщение о готовности #{str}."
   end
  end
 rescue TimeoutError
  raise "He получено сообщение о готовности от противника."
 end
 puts "Ваш противник #{opponent}... у вас белые.n"
 who = WHITE
 move = nil
 board = nil        # В этом примере не используется.
 num = 0
 draw_board(board)  # Нарисовать начальное положение для белых.
 loop do
  num += 1
  move = my_move(who, move, num, board, session)
  draw_board(board)
  case move
   when "resign"
    puts "nВы сдались. #{opponent} выиграл."
    break
  when /Checkmate/
    puts "nВы поставили мат #{opponent}!"
    draw_board(board)
    break
  end
  move = other_move(who, move, num, board, session)
  draw_board(board)
  case move
   when "resign"
    puts "n#{opponent} сдался... вы выиграли!"
    break
   when /Checkmate/
    puts "n#{opponent} поставил вам мат."
    break
  end
 end
else                # Мы играем черными,
 puts "nУстанавливается соединение..."
 socket = TCPSocket.new(ipname, PeerPort)
 socket.puts "ready"
 puts "Ваш противник #{opponent}... у вас черные.n"
 who = BLACK
 move = nil
 board = nil        # В этом примере не используется.
 num = 0
 draw_board(board)  # Нарисовать начальное положение.
 loop do
  num += 1
  move = other_move(who, move, num, board, socket)
  draw_board(board) # Нарисовать доску после хода белых,
  case move
   when "resign"
    puts "n#{opponent} сдался... вы выиграли!"
    break
   when /Checkmate/
    puts "n#{opponent} поставил вам мат."
    break
  end
  move = my_move(who, move, num, board, socket)
  draw_board(board)
  case move
   when "resign"
    puts "nВы сдались. #{opponent} выиграл."
    break
   when /Checkmate/
    puts "n#{opponent} поставил вам мат."
    break
  end
 end
 socket.close
end

Я определил этот протокол так, что черные посылают белым сообщение «ready», чтобы партнер знал о готовности начать игру. Затем белые делают первый ход. Ход посылается черным, чтобы клиент мог нарисовать такую же позицию на доске, как у другого игрока.

Повторю, приложение ничего не знает о шахматах. Вместо проверки допустимости хода вставлена заглушка; проверка выполняется локально, то есть на той стороне, где делается ход. Никакой реальной проверки нет — заглушка всегда говорит, что ход допустим. Кроме того, мы хотим, чтобы имитация игры завершалась после нескольких ходов, поэтому мы написали программу так, что черные всегда выигрывают на четвертом ходу. Победа обозначается строкой «Checkmate!» в конце хода. Эта строка печатается на экране соперника и служит признаком выхода из цикла.

Помимо «традиционной» шахматной нотации (например, «P-K4») существует еще «алгебраическая», которую многие предпочитают. Но написанный код вообще не имеет представления о том, какой нотацией мы пользуемся.

Поскольку это было несложно сделать, мы позволяем игроку в любой момент сдаться. Рисование доски тоже заглушено. Желающие могут реализовать грубый рисунок, выполненный ASCII-символами.

Метод my_move всегда относится к локальному концу, метод other_move — к удаленному.

В листинге 18.3 приведен протокол сеанса. Действия клиентов нарисованы друг против друга.

Листинг 18.3. Протокол сеанса шахматной игры

% ruby chess.rb Hal                      % ruby chess.rb
Capablanca:deepthought.org               Hal:deepdoodoo.org
Устанавливается соединение...            Устанавливается соединение...
Ваш противник Capablanca... у вас белые. Ваш противник Hal... у вас черные.
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Ваш ход: N-QB3                           Противник: N-QB3
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Противник: P-K4                          Ваш ход: P-K4
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Ваш ход: P-K4                            Противник: P-K4
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Противник: B-QB4                         Ваш ход: B-QB4
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Ваш ход: B-QB4                           Противник: B-QB4
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... +
+------------------------------+         +------------------------------+
Противник: Q-KR5                         Ваш ход: Q-KR5
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Ваш ход: N-KB3                           Противник: N-KB3
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Противник: QxP Checkmate!                Ваш ход: QxP
+------------------------------+         +------------------------------+
| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |
+------------------------------+         +------------------------------+
Capablanca поставил вам мат.             Вы поставили мат Hal!

Оглавление книги


Генерация: 1.257. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз