1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
|
# Design Overview
The interfaces provided by {ruby Protocol::HTTP} underpin all downstream implementations. Therefore, we provide some justification for the design choices.
## Request/Response Model
The main model we support is the request/response model. A client sends a request to a server which return response. The protocol is responsible for serializing the request and response objects.
```mermaid
sequenceDiagram
participant CA as Application
participant Client
participant Server
participant SA as Application
CA->>+Client: Request
Client->>+Server: Request
Server->>+SA: Request
SA->>+Server: Response
Server->>+Client: Response
Client->>+CA: Response
```
We provide an interface for request and response objects. This provides performance, predictability and robustness. This model has proven itself over several years, handling a variety of different use cases.
~~~ ruby
class Request
attr :verb
attr :target
attr :headers
attr :body
end
class Response
attr :status
attr :headers
attr :body
end
~~~
One other advantage is that it's symmetrical between clients and servers with a clear mapping, i.e. the protocol is responsible for transiting requests from the client to the server, and responses from the server back to the client. This helps us separate and define request/response interfaces independently from protocol implementation.
### Client Design
A request/response model implies that you create a request and receive a response back. This maps to a normal function call where the request is the argument and the response is the returned value.
~~~ ruby
request = Request.new("GET", url)
response = client.call(request)
response.headers
response.read
~~~
## Stream Model
An alternative model is the stream model. This model is more suitable for WebSockets and other persistent bi-directional channels.
```mermaid
sequenceDiagram
participant CA as Application
participant Client
participant Server
participant SA as Application
CA->>+Client: Stream
Client->>+Server: Stream
Server->>+SA: Stream
```
The interfaces for streaming can be implemented a bit differently, since a response is not returned but rather assigned to the stream, and the streaming occurs in the same execution context as the client or server handling the request.
~~~ ruby
class Stream
# Request details.
attr :verb
attr :target
attr :headers
attr :response
# Write the response and start streaming the output body.
def respond(status, headers)
response.status = status
response.headers = headers
end
# Request body.
attr_accessor :input
# Response body.
attr_accessor :output
# Write to the response body.
def write(...)
@output.write(...)
end
# Read from the request body.
def read
@input.read
end
end
class Response
def initialize(verb, target)
@input = Body::Writable.new
@output = Body::Writable.new
end
attr_accessor :status
attr_accessor :headers
# Prepare a stream for making a request.
def request(verb, target, headers)
# Create a request stream suitable for writing into the buffered response:
Stream.new(verb, target, headers, self, @input, @output)
end
# Write to the request body.
def write(...)
@input.write(...)
end
# Read from the response body.
def read
@output.read
end
end
~~~
### Client Design
A stream model implies that you create a stream which contains both the request and response bodies. This maps to a normal function call where the argument is the stream and the returned value is ignored.
~~~ ruby
response = Response.new
stream = response.request("GET", url)
client.call(stream)
response.headers
response.read
~~~
## Differences
The request/response model has a symmetrical design which naturally uses the return value for the result of executing the request. The result encapsulates the behaviour of how to read the response status, headers and body. Because of that, streaming input and output becomes a function of the result object itself. As in:
~~~ ruby
def call(request)
body = Body::Writable.new
Fiber.schedule do
while chunk = request.input.read
body.write(chunk.reverse)
end
end
return Response[200, headers, body]
end
input = Body::Writable.new
response = call(... body ...)
input.write("Hello World")
input.close
response.read -> "dlroW olleH"
~~~
The streaming model does not have the same symmetry, and instead opts for a uni-directional flow of information.
~~~ruby
def call(stream)
stream.respond(200, headers)
Fiber.schedule do
while chunk = stream.read
stream.write(chunk.reverse)
end
end
end
input = Body::Writable.new
response = Response.new(...input...)
call(response.stream)
input.write("Hello World")
input.close
response.read -> "dlroW olleH"
~~~
The value of this uni-directional flow is that it is natural for the stream to be taken out of the scope imposed by the nested `call(request)` model. However, the user must explicitly close the stream, since it's no longer scoped to the client and/or server.
|