Хэл Фултон - Программирование на языке Ruby
Предупреждаю, что ради простоты показанная ниже программа ничего не знает о шахматах. Логика игры просто заглушена, чтобы можно было сосредоточиться на сетевых аспектах.
Для установления соединения между клиентом и сервером будем использовать протокол 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Устанавливается соединение..."