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 |