óCoffeeScript Cookbook

最简单的HTTP服务器

问题

你想到网络中搭建一个HTTP服务器。在本菜谱中,我们从最简单的服务器到一个功能完好的键值对存储服务器,一步步地学习创建HTTP服务器。

方法

处于自私的目的,我们将使用node.js的HTTP类库,使用CoffeeScript创建最简单的服务器。

问候 ‘hi\n’

我们可以从引入node.js的HTTP模块开始。该模块包含了createServer,这是一个简单的请求处理器,返回一个HTTP服务器。我们让这个服务器监听在一个TCP端口上。

http = require 'http'
server = http.createServer (req, res) -> res.end 'hi\n'
server.listen 8000

把这些代码放在一个文件中运行,就可以执行这个示例。你可以使用Ctrl-C来关掉它。我们可以使用curl命令测试,在绝大多数的*nix平台上都可以运行:

$ curl -D - http://localhost:8000/
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked

hi

接下来呢?

让我们弄点反馈,看看我们服务器上发生了什么。同时,我们还对我们用户更友好一点,并为他们提供一些HTTP头。

http = require 'http'

server = http.createServer (req, res) ->
    console.log req.method, req.url
    data = 'hi\n'
    res.writeHead 200,
        'Content-Type':     'text/plain'
        'Content-Length':   data.length
    res.end data

server.listen 8000

试着再访问一次这个服务器,但是这次要使用其他的URL地址。比如 http://localhost:8000/coffee。你可以在服务器上看到如下的调试信息:

$ coffee http-server.coffee 
GET /
GET /coffee
GET /user/1337

GET点啥

服务器上放点数据?我们就放一个简单的键值存储吧,键值元素可以通过GET请求获取。把key放到请求路径中,服务器就会返回相应的value &mdash,如果不错在的话就返回404。

http = require 'http'

store = # we'll use a simple object as our store
    foo:    'bar'
    coffee: 'script'

server = http.createServer (req, res) ->
    console.log req.method, req.url
    
    value = store[req.url[1..]]

    if not value
        res.writeHead 404
    else
        res.writeHead 200,
            'Content-Type': 'text/plain'
            'Content-Length': value.length + 1
        res.write value + '\n'
    
    res.end()

server.listen 8000

我们可以找几个URLs尝试一下,看看他是如何返回的:

$ curl -D - http://localhost:8000/coffee
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 7
Connection: keep-alive

script

$ curl -D - http://localhost:8000/oops
HTTP/1.1 404 Not Found
Connection: keep-alive
Transfer-Encoding: chunked

加上headers

承认吧,text/plain挺无聊的。我们要不使用application/json或者text/xml等试试看?并且,我们的读取存储的过程是不是应该重构一下&mdash,添加点异常处理?让我们看看能产生什么效果:

http = require 'http'

# known mime types
[any, json, xml] = ['*/*', 'application/json', 'text/xml']

# gets a value from the db in format [value, contentType]
get = (store, key, format) ->
    value = store[key]
    throw 'Unknown key' if not value
    switch format
        when any, json then [JSON.stringify({ key: key, value: value }), json]
        when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml]
        else throw 'Unknown format'

store =
    foo:    'bar'
    coffee: 'script'

server = http.createServer (req, res) ->
    console.log req.method, req.url
    
    try
        key = req.url[1..]
        [value, contentType] = get store, key, req.headers.accept
        code = 200
    catch error
        contentType = 'text/plain'
        value = error
        code = 404
 
    res.writeHead code,
        'Content-Type': contentType
        'Content-Length': value.length + 1
    res.write value + '\n'
    res.end()

server.listen 8000

服务器返回的还是key能够匹配到的值,无匹配的话就返回404。但是它根据Accept头,把返回值格式化成了JSON或者XML。自己试试看:

$ curl http://localhost:8000/
Unknown key

$ curl http://localhost:8000/coffee
{"key":"coffee","value":"script"}

$ curl -H "Accept: text/xml" http://localhost:8000/coffee
<key>coffee</key>
<value>script</value>

$ curl -H "Accept: image/png" http://localhost:8000/coffee
Unknown format

你必须让他们有恢复的能力

在我们冒险旅行的上一步,给我们的客户端提供了存储数据的能力。我们会保证我们是RESTfull的,提供对POST请求的监听。

http = require 'http'

# known mime types
[any, json, xml] = ['*/*', 'application/json', 'text/xml']

# gets a value from the db in format [value, contentType]
get = (store, key, format) ->
    value = store[key]
    throw 'Unknown key' if not value
    switch format
        when any, json then [JSON.stringify({ key: key, value: value }), json]
        when xml then ["<key>#{ key }</key>\n<value>#{ value }</value>", xml]
        else throw 'Unknown format'

# puts a value in the db
put = (store, key, value) ->
    throw 'Invalid key' if not key or key is ''
    store[key] = value

store =
    foo:    'bar'
    coffee: 'script'

# helper function that responds to the client
respond = (res, code, contentType, data) ->
    res.writeHead code,
        'Content-Type': contentType
        'Content-Length': data.length
    res.write data
    res.end()

server = http.createServer (req, res) ->
    console.log req.method, req.url
    key = req.url[1..]
    contentType = 'text/plain'
    code = 404
    
    switch req.method
        when 'GET'
            try
                [value, contentType] = get store, key, req.headers.accept
                code = 200
            catch error
                value = error
            respond res, code, contentType, value + '\n'

        when 'POST'
            value = ''
            req.on 'data', (chunk) -> value += chunk
            req.on 'end', () ->
                try
                    put store, key, value
                    value = ''
                    code = 200
                catch error
                    value = error + '\n'
                respond res, code, contentType, value

server.listen 8000

请注意是如何接受POST请求中的数据的。听过给请求对象的'data''end'事件绑定处理器来实现。我们可以把来来自客户端的数据暂存或者最终存储到store中。

$ curl -D - http://localhost:8000/cookie
HTTP/1.1 404 Not Found # ...
Unknown key

$ curl -D - -d "monster" http://localhost:8000/cookie
HTTP/1.1 200 OK # ...

$ curl -D - http://localhost:8000/cookie
HTTP/1.1 200 OK # ...
{"key":"cookie","value":"monster"}

Discussion

讨论

http.createServer传递一个型如(request, respone) ->...这样的函数,它就会返回一个server对象,我们可以使用这个server对象监听某个端口。与requestresponse对象交互,实现server的行为。使用server.listen 8000来监听8000端口。

关于这个主题的API或者更为详细的信息,请参考http以及https这两页文档。而且HTTP spec迟早也会用到。

练习

  • 在服务器和开发者之间建立一个layer(layer),允许开发者可以像下面这样写:
server = layer.createServer
    'GET /': (req, res) ->
        ...
    'GET /page': (req, res) ->
        ...
    'PUT /image': (req, res) ->
        ...