NOTE: This article is intended for developers with a year or more experience with ruby
Link to source code on github
Slides on my talk Socket programming with ruby
To create our chat app we need to require socket
which is a part of ruby's standard library.
Our chat app will need
- Server
This will be a TCPServer which will bind to a specific port, listen and accept connections on that port
- Client
This will be us connecting to a TCPServer and sending messages to it.
So, let's create a server first
require 'socket'
class Server
def initialize(port)
@server = TCPServer.new(port)
puts "Listening on port #{port}"
end
end
TCPServer.new
creates a tcp server that will bind to a port and listen on it.
It could have also be written as
require 'socket'
socket = Socket.new(:INET, :STREAM)
socket.bind(Socket.pack_sockaddr_in(3000, '127.0.0.1'))
socket.listen(Socket::SOMAXCONN)
Socket.new(:INET, :STREAM)
takes two parameters where :INET means internet and :STREAM means the socket will be of type TCP. If we want to create a UDP socket, we could have passed :DGRAM.
Socket.pack_sockaddr_in(3000, '127.0.0.1')
this method create a C struct that holds the port and ip.
socket.bind
binds to that port
Socket::SOMAXCONN
is a constant that gives how many connections the listen queue can accept. Listen queue is the total pending connection that a socket can tolerate.
socket.listen
will listen for connection on the port.
As you can see, we could achieve all this in just one line with TCPServer.new
. We, ruby programmers, love one-liners.
Now the server needs to accept connections
Socket.accept_loop()
will pop a connection from the listen queue, process it and then exit.
Socket.accept_loop()
can also be written as
# old code
require 'socket'
socket = Socket.new(:INET, :STREAM)
socket.bind(Socket.pack_sockaddr_in(3000, '127.0.0.1'))
socket.listen(Socket::SOMAXCONN)
# new code
loop do
connection, _ = socket.accept
end
Fun fact: To create a server you need a forever loop. In our case loop
socket.accept is a blocking call.
When the connection is accepted and processed, the connection is closed itself. But, we personally want to close it ourselves too because
- we want the garbage collector to collect unused references to the socket
- there is a limit to how much a process can have open files at a given time.
socket.close
will close the connection.
We save how to create a tcp socket, bind it to a port, listen on that port and accept connections.
The Server
So, for our server, we need to do something similar.
require 'socket'
class Server
def initialize(port)
@server = TCPServer.new(port)
@connections = []
puts "Listening on port #{port}"
end
def start
Socket.accept_loop(@server) do |connection|
@connections << connection
puts @connections
Thread.new do
loop do
handle(connection)
end
end
end
end
private
def handle(connection)
request = connection.gets
connection.close if request.nil?
@connections.each do |client|
next if client.closed?
client.puts(request) if client != connection && !client.closed?
end
end
end
server = Server.new(4002)
server.start
Our server needs to keep the state of connected clients. It needs to relay messages to all connected clients and close a connection if a client exits the chat.
To maintain the state of the connected client, we can set up an instance variable in our constructor @connections
Inside of accept_loop
, which listens for new connections, we will push the connection inside @connections
.
Our private handle()
method, takes a connection and reads its message and then relay it to every connection. It will close those connections of the user who exited the chat.
Since we will support more than one active connection we will run them inside a separate thread.
Phew, this was too much.
Let's move to the client
To create a server we used TCPServer.new
but we need to create a client so we create a TCPSocket.new
. It will accept two-parameter. A host and a port.
TCPSocket.new
is a ruby wrapper for a much more line of code.
require 'socket'
socket = Socket.new(:INET, :STREAM)
remote_addr = Socket.pack_sockaddr_in(3000, '127.0.0.1')
socket.connect(remote_addr)
This is similar to creating a server except for we don't bind and listen on a port.
Our Client
require 'socket'
class Client
class << self
attr_accessor :host, :port
end
def self.request
@client = TCPSocket.new(host, port)
listen
send
end
def self.listen
Thread.new do
loop do
puts "====#{@client.gets}"
end
end
end
def self.send
Thread.new do
loop do
msg = $stdin.gets.chomp
@client.puts(msg)
end
end.join
end
end
Client.host = '127.0.0.1'
Client.port = 4002
Client.request
The listen class method will print for any new messages send by other clients connected to the socket.
The send class method will send the server message.
They run in their own separate thread so that we can listen and send messages without getting blocked.
Top comments (2)
This is a great intro to sockets in Ruby! May I suggest a small improvement to make it easier to read code examples. In markdown, you can add a language to highlight syntax in code blocks after the 3 marks say
ruby
, for example:thanks.