Skip to content

marcosortiz/easy_sockets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Easy Sockets

Gem Version Build Status Dependency Status Code Climate Coverage Status

Description

Over and over I see developers struggling to implement basic sockets with features available on ruby socket stdlib.

easy_sockets, takes care of basic details that usually are overlooked by developers when implementing TCP/Unix sockets from scratch.

I also strongly recommend the following book if you want to learn more about TCP sockets.

Dependencies

easy_sockets only uses the following ruby stdlib gems:

  • sockets
  • logger
  • timeout (just to raise Timeout::Error)

Transparent idempotent connect and disconnect operations

You don't needneed to worry about connecting your socket (you can still call it if you want). All you need to do is call send_msg. If the socket object is not connected yet, it will automatically try to connect the socket before sending the message. You still need to disconnect your socket after using it.

The connect and disconnect methods are idempotent methods. That means you can call them over and over again and they will only try to do something (connect and disconnect respectively) when the instance of your socket object is disconnected and connected respectively.

The code bellow illustrates this:

irb(main):001:0> require 'easy_sockets'
=> true
irb(main):002:0> s = EasySockets::TcpSocket.new
=> #<EasySockets::TcpSocket:0x007fd16bb69308 @logger=nil, @timeout=0.5, @separator="\r\n", @connected=false, @port=2000, @host="127.0.0.1">
irb(main):003:0> s.connected
=> false
irb(main):004:0> s.connect
=> true
irb(main):005:0> s.connected
=> true
irb(main):006:0> s.connect
=> nil
irb(main):007:0> s.connect
=> nil
irb(main):008:0> s.connected
=> true
irb(main):009:0> s.disconnect
=> true
irb(main):010:0> s.connected
=> false
irb(main):011:0> s.disconnect
=> nil
irb(main):012:0> s.disconnect
=> nil

Safe connect, read and write timeout implementation

There is a lot of material on the internet saying why you should not use ruby timeout stdlib. However, over and over I see developers using the timeout stdlib for production code!

easy_sockets implements connect, read and write timeouts using IO.select.

Framing Messages

Usually, if you don't want to open and close a new connection everytime you need to send something to the server, you need to implement some sort of message framing. Openning (and closing) a new connection for each message, generates unnecessary overhead. While this might be ok for some communications where the messsage exchange rate is low, it might be a show stopper when this rate needs to be bigger.

Message framing is an agreement between client and server on the message format. That way clients and server can signal that one message is ending and another on is beginning.

There are numerous ways of framing messages. easy_sockets support 2:

  1. Message separators: When pass the :separator option when creating your socket, easy_sockets will add it to the end of the message. For instance, if you setup separator: "\r\n" and you call send_msg("some_message"), the server will receive "some_message\r\n".

  2. No separators: When you pass the option no_separator: true when creating your socket, easy_sockets will not add anything to the end of the message. This is useful when both client and server uses a more specific protocol. For instance, both client and server know that the first 4 bytes of the message represent and little ending integer, and depending on the value of that integer, the message will have a specific size and format.

Whether to use separators or not, is totally up to what both client and server expects.

If you decide to use new lines as the message separator, remember that it is \n on Unix systems but \r\n on Windows. So, be sure that both client and server are using the same separator.

Installation

Add this line to your application's Gemfile:

gem 'easy_sockets'

And then execute:

$ bundle

Or install it yourself as:

$ gem install easy_sockets

Usage

Make sure you have netcat installed on your system. We will use it to emulate our servers.

TCP Sockets

Open up a terminal window and type the following to start a TCP server:

nc -ckl 2500

On another terminal window, run the following code to start the client:

require 'easy_sockets'

host = ARGV[0] || '127.0.0.1'

port = ARGV[1].to_i
port = 2500 if port <= 0

opts = {
    host:      host,
    port:      port,
    timeout:   300,
    separator: "\r\n",
    logger: Logger.new(STDOUT),
}
s = EasySockets::TcpSocket.new(opts)
[:INT, :QUIT, :TERM].each do |signal|
    Signal.trap(signal) do
        exit
    end
end

loop do
    puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
    msg = gets.chomp
    s.send_msg(msg)
end

Then typing sample_request in the client terminal, you should see:

$ bundle exec ruby examples/tcp_socket.rb 
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
sample_request
D, [2016-06-30T15:17:39.648945 #90385] DEBUG -- : Successfully connected to tcp://127.0.0.1:2500.
D, [2016-06-30T15:17:39.649044 #90385] DEBUG -- : Sending "sample_request\r\n"

And the server terminal window should display:

$ nc -ckl 2500
sample_request

Then type sample_response on the server terminal window, and you should see:

$ nc -ckl 2500
sample_request
sample_response

And the client window should show:

$ bundle exec ruby examples/tcp_socket.rb 
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
sample_request
D, [2016-06-30T15:17:39.648945 #90385] DEBUG -- : Successfully connected to tcp://127.0.0.1:2500.
D, [2016-06-30T15:17:39.649044 #90385] DEBUG -- : Sending "sample_request\r\n"
D, [2016-06-30T15:19:52.494791 #90385] DEBUG -- : Got "sample_response\r\n"
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:

Press Ctrl+c on the client and server terminal windows to terminate both.

Unix Sockets

Open up a terminal window and type the following to start a Unix server:

nc -Ucl /tmp/test_socket

On another terminal window, run the following code to start the client:

require 'easy_sockets'

host = ARGV[0] || '127.0.0.1'

socket_path = ARGV[1]
socket_path ||= '/tmp/test_socket'

opts = {
    host:      host,
    socket_path: socket_path,
    timeout:   300,
    separator: "\n",
    logger: Logger.new(STDOUT),
}
s = EasySockets::UnixSocket.new(opts)
[:INT, :QUIT, :TERM].each do |signal|
    Signal.trap(signal) do
        exit
    end
end

loop do
    puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
    msg = gets.chomp
    s.send_msg(msg)
end

Then typing sample_request in the client terminal, you should see:

marcosortiz@~/dev/easy_sockets$ bundle exec ruby examples/unix_socket.rb 
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
sample_request
D, [2016-06-30T15:38:10.303188 #96993] DEBUG -- : Successfully connected to /tmp/test_socket.
D, [2016-06-30T15:38:10.303265 #96993] DEBUG -- : Sending "sample_request\n"

And the server terminal window should display:

$ nc -Ul /tmp/test_socket
sample_request

Then type sample_response on the server terminal window, and you should see:

$ nc -Ul /tmp/test_socket
sample_request
sample_response    

And the client window should show:

$ bundle exec ruby examples/unix_socket.rb 
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
sample_request
D, [2016-06-30T15:38:10.303188 #96993] DEBUG -- : Successfully connected to /tmp/test_socket.
D, [2016-06-30T15:38:10.303265 #96993] DEBUG -- : Sending "sample_request\n"
D, [2016-06-30T15:38:23.503411 #96993] DEBUG -- : "sample_response\n"
D, [2016-06-30T15:38:23.503488 #96993] DEBUG -- : Got "sample_response\n"
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:

Press Ctrl+c on the client and server terminal windows to terminate both. Also, type rm -rf /tmp/test_socket to remove the socket file.

UDP Sockets

Open up a terminal window and type the following to start a TCP server:

nc -ukcl 2500

On another terminal window, run the following code to start the client:

require 'easy_sockets'

host = ARGV[0] || '127.0.0.1'

port = ARGV[1].to_i
port = 2500 if port <= 0

opts = {
    host:      host,
    port:      port,
    timeout:   300,
    separator: "\r\n",
    logger: Logger.new(STDOUT),
}
s = EasySockets::UdpSocket.new(opts)
[:INT, :QUIT, :TERM].each do |signal|
    Signal.trap(signal) do
        exit
    end
end

loop do
    puts "Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:"
    msg = gets.chomp
    s.send_msg(msg)
end

Then typing sample_request in the client terminal, you should see:

$ bundle exec ruby examples/udp_socket.rb 
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
sample_request
D, [2016-07-08T10:45:17.935787 #83697] DEBUG -- : Successfully connected to udp://127.0.0.1:2500.
D, [2016-07-08T10:45:17.935893 #83697] DEBUG -- : Sending "sample_request\r\n"

And the server terminal window should display:

$ nc -ukcl 2500
sample_request

Then type sample_response on the server terminal window, and you should see:

$ nc -ukcl 2500
sample_request
sample_response

And the client window should show:

$ bundle exec ruby examples/udp_socket.rb 
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:
sample_request
D, [2016-07-08T10:45:17.935787 #83697] DEBUG -- : Successfully connected to udp://127.0.0.1:2500.
D, [2016-07-08T10:45:17.935893 #83697] DEBUG -- : Sending "sample_request\r\n"
D, [2016-07-08T10:45:22.086731 #83697] DEBUG -- : Got "sample_response\r\n"
Please write the message you want to send and hit ENTER, or type Ctrl+c to quit:

Press Ctrl+c on the client and server terminal windows to terminate both.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

5. Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/marcosortiz/easy_sockets.

6. License

The gem is available as open source under the terms of the MIT License.

About

Over and over I see developers struggling to implement basic sockets with features available on ruby socket stdlib.

Resources

License

Stars

Watchers

Forks

Packages

No packages published