Sanford Protocol
Ruby implementation of Sanford TCP communication protocol.
The Protocol
Version: 2
Sanford communicates using binary encoded messages. Sanford messages are two headers and a body:
|------ 1B -------|------ 4B -------|---- (Body Size)B ----|
| (packed header) | (packed header) | (BSON binary string) |
| Version | Body Size | Body |
|-----------------|-----------------|----------------------|
Version
The first header represents the protocol version in use. It is a 1 byte unsigned integer and exists to ensure both the client and the server are talking the same protocol.
Body Size
The second header represents the size of the message's body. It is a 4 byte unsigned integer and tells the receiver how many bytes to read to receive the body.
Body
The Body is the content of the message. It is a BSON encoded binary string that decodes to a ruby hash. Since the size of the body is encoded as a 4 byte (32 bit) unsigned integer, there is a size limit for body data ((2 ** 32) - 1
or 4,294,967,295
or ~4GB
).
Request
A request is made up of 2 required parts: the name, and the params.
- name - (string) name of the requested API service.
- params - (document) data for the service call - must be a BSON document (ruby Hash, python dict, Javascript Object).
Requests are encoded as BSON hashes when transmitted in messages.
{ 'name' => 'some_service',
'params' => { 'key' => 'value' }
}
request = Sanford::Protocol::Request.parse(a_bson_request_hash)
request.name
request.params
request.to_s
Response
A response is made up of 2 parts: the status and the data.
- status - (tuple, required) A code and message describing the result of the service call.
- data - (object, optional) Return value of the service call. This can be any BSON serializable object. Typically won't be set if the request is not successful.
Responses are encoded as BSON hashes when transmitted in messages.
{ 'status' => [ 200, 'The request was successful.' ]
'data' => true
}
response = Sanford::Protocol::Response.parse(a_bson_response_hash)
response.status.code
response.status.to_i
response.status.name
response.status.message
response.status.to_s
response.code
response.to_s
response.data
Status Codes
This is the list of defined status codes.
200
- OK
- The request was successful.400
- BAD REQUEST
- The request couldn't be read. This is usually because it was not formed correctly.404
- NOT FOUND
- The server couldn't find something requested.408
- TIMEOUT
- A client connected but didn't write a request before the server timeod out waiting for one.422
- INVALID
- The request was sent with invalid params.500
- ERROR
- The server errored responding to the request.
In addition to these, a service can return custom status codes, but they should use a number greater than or equal to 600
to avoid collisions with Sanford's defined status codes.
Usage
The Sanford::Protocol
module defines helper methods for encoding and decoding messages.
data = { 'something' => true }
msg_body = Sanford::Protocol.msg_body.encode(data)
msg_size = Sanford::Protocol.msg_size.encode(msg_body.bytesize)
msg = [Sanford::Protocol.msg_version, msg_size, msg_body].join
Connection
If you are sending and receiving messages using a tcp socket, use Sanford::Protocol::Connection
.
connection = Sanford::Protocol::Connection.new(tcp_socket)
incoming_data = connection.read
connection.write(outgoing_data)
For incoming messages, it reads them off the socket, validates them, and returns the decoded body data. For outgoing messages, it encodes the message body from given data, adds the appropiate message headers, and writes the message to the socket.
Timeout
When reading data from a connection, you can optionally pass a timeout value. If given, the connection will block and wait until data is ready to be read. If a timeout occurs, the connection will raise TimeoutError
.
begin
connection.read(10)
rescue Sanford::Protocol::TimeoutError => err
puts "timeout - so sad :("
end
Requests And Responses
Request and response objects have helpers for sending and receiving data using a connection.
data_hash = server_connection.read
incoming_request = Sanford::Protocol::Request.parse(data_hash)
outgoing_response = Sanford::Protocol::Response.new(status, data)
server_connection.write(outgoing_response.to_hash)
outgoing_request = Sanford::Protocol::Request.new(name, params)
client_connection.write(outgoing_request.to_hash)
data_hash = client_connection.read
incoming_response = Sanford::Protocol::Response.parse(data_hash)
Test Helpers
A FakeSocket
helper class and an associated TestHelpers
module are provided to help test receiving and sending Sanford::Protocol messages without using real sockets.
socket = FakeSocket.new(msg_binary_string)
connection = Sanford::Protocol::Connection.new(socket)
msg_data = connection.read
connection.write(msg_data)
puts socket.out
socket = FakeSocket.with_request(*request_params)
msg_data = Sanford::Protocol::Connection.new(socket).read
request = Sanford::Protocol::Request.parse(msg_data)
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request