Home
Ratchet Library :: Sockets
API  ·  Manual

ratchet.socket API Reference

Creation

Sockets are one of the handier usages of the ratchet library, as one process is fully capable of handling many connections as long as functions that normally would block the process instead allow other code to run. The socket library in ratchet looks and works very similar to a simple Lua binding of the system calls, but avoids blocking and instead schedules other threads that are ready to go.

To create a new socket object, you generally use one of the prepare_XXX functions (such as prepare_tcp) and pass parts of the result into ratchet.socket.new(). You may also have an existing file descriptor integer from somewhere else that you want to wrap a ratchet socket around, in which case ratchet.socket.from_fd() will do the trick.

For sockets that listen on a port, the socket:accept() method will return a brand new socket object for new connections.

Once a socket object is created, that object claims ownership of the file descriptor, which will be closed if socket:close() is called or when the object is collected.

local rec = ratchet.socket.prepare_tcp("ratchet.icgood.net", 80)
local socket = ratchet.socket.new(rec.family, rec.socktype, rec.protocol)

 

local fd = get_fd_for_some_socket()
local socket = ratchet.socket.from_fd(fd)

 

local client_socket, from_ip = server_socket:accept()

Connect / Listen

Once you have a socket object, unless it was created with socket:accept(), it needs to either connect to a server or open itself to connections.

To connect to a server, use socket:connect() with a userdata object that contains a struct sockaddr to connect to. Often, this sockaddr is obtained from a [DNS resolution][1]. When connecting, the thread will pause and wait for success or failure. On failure, connect() returns false (or raises an error if the failure was unusual). On success, it returns true. A socket can also bind() to a struct sockaddr pointing to a local network interface before connecting, which means the connection will appear to originate from that network interface on the other end of the socket.

local rec = ratchet.socket.prepare_tcp("ratchet.icgood.net", 80)
local socket = ratchet.socket.new(rec.family, rec.socktype, rec.protocol)
if self:connect(rec.addr) then
    -- Connection succeeded!
end

To open a socket for listening, first use socket:bind() with a userdata object that contains a struct sockaddr with the interface and port to listen on. If the sockaddr was from a [DNS resolution][1] where the host was given as an asterisk (*), then the socket will listen on all network interfaces. Once bound to a sockaddr, the socket must then call socket:listen(), which optionally takes the maximum queue size for incoming connections and defaults to SOMAXCONN (see listen(2)). A listening socket cannot call send() or recv(), it may only call accept().

local rec = ratchet.socket.prepare_tcp("ratchet.icgood.net", 80)
local socket = ratchet.socket.new(rec.family, rec.socktype, rec.protocol)
self:bind(rec.addr)
self:listen()

Communication

On a socket that ran socket:connect() or a socket created by socket:accept(), communication is what makes the socket useful. A socket may send or receive data, or shutdown a part of the connection.

Send

To send data across the connection, call socket:send() with a string of data. This call will pause the current thread until the connection is ready to send and then send the string. A connection is "ready to send" when the internal buffers used by Berkeley sockets are available (see EAGAIN in send(2)).

socket:send("hello world")

Recv

To receive data from across the connection, call socket:recv(). This call will pause the current thread until data is available on the connection, and will return it.

local data = socket:recv()
assert(data == "hello world")

Shutdown

Sometimes it is useful to shutdown either the send or receive capabilities of a socket. This is done with socket:shutdown(), which corresponds to the shutdown(2) system call. The arguments to shutdown may be one of "read", "write", or "both", with "both" being the default.

If a socket calls shutdown() with "write", the other end of the socket will receive an empty string, usually signalling the socket has been closed. However, they can still send data back, and the socket can still call recv(). This may be useful for protocols to signal to the other end that it is done sending an arbitrary data chunk.

-- Client side...
socket:shutdown("write")
local data = socket:recv()
assert(data == "goodbye")

-- Server side...
local data = socket:recv()
assert(data == "")
socket:send("goodbye")

Buffered Communication

Once a socket has been connected and is in a stage ready for normal communication, a socketpad object can be created to wrap the socket in a more usable, buffered manner. To create a socketpad object from an existing socket:

local pad = ratchet.socketpad.new(socket)

This object can then be used as described in the following subsections.

Buffered Send

Often it is desired to save up data for a socket to send all at once. A protocol could implement its own buffering, or a socketpad object can store up data to be sent.

The send() method of a socketpad is identical to that of a normal socket if called the same way, but it provides an optional parameter to say that there is more data coming before the message should be sent across the network. The following example shows two equivalent statements, one done with raw sockets and one with buffered socketpad:

local pad = ratchet.socketpad.new(socket)

-- Sending two lines in one message with socketpad:
pad:send('line one\r\n', true)
pad:send('line two\r\n')

-- Sending two lines in one message with raw socket:
socket:send('line one\r\nline two\r\n')

An example use case for this would be the PIPELINING extension to the SMTP protocol, where a client tries to send as many pipeline-able commands as possible in the same transmission.

Buffered Recv

Socket protocol specifications, whether ASCII or binary, often specify methods of reading chunks of data. A binary protocol may declare reading 4 bytes from the socket to get the size of incoming chunk of data. An ASCII protocol may say a command is defined as everything up-to a CR-LF.

Passing a number to the recv() method of socketpad will attempt to read that many bytes from the network. Here is an example of how that could be implemented:

local chunk_size = ratchet.socket.ntoh(pad:recv(4))
local chunk = pad:recv(chunk_size)

Passing a string to the recv() method of socketpad will attempt to read from the network until that string is seen. Here is an example of how that could be implemented:

local command = pad:recv("\r\n")
if command == "QUIT\r\n" then
    quit()
end

Either way, receiving by bytes or by string, you may have received more data from the network than you asked for. That remaining data will be used in the next call to socketpad recv(), or you can glance at with the peek() method. With peek(), you can be sure that you will not pause the thread or attempt to receive data from the network.

Encryption

Many important web services allow encryption to protect both sensitive data and against interception. As SSL/TLS is the dominant form of encryption for sockets, ratchet provides it through OpenSSL. Because most encrypted traffic will look similar to, and follow the same protocols of, unencrypted traffic, calls to send()/recv() after encryption is initiated look and function exactly the same as unencrypted ones. That way, encryption can be added or enabled without changing or adding special versions of a protocol's communication.

To initiate encryption on a socket, use the socket:encrypt() method. This method requires you give it a ratchet.ssl object. The returned session object from this method may be worth hanging on to, for example to initiate a handshake with the remote host to verify its authenticity. See the API1 2 and manual pages for details

local ssl = ratchet.ssl.new()
ssl:load_cas("/path/to/CAs")

local rec = ratchet.socket.prepare_tcp("ratchet.icgood.net", 80)
local socket = ratchet.socket.new(rec.family, rec.socktype, rec.protocol)
socket:connect(rec.addr)

-- Do any unencrypted send()/recv()'s.

local enc = socket:encrypt(ssl)
enc:client_handshake()

local got_cert, verified, host_matched = enc:verify_certificate(rec.host)
assert(got_cert, "Remote peer did not provide a certificate.")
assert(verified, "Remote peer certificate was not verified by trusted CAs.")
assert(host_matched, "Remote peer certificate CN did not match host.")

-- Further send()/recv()'s will be encrypted.

Timeouts

By default, socket methods that pause the thread will do so indefinitely until the the action is taken successfully. This is rarely desirable in the real world, as a bad remote host could indefinitely consume valuable server resources. With timeouts, unsuccessful calls will unpause the thread after a certain number of seconds. To set the timeout for all socket methods that pause, use socket:set_timeout() with a floating-point number of seconds. To get the current timeout setting, use socket:get_timeout(). If you need different timeouts for different socket methods, you must call set_timeout() every time it must be changed.

local rec = ratchet.socket.prepare_tcp("website.is.inaccessible.com", 80)
local socket = ratchet.socket.new(rec.family, rec.socktype, rec.protocol)
socket:set_timeout(10.0)
if not pcall(socket.connect, socket, rec.addr) then
    -- Connected refused or 10 second timeout has elapsed.
end

Socket Options

POSIX sockets provide certain socket options via the getsockopt(2) and setsockopt(2) system calls. In the ratchet library, these options are exposted for access and change as simple object properties. That means to find out, for example, the size of the socket's send buffer, you can simply index the socket object on "SO_SNDBUF" and the value is retrieved real-time. To change it, you assign a new value to the "SO_SNDBUF" index. Available socket options are system-specific, a list of possibilities is found on the socket(7) man page.

local function get_socket_send_size(socket)
    return socket:getsockopt("SO_SNDBUF") / 2
end

local function set_socket_send_size(socket, new_size)
   socket:setsockopt("SO_SNDBUF", new_size)
end



Last modified:  Sun, 17 Aug 2014 09:32:32 -0400
Author:  ian.good