1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH << File.expand_path("../../lib", __dir__)
require 'set'
require 'logger'
require 'async'
require 'async/io/host_endpoint'
require 'async/io/protocol/line'
class User < Async::IO::Protocol::Line
attr_accessor :name
def login!
self.write_lines "Tell me your name, traveller:"
self.name = self.read_line
end
def to_s
@name || "unknown"
end
end
class Server
def initialize
@users = Set.new
end
def broadcast(*message)
puts *message
@users.each do |user|
begin
user.write_lines(*message)
rescue EOFError
# In theory, it's possible this will fail if the remote end has disconnected. Each user has it's own task running `#connected`, and eventually `user.read_line` will fail. When it does, the disconnection logic will be invoked. A better way to do this would be to have a message queue, but for the sake of keeping this example simple, this is by far the better option.
end
end
end
def connected(user)
user.login!
broadcast("#{user} has joined")
user.write_lines("currently connected: #{@users.map(&:to_s).join(', ')}")
while message = user.read_line
broadcast("#{user.name}: #{message}")
end
rescue EOFError
# It's okay, client has disconnected.
ensure
disconnected(user)
end
def disconnected(user, reason = "quit")
@users.delete(user)
broadcast("#{user} has disconnected: #{reason}")
end
def run(endpoint)
Async do |task|
endpoint.accept do |peer|
stream = Async::IO::Stream.new(peer)
user = User.new(stream)
@users << user
connected(user)
end
end
end
end
Console.logger.level = Logger::INFO
Console.logger.info("Starting server...")
server = Server.new
endpoint = Async::IO::Endpoint.parse(ARGV.pop || "tcp://localhost:7138")
server.run(endpoint)
|