Colored for chat colors
gem install colored
It has many comments since I tried to explain what is going on for less-experienced Ruby people.
| # A Ruby chatsever written for the sole purpose of learning to use | |
| # sockets and writing non-blocking I/O in Ruby. Furthermore I've | |
| # written comments for you to understand how it all works. They | |
| # are presented in this awesome format with [Rocco][ro]! | |
| # | |
| # [ro]: http://github.com/rtomayko/rocco | |
| #### Prerequesties | |
| # We'll need a few libraries to get started, these are "socket" | |
| # which is found within the Ruby standard library and [colored][co] | |
| # which is for pretty terminal colors. | |
| # | |
| # Install colored: | |
| # | |
| # $ gem install colored | |
| # | |
| # [co]: http://github.com/defunkt/colored | |
| # | |
| %w{ | |
| socket | |
| colored | |
| }.each {|lib| require lib} | |
| # We start by defining our chat Module (or namespace, if you prefer) | |
| module Chat | |
| #### Connection | |
| # Our connections class which is a metaclass of Array that adds a few | |
| # methods that are handy for our application. | |
| class Connections < Array | |
| # Collect all the sockets from the objects. | |
| # | |
| # Example: | |
| # | |
| # clients = Connections.new Client.new(:socket => socket), | |
| # Client.new(:socket => socket) | |
| # sockets = clients.sockets | |
| # | |
| # #=> [TCPSocket, TCPSocket] | |
| # | |
| def sockets | |
| collect {|e| e.socket} | |
| end | |
| # Reverse lookup the socket; find the object the socket belongs to. | |
| # | |
| # Example (continues example for Chat::Connections#sockets): | |
| # | |
| # clients[sockets.first] | |
| # | |
| # #=> Chat::Client | |
| # | |
| def [](socket) | |
| select {|e| e.socket === socket}.first if socket.is_a?(TCPSocket) | |
| end | |
| end | |
| #### Client class | |
| # Client class, each chat client has a corresponding instance of | |
| # this class. | |
| class Client | |
| # We want out clients to have a: | |
| # | |
| # * Username | |
| # - Name of the user | |
| # * Socket | |
| # - Biredirectional connection to the user | |
| # * Channel(s) | |
| # - Channel(s) the user is on | |
| # | |
| attr_accessor :username, :socket, :channel | |
| # We give our client some properties. | |
| # | |
| # Example: | |
| # | |
| # Chat::Client.new :socket => socket, | |
| # :channel => self, | |
| # :username => "Goomba" | |
| # | |
| def initialize(properties) | |
| @socket, @channel = properties[:socket], properties[:channel] | |
| @username = properties[:username] | |
| end | |
| end | |
| #### Channel class | |
| # The channel class handles our chat channel. Currently our app. | |
| # doesn't support multiple channels, however, this is a great | |
| # example of how well structured OO can make something much | |
| # easier in the future. | |
| class Channel | |
| # As with Client we want some information about our channel, | |
| # this includes: | |
| # | |
| # * Name | |
| # - Name of the channel | |
| # * Clients | |
| # - Channel clients | |
| # * Server socket | |
| # - The server socket | |
| # | |
| attr_accessor :name, :clients, :socket | |
| # Specify what port the channel should run on | |
| def initialize(port) | |
| # Start the server socket | |
| @socket = TCPServer.new "localhost", port | |
| # We want a Connections array for our clients | |
| @clients = Connections.new | |
| # We're ready! | |
| puts "Chatserver started on port #{port.to_s.bold}\n" | |
| end | |
| # Accepts a new connection on the server | |
| def accept_new_connection | |
| # Create a new Client with the accepted socket, add our channel | |
| # and default username. | |
| new_client = Client.new :socket => @socket.accept_nonblock, | |
| :channel => self, | |
| :username => "Guest#{@clients.size}" | |
| # Add the new client to the channel's clients | |
| @clients << new_client | |
| # Write to all the channels users that someone has connected | |
| self << "#{new_client.username.bold} has joined!\n" | |
| end | |
| # Send a message to the channel | |
| def send_message(message) | |
| # Send the message to each user in the channel via the socket | |
| @clients.each do |client| | |
| client.socket.write_nonblock message | |
| end | |
| # Print the message to the server log | |
| print message | |
| end | |
| # Makes #<< an alias for #send_message | |
| alias_method :<<, :send_message | |
| end | |
| #### Server class | |
| # The server class handles the chat server | |
| class Server | |
| # What port are we running on? | |
| def initialize(port) | |
| # Channels connections | |
| @channels = Connections.new | |
| # Add a "default" channel | |
| @channels << Channel.new(port) | |
| end | |
| # The run method sets the server in an infinite loop where | |
| # it listens for events. | |
| def run | |
| loop do | |
| # Refresh new clients (someone could have been accepted | |
| # in the last loop) | |
| @clients = refresh_clients | |
| # This is where the magic happens. | |
| # select(2), see `man 2 select` listens on all the sockets passed | |
| # and blocks until I/O is ready on any of the sockets. In this case | |
| # it listens on **all** of our client sockets, and channel server | |
| # sockets for I/O. I/O on a channel server typically means someone | |
| # has connected on the server, and ready I/O on a client socket usually | |
| # means the client has attempted to send a message to the socket or | |
| # has terminated its session. | |
| # When I/O is ready on anything, it returns an array like this: | |
| # | |
| # [[readable_sockets], [write], [errors]] | |
| # | |
| # In this case, we're only interested in the readable_sockets. | |
| read, write = select(@clients.sockets + @channels.sockets) | |
| # For each (readable) socket that is ready for I/O | |
| read.each do |socket| | |
| # Reverse lookup the socket so we get the right client object | |
| # back, so we have access to #username, etc. | |
| client = @clients[socket] unless @clients.empty? | |
| # If the readable socket is a server, someone is trying to connect | |
| if socket.is_a?(TCPServer) | |
| # We accept that connection | |
| accept_new_connection(socket) | |
| # If we're at EOF of the socket, someone has disconnected | |
| elsif socket.eof? | |
| # So we close the connection to the socket | |
| close_connection(socket, client) | |
| else | |
| # Else, someone is sending a message to our socket, and | |
| # we can parse it! | |
| parse(socket, client) | |
| end | |
| end | |
| end | |
| end | |
| private | |
| # Helper method for parsing a message, note that we could | |
| # easily add more to it like the ability to change a users | |
| # username by sending a message like "/nick Newnick", again | |
| # this is free from good design! | |
| def parse(socket, client) | |
| # Read 1024 bytes a time, and strip the output so we don't get | |
| # fancy newlines etc. we want to control this ourselves. | |
| buffer = socket.read_nonblock(1024).strip | |
| # And finally write the message to the server. | |
| client.channel << "#{client.username.bold}: #{buffer}\n" | |
| end | |
| # Helper method for accepting a new connection | |
| def accept_new_connection(socket) | |
| # Reverse lookup the sever to get the server object and | |
| # then accept the new connection. | |
| @channels[socket].accept_new_connection | |
| end | |
| # Helper method for closing a connection | |
| def close_connection(socket) | |
| # Tell people in the channel that the client left | |
| client.channel << "Client left #{client.username.bold}\n" | |
| # Close the socket | |
| client.socket.close | |
| # And delete the client so we don't attempt to listen on | |
| # a closed socket | |
| @clients.delete(sock) | |
| end | |
| # Refresh client list | |
| def refresh_clients | |
| # Basically it just checks all the channels for clients | |
| # and returns them in an array. | |
| clients = Connections.new | |
| @channels.each do |channel| | |
| clients << channel.clients | |
| end | |
| clients.flatten | |
| end | |
| end | |
| end | |
| # Start our server! | |
| Chat::Server.new(1337).run |