|
| 1 | +--- |
| 2 | +layout: recipe |
| 3 | +title: Basic HTTP Server |
| 4 | +chapter: Networking |
| 5 | +--- |
| 6 | + |
| 7 | +h2. Problem |
| 8 | + |
| 9 | +You want to create a HTTP server over a network. Over the course of this recipe, we'll go step by step from the smallest server possible to a functional key-value store. |
| 10 | + |
| 11 | +h2. Solution |
| 12 | + |
| 13 | +We'll use "node.js":http://nodejs.org/ 's HTTP library to our own selfish purposes and create the simplest web server possible in Coffeescript. |
| 14 | + |
| 15 | +h3. Say 'hi\n' |
| 16 | + |
| 17 | +We can start by importing the @http@ module. This module has a nice helper function — @createServer@ — which, given a simple request handler, creates a HTTP server. All that's left to do then is have the server listening on a port. |
| 18 | + |
| 19 | +{% highlight coffeescript %} |
| 20 | +http = require 'http' |
| 21 | +server = http.createServer (req, res) -> res.end 'hi\n' |
| 22 | +server.listen 8000 |
| 23 | +{% endhighlight %} |
| 24 | + |
| 25 | +To run this example, simply put in a file and run it. You can kill it with @Ctrl-C@. We can test it using the @curl@ command, available on most *nix platforms: |
| 26 | + |
| 27 | +{% highlight console %} |
| 28 | +$ curl -D - http://localhost:8000/ |
| 29 | +HTTP/1.1 200 OK |
| 30 | +Connection: keep-alive |
| 31 | +Transfer-Encoding: chunked |
| 32 | + |
| 33 | +hi |
| 34 | +{% endhighlight %} |
| 35 | + |
| 36 | +h3. What's going on? |
| 37 | + |
| 38 | +Let's get a little bit more feedback on what's happening on our server. While we're at it, we could also be friendlier to our clients and provide them some HTTP headers. |
| 39 | + |
| 40 | +{% highlight coffeescript %} |
| 41 | +http = require 'http' |
| 42 | + |
| 43 | +server = http.createServer (req, res) -> |
| 44 | + console.log req.method, req.url |
| 45 | + data = 'hi\n' |
| 46 | + res.writeHead 200, |
| 47 | + 'Content-Type': 'text/plain' |
| 48 | + 'Content-Length': data.length |
| 49 | + res.end data |
| 50 | + |
| 51 | +server.listen 8000 |
| 52 | +{% endhighlight %} |
| 53 | + |
| 54 | +Try to access it once again, but this time use different URL paths, such as @http://localhost:8000/coffee@. You'll see something like this on the server console: |
| 55 | + |
| 56 | +{% highlight console %} |
| 57 | +$ coffee http-server.coffee |
| 58 | +GET / |
| 59 | +GET /coffee |
| 60 | +GET /user/1337 |
| 61 | +{% endhighlight %} |
| 62 | + |
| 63 | +h3. GETting stuff |
| 64 | + |
| 65 | +What if our webserver was able to hold some data? We'll try to come up with a simple key-value store in which elements are retrievable via GET requests. Provide a key on the request path and the server will return the corresponding value - or 404 if it doesn't exist. |
| 66 | + |
| 67 | +{% highlight coffeescript %} |
| 68 | +http = require 'http' |
| 69 | + |
| 70 | +store = # we'll use a simple object as our store |
| 71 | + foo: 'bar' |
| 72 | + coffee: 'script' |
| 73 | + |
| 74 | +server = http.createServer (req, res) -> |
| 75 | + console.log req.method, req.url |
| 76 | + |
| 77 | + value = store[req.url[1..]] |
| 78 | + |
| 79 | + if not value |
| 80 | + res.writeHead 404 |
| 81 | + else |
| 82 | + res.writeHead 200, |
| 83 | + 'Content-Type': 'text/plain' |
| 84 | + 'Content-Length': value.length + 1 |
| 85 | + res.write value + '\n' |
| 86 | + |
| 87 | + res.end() |
| 88 | + |
| 89 | +server.listen 8000 |
| 90 | +{% endhighlight %} |
| 91 | + |
| 92 | +{% highlight console %} |
| 93 | +$ curl -D - http://localhost:8000/coffee |
| 94 | +HTTP/1.1 200 OK |
| 95 | +Content-Type: text/plain |
| 96 | +Content-Length: 7 |
| 97 | +Connection: keep-alive |
| 98 | + |
| 99 | +script |
| 100 | + |
| 101 | +$ curl -D - http://localhost:8000/oops |
| 102 | +HTTP/1.1 404 Not Found |
| 103 | +Connection: keep-alive |
| 104 | +Transfer-Encoding: chunked |
| 105 | + |
| 106 | +{% endhighlight %} |
| 107 | + |
| 108 | +h3. Use your head(ers) |
| 109 | + |
| 110 | +Let's face it, @text/plain@ is kind of lame. How about if we use something hip like @application/json@ or @text/xml@? Also, our store retrieval process could use a bit of refactoring — how about some exception throwing & handling? Let's see what we can come up with: |
| 111 | + |
| 112 | +{% highlight coffeescript %} |
| 113 | +http = require 'http' |
| 114 | + |
| 115 | +# known mime types |
| 116 | +[any, json, xml] = ['*/*', 'application/json', 'text/xml'] |
| 117 | + |
| 118 | +# gets a value from the db in format [value, contentType] |
| 119 | +get = (store, key, format) -> |
| 120 | + value = store[key] |
| 121 | + throw 'Unknown key' if not value |
| 122 | + switch format |
| 123 | + when any, json then [JSON.stringify({ key: key, value: value }), json] |
| 124 | + when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml] |
| 125 | + else throw 'Unknown format' |
| 126 | + |
| 127 | +store = |
| 128 | + foo: 'bar' |
| 129 | + coffee: 'script' |
| 130 | + |
| 131 | +server = http.createServer (req, res) -> |
| 132 | + console.log req.method, req.url |
| 133 | + |
| 134 | + try |
| 135 | + key = req.url[1..] |
| 136 | + [value, contentType] = get store, key, req.headers.accept |
| 137 | + code = 200 |
| 138 | + catch error |
| 139 | + contentType = 'text/plain' |
| 140 | + value = error |
| 141 | + code = 404 |
| 142 | + |
| 143 | + res.writeHead code, |
| 144 | + 'Content-Type': contentType |
| 145 | + 'Content-Length': value.length + 1 |
| 146 | + res.write value + '\n' |
| 147 | + res.end() |
| 148 | + |
| 149 | +server.listen 8000 |
| 150 | +{% endhighlight %} |
| 151 | + |
| 152 | +This server will still return the value which matches a given key, or 404 if non-existent. But it will structure the response either in JSON or XML, according to the @Accept@ header. See for yourself: |
| 153 | + |
| 154 | +{% highlight console %} |
| 155 | +$ curl http://localhost:8000/ |
| 156 | +Unknown key |
| 157 | + |
| 158 | +$ curl http://localhost:8000/coffee |
| 159 | +{"key":"coffee","value":"script"} |
| 160 | + |
| 161 | +$ curl -H "Accept: text/xml" http://localhost:8000/coffee |
| 162 | +<key>coffee</key> |
| 163 | +<value>script</value> |
| 164 | + |
| 165 | +$ curl -H "Accept: image/png" http://localhost:8000/coffee |
| 166 | +Unknown format |
| 167 | +{% endhighlight %} |
| 168 | + |
| 169 | +h3. You gotta give to get back |
| 170 | + |
| 171 | +The obvious last step in our adventure is to provide the client the ability to store data. We'll keep our RESTiness by listening to POST requests for this purpose. |
| 172 | + |
| 173 | +{% highlight coffeescript %} |
| 174 | +http = require 'http' |
| 175 | + |
| 176 | +# known mime types |
| 177 | +[any, json, xml] = ['*/*', 'application/json', 'text/xml'] |
| 178 | + |
| 179 | +# gets a value from the db in format [value, contentType] |
| 180 | +get = (store, key, format) -> |
| 181 | + value = store[key] |
| 182 | + throw 'Unknown key' if not value |
| 183 | + switch format |
| 184 | + when any, json then [JSON.stringify({ key: key, value: value }), json] |
| 185 | + when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml] |
| 186 | + else throw 'Unknown format' |
| 187 | + |
| 188 | +# puts a value in the db |
| 189 | +put = (store, key, value) -> |
| 190 | + throw 'Invalid key' if not key or key is '' |
| 191 | + store[key] = value |
| 192 | + |
| 193 | +store = |
| 194 | + foo: 'bar' |
| 195 | + coffee: 'script' |
| 196 | + |
| 197 | +# helper function that responds to the client |
| 198 | +respond = (res, code, contentType, data) -> |
| 199 | + res.writeHead code, |
| 200 | + 'Content-Type': contentType |
| 201 | + 'Content-Length': data.length |
| 202 | + res.write data |
| 203 | + res.end() |
| 204 | + |
| 205 | +server = http.createServer (req, res) -> |
| 206 | + console.log req.method, req.url |
| 207 | + key = req.url[1..] |
| 208 | + contentType = 'text/plain' |
| 209 | + code = 404 |
| 210 | + |
| 211 | + switch req.method |
| 212 | + when 'GET' |
| 213 | + try |
| 214 | + [value, contentType] = get store, key, req.headers.accept |
| 215 | + code = 200 |
| 216 | + catch error |
| 217 | + value = error |
| 218 | + respond res, code, contentType, value + '\n' |
| 219 | + |
| 220 | + when 'POST' |
| 221 | + value = '' |
| 222 | + req.on 'data', (chunk) -> value += chunk |
| 223 | + req.on 'end', () -> |
| 224 | + try |
| 225 | + put store, key, value |
| 226 | + value = '' |
| 227 | + code = 200 |
| 228 | + catch error |
| 229 | + value = error + '\n' |
| 230 | + respond res, code, contentType, value |
| 231 | + |
| 232 | +server.listen 8000 |
| 233 | +{% endhighlight %} |
| 234 | + |
| 235 | +Notice how the data is received in a POST request. By attaching some handlers on the @'data'@ and @'end'@ events of the request object, we're able to buffer and finally save the data from the client in the @store@. |
| 236 | + |
| 237 | +{% highlight console %} |
| 238 | +$ curl -D - http://localhost:8000/cookie |
| 239 | +HTTP/1.1 404 Not Found # ... |
| 240 | +Unknown key |
| 241 | + |
| 242 | +$ curl -D - -d "monster" http://localhost:8000/cookie |
| 243 | +HTTP/1.1 200 OK # ... |
| 244 | + |
| 245 | +$ curl -D - http://localhost:8000/cookie |
| 246 | +HTTP/1.1 200 OK # ... |
| 247 | +{"key":"cookie","value":"monster"} |
| 248 | +{% endhighlight %} |
| 249 | + |
| 250 | +h2. Discussion |
| 251 | + |
| 252 | +Give @http.createServer@ a function in the shape of @(request, response) -> ...@ and it will return a server object, which we can use to listen on a port. Interact with the @request@ and @response@ objects to give the server its behaviour. Listen on port 8000 using @server.listen 8000@. |
| 253 | + |
| 254 | +For API and overall information on this subject, check node.js's "http":http://nodejs.org/docs/latest/api/http.html and "https":http://nodejs.org/docs/latest/api/https.html documentation pages. Also, the "HTTP spec":http://www.ietf.org/rfc/rfc2616.txt might come in handy. |
| 255 | + |
| 256 | +h3. Exercises |
| 257 | + |
| 258 | +* Create a layer in between the server and the developer which would allow the developer to do something like: |
| 259 | + |
| 260 | +{% highlight coffeescript %} |
| 261 | +server = layer.createServer |
| 262 | + 'GET /': (req, res) -> |
| 263 | + ... |
| 264 | + 'GET /page': (req, res) -> |
| 265 | + ... |
| 266 | + 'PUT /image': (req, res) -> |
| 267 | + ... |
| 268 | +{% endhighlight %} |
| 269 | + |
0 commit comments