## 问题
你想到网络中搭建一个HTTP服务器。在本菜谱中,我们从最简单的服务器到一个功能完好的键值对存储服务器,一步步地学习创建HTTP服务器。
## 方法
处于自私的目的,我们将使用[node.js](http://nodejs.org/)的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对象监听某个端口。与`request`和`response`对象交互,实现server的行为。使用`server.listen 8000`来监听8000端口。
关于这个主题的API或者更为详细的信息,请参考[http](http://nodejs.org/docs/latest/api/http.html)以及[https](http://nodejs.org/docs/latest/api/https.html)这两页文档。而且[HTTP spec](http://www.ietf.org/rfc/rfc2616.txt)迟早也会用到。
### 练习
* 在服务器和开发者之间建立一个layer(layer),允许开发者可以像下面这样写:
~~~
server = layer.createServer
'GET /': (req, res) ->
...
'GET /page': (req, res) ->
...
'PUT /image': (req, res) ->
...
~~~
- 贡献
- 作者
- 授权协议
- 1、Syntax
- 在服务端和客户端重用代码
- For循环
- 嵌入JavaScript代码
- 值域
- 2、Classes and Objects
- 类方法和实例方法
- CoffeeScript式的Type函数
- 链式调用
- 克隆对象(深度克隆)
- 不存在就赋值为对象字面量
- 类变量
- 3、Strings
- 分割字符串
- 字符串匹配
- 查找子字符串
- 让整个字符串小写
- 大写整个字符
- 去掉字符串首尾的空白
- 生成唯一的ID
- 首字母大写
- 重复字符串
- 字符串插值
- 4、Arrays
- Python式的Zip函数 Python-like Zip Function
- 数组去重 Removing Duplicate Elements from Arrays
- 基于数组构建字典对象 Creating a dictionary Object from an Array
- 数组转成字符串 Creating a String from an Array
- 检查每一个元素 Testing Every Element
- 数组最大值
- 过滤数组 Filtering Arrays
- 定义区间数组 Define Ranges Array
- 转置数组 Reversing Arrays
- 化简数组 Reducing Arrays
- 使用数组来做值交换 Using Arrays to Swap Variables
- 列表解析 List Comprehensions
- 检查值的类型是否是数组
- 连接数组
- 搅乱数组中的元素 Shuffling Array Elements
- 数组映射 Mapping Arrays
- 5、Dates and Times
- Calculate Phase of the Moon for a Date
- 找出某月的最后一天是几号 Finding the Last Day of the Month
- 获取两个日期相差的天数 Get Days Between Two Dates
- 计算复活节岛日期 Calculate the Date of Easter Sunday
- 计算感恩节的日期(美国和加拿大) Calculate the Date of Thanksgiving (USA and Canada)
- 计算上一个(下一个)月份 Finding Last (or Next) Month
- 6、Math
- 快速逆平方根
- 一个随机整数的函数
- 更快的斐波那契算法
- 生成可预测的随机数
- 弧度与度转换
- 生成随机数
- 数学常数
- 7、Functions
- 反抖动函数 Debounce Functions
- 参数数组化 Splat Arguments
- 当函数调用的括号不可以省略时 When Function Parentheses Are Not Optional
- 递归函数 Recursive Functions
- 8、Metaprogramming
- 扩展内置对象 Extending Built-in Objects
- 检测并创建缺失的函数 Detecting and Creating Missing Functions
- 9、jQuery
- 回调绑定
- 创建jQuery插件
- AJAX
- 10、Ajax
- 不依赖jQuery的Ajax请求 Ajax Request Without jQuery
- 11、Regular Expressions
- 替换子字符串 Replacing Substrings
- 使用定点 Using Heregexes
- 使用HTML字符实体替换HTML标签 Replacing HTML Tags with HTML Named Entities
- 搜索子字符串 Searching for Substrings
- 12、Networking
- 简单的服务器
- 双向客户端
- 最简单的HTTP客户端
- 最简单的HTTP服务器
- 简单的客户端
- 双向服务端 Bi-Directional Server
- 13、Design Patterns
- 命令模式
- 单例模式
- 策略模式 Strategy Pattern
- 建造者模式 Builder Pattern
- 备忘录模式 Memento Pattern
- 解释器模式 Interpreter Pattern
- 装饰者模式
- 桥接模式
- 工厂方法模式
- 14、Databases
- MongoDB
- SQLite
- 15、Testing
- 使用Jasmine测试