Skip to content

Commit 3b82a24

Browse files
committed
Added basic HTTP server + client recipes.
1 parent c2636cc commit 3b82a24

File tree

4 files changed

+335
-0
lines changed

4 files changed

+335
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ _site
33

44
*.swp
55
*.swo
6+
7+
.DS_Store

authors.textile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ _The following people are totally rad and awesome because they have contributed
1414
* Sebastian Slomski [email protected]_
1515
* Aaron Weinberger [email protected]_
1616
* James C. Holder [email protected]_
17+
* João Moreno _coffeecb @joaomoreno.com_
1718
* ...You! What are you waiting for? Check out the <a href="/contributing">contributing</a> section and get cracking!
1819

1920

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
layout: recipe
3+
title: Basic HTTP Client
4+
chapter: Networking
5+
---
6+
7+
h2. Problem
8+
9+
You want to create a HTTP client.
10+
11+
h2. Solution
12+
13+
In this recipe, we'll use "node.js":http://nodejs.org/ 's HTTP library. We'll go from a simple GET request example to a client which returns the external IP of a computer.
14+
15+
h3. GET something
16+
17+
{% highlight coffeescript %}
18+
http = require 'http'
19+
20+
http.get { host: 'www.google.com' }, (res) ->
21+
console.log res.statusCode
22+
{% endhighlight %}
23+
24+
The @get@ function, from node.js's @http@ module, issues a GET request to a HTTP server. The response comes in the form of a callback, which we can handle in a function. This example merely prints the response status code. Check it out:
25+
26+
{% highlight console %}
27+
$ coffee http-client.coffee
28+
200
29+
30+
{% endhighlight %}
31+
32+
h3. What's my IP?
33+
34+
If you are inside a network which relies on "NAT":http://en.wikipedia.org/wiki/Network_address_translation such as a LAN, you probably have faced the issue of finding out what's your external IP address. Let's write a small coffeescript for this.
35+
36+
{% highlight coffeescript %}
37+
http = require 'http'
38+
39+
http.get { host: 'checkip.dyndns.org' }, (res) ->
40+
data = ''
41+
res.on 'data', (chunk) ->
42+
data += chunk.toString()
43+
res.on 'end', () ->
44+
console.log data.match(/([0-9]+\.){3}[0-9]+/)[0]
45+
{% endhighlight %}
46+
47+
We can get the data from the result object by listening on its @'data'@ event; and know that it has come to an end once the @'end'@ event has been fired. When that happens, we can do a simple regular expression match to extract our IP address. Try it:
48+
49+
{% highlight console %}
50+
$ coffee http-client.coffee
51+
123.123.123.123
52+
{% endhighlight %}
53+
54+
h2. Discussion
55+
56+
Note that @http.get@ is a shortcut of @http.request@. The latter allows you to issue HTTP requests with different methods, such as POST or PUT.
57+
58+
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.
59+
60+
h3. Exercises
61+
62+
* Create a client for the key-value store HTTP server, from the "Basic HTTP Server":http://coffeescriptcookbook.com/chapters/networking/basic-http-server recipe.
63+
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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 &mdash; @createServer@ &mdash; 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 &mdash; how about some exception throwing &amp; 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

Comments
 (0)