A while back I stressed the point that TCP sockets provide an ordered stream of data. In other words, you can imagine the TCP data stream as a queue. For example, one end of a socket connection writes some data to the connection, this pushes it onto the queue. It moves through the various stages (local buffers, network transit, remote buffers), and then is popped off this ‘queue’ by the receiving socket.
This mental model holds true for typical TCP communication. TCP urgent data, more often referred to as out-of-band data, allows you to push data all the way to the front of the queue, where it can be received by the other end of the connection as soon as possible, even bypassing data that is already en route.
There’s a method on
Socket that we haven’t come across yet,
Socket#send is like a specialized version of
Socket#write (inherited from
IO). In fact, without arguments, it behaves just like `write'.
# These have the same effect
write method is generalized to be used with any
IO object, the
send method is specialized to work just with sockets. This specialization allows
Socket#send to accept a second argument: flags. We can specify a flag to
send to denote some data as urgent.
Sending Urgent Data
Let’s have a look:
socket = TCPSocket.new 'localhost', 4481
# Send some data using the standard method
# Send some urgent data
socket.send '!', Socket::MSG_OOB
To send a byte of urgent data, we call
send and pass the
Socket::MSG_OOB constant as the flag.
OOB here refers to out-of-band.
This is the what it takes to send some urgent data, but this isn’t enough to cause the receiver to get the urgent data first. In other words, the sender and receiver need to collaborate in order for this to work.
Receiving Urgent Data
Here’s how the receiver might get the urgent data using
Socket.tcp_server_loop(4481) do |connection|
# receive urgent data first
urgent_data = connection.recv(1, Socket::MSG_OOB)
data = connection.readpartial(1024)
In order to receive urgent data, we had to use
Socket#recv with the same flag we used to send the urgent data. Just as
Socket#send is a socket-specific way to write data,
Socket#recv is a socket-specific way to read data. It, too, can accept flags.
Notice that we were able to consume the urgent data before the ‘regular’ data, even though the regular data was written first. This is what you can do with urgent data. Notice also that we had to explicitly receive the urgent data. If we hadn’t called
recv, this server wouldn’t have noticed the urgent data. In other words, if the receiver isn’t looking for urgent data, it won’t receive any. Read on for ways of dealing with this.
connection.recv(1, Socket::MSG_OOB) will fail with
Errno::EINVAL if there’s no pending urgent data.
You may have noticed that I only sent a single byte of urgent data in the above example. This was intentional. The TCP implementation has limited support for urgent data in that only a single byte of urgent data can be sent at one time. If you send multiple bytes of urgent data, only the last byte will be considered urgent. Any earlier bytes will appear as part of the ‘regular’ TCP data stream.
Urgent Data and IO.select
As with most things, we can use
IO.select to monitor sockets for urgent data. However, there is a serious caveat.
Remember how I said that the third argument to
IO.select was an array of IO objects for which you are interested in out-of-band data? Here’s how it might be used:
for_reading = [<TCPSocket>, <TCPSocket>, <TCPSocket>]
for_writing = [<TCPSocket>, <TCPSocket>, <TCPSocket>]
IO.select(for_reading, for_writing, for_reading)
Notice that we’re passing the array of sockets to monitor for reading as the third argument? This means that if any of those sockets receives urgent data, they’ll be included in the third element of the returned Array from
This is great, it means we can monitor sockets for urgent data without having to call
recv blindly. However, in my experience,
IO.select will continue to say that there’s urgent data available even after it’s all been consumed! This continues until some of the ‘regular’ TCP data stream has been consumed, most likely until the local recv buffer is empty. This means that you’ll need to add some extra error handling or state tracking to make sure you don’t get stuck in a tight loop trying to consume urgent data that isn’t coming.
Given this caveat, and the single byte limitation, this is a rarely-used TCP feature.
The SO_OOBINLINE Option
Another way to deal with urgent data is just to stick it in the regular data stream. There is a socket option, called
SO_OOBINLINE, that will allow out-of-band data to be received in-band. In other words, the urgent data will be combined, in order, with the ‘regular’ data stream. With this option enabled, the urgent data will no longer be treated as such. It will be read from the queue in the same order it was sent relative to other writes.
Here’s how to turn it on:
Socket.tcp_server_loop(4481) do |connection|
# receive urgent data inline with 'regular' data
connection.setsockopt :SOCKET, :OOBINLINE, true
# note that the read stops when it
# encounters urgent data
connection.readpartial(1024) #=> 'foo'
connection.readpartial(1024) #=> '!'
In this example, the
foo data is received before the
! byte, so the urgent data is no longer received first. I made a point of showing that the
read family of methods is aware of urgent data. Even though it could have fit the
foo data and the
! data inside its 1024 byte limit, it stopped reading when it encountered urgent data, returned what it had, then started over.
This option only has an effect on the receiving socket, not the sending socket.