# 12網絡
### 客戶端
### 問題
你想使用網絡上提供的服務。
### 解決方案
創建一個基本的 TCP 客戶機。
#### 在 Node.js 中
~~~
net = require 'net'
?
domain = 'localhost'
port = 9001
?
connection = net.createConnection port, domain
?
connection.on 'connect', () ->
console.log "Opened connection to #{domain}:#{port}."
?
connection.on 'data', (data) ->
console.log "Received: #{data}"
connection.end()
~~~
### 使用示例
可訪問 [Basic Server](http://coffeescript-cookbook.github.io/chapters/networking/basic-server) :
~~~
$ coffee basic-client.coffee
Opened connection to localhost:9001
Received: Hello, World!
~~~
### 討論
最重要的工作發生在 *connection.on 'data'* 處理過程中,客戶端接收到來自服務器的響應并最有可能安排對它的應答。
另請參閱 [Basic Server](http://coffeescript-cookbook.github.io/chapters/networking/basic-server),[Bi-Directional Client](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-client) 和 [Bi-Directional Server](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-server) 。
### 練習
- 根據命令行參數或配置文件為選定的目標域和端口添加支持。
### HTTP 客戶端
### 問題
你想創建一個 HTTP 客戶端。
### 解決方案
在這個方法中,我們將使用 [node.js's](http://nodejs.org/) HTTP 庫。我們將從一個簡單的客戶端 GET 請求示例返回計算機的外部 IP 。
#### 關于 GET
~~~
http = require 'http'
?
http.get { host: 'www.google.com' }, (res) ->
console.log res.statusCode
~~~
get 函數,從 node.js's http 模塊,發出一個 GET 請求到一個 http 服務器。響應是以回調的形式,我們可以在一個函數中處理。這個例子僅僅輸出響應狀態代碼。檢查一下:
~~~
$ coffee http-client.coffee
200
~~~
#### 我的 IP 是什么?
如果你是在一個類似局域網的依賴于 NAT 的網絡中,你可能會面臨找出外部 IP 地址的問題。讓我們為這個問題寫一個小的 coffeescript 。
~~~
http = require 'http'
?
http.get { host: 'checkip.dyndns.org' }, (res) ->
data = ''
res.on 'data', (chunk) ->
data += chunk.toString()
res.on 'end', () ->
console.log data.match(/([0-9]+\.){3}[0-9]+/)[0]
~~~
我們可以從監聽 'data' 事件的結果對象中得到數據,知道它結束了一次 'end' 的觸發事件。當這種情況發生時,我們可以做一個簡單的正則表達式來匹配我們提取的 IP 地址。試一試:
~~~
$ coffee http-client.coffee
123.123.123.123
~~~
### 討論
請注意 http.get 是 http.request 的快捷方式。后者允許您使用不同的方法發出 HTTP 請求,如 POST 或 PUT。
在這個問題上的 API 和整體信息,檢查 node.js's [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) 可能派上用場。
### 練習
- 為鍵值存儲 HTTP 服務器創建一個客戶端,使用基本的 HTTP 服務器方法。
### 基本的 HTTP 服務器
### 問題
你想在網絡上創建一個 HTTP 服務器。在這個方法中,我們將逐步從最小的服務器成為一個功能鍵值存儲。
### 解決方案
我們將使用 [node.js](http://nodejs.org/) HTTP 庫并在 Coffeescript 中創建最簡單的 web 服務器。
#### 開始 '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 方法 請求檢索的元素中設法想出一個簡單的鍵值存儲。并提供一個關鍵路徑,服務器將請求返回相應的值,如果不存在則返回 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
~~~
我們可以試試幾種 url,看看它們如何回應:
~~~
$ 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
~~~
#### 使用你的頭文件
text/plain 是站不住腳的。如果我們使用 application/json 或 text/xml 會怎么樣?同時,我們的存儲檢索過程也可以用一點重構——一些異常的拋出 & 處理怎么樣? 來看看我們能想出什么:
~~~
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
~~~
這個服務器仍然會返回一個匹配給定鍵的值,如果不存在則返回 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
~~~
### 你需要有所返回
我們的最后一步是提供客戶端存儲數據的能力。我們將通過監聽 POST 請求來保持 RESTiness。
~~~
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 請求中注意數據是如何接收的。通過在“數據”和“結束”請求對象的事件中附上一些處理程序,我們最終能夠從客戶端緩沖和保存數據。
~~~
$ 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"}
~~~
### 討論
給 http.createServer 一個函數 (request,response) - >…… 它將返回一個服務器對象,我們可以用它來監聽一個端口。讓服務器與 request 和 response 對象交互。使用 server.listen 8000 監聽端口 8000。
在這個問題上的 API 和整體信息,參考 node.js [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) 可能派上用場。
### 練習
在服務器和開發人員之間創建一個層,允許開發人員做類似的事情:
~~~
server = layer.createServer
'GET /': (req, res) ->
...
'GET /page': (req, res) ->
...
'PUT /image': (req, res) ->
...
~~~
### 服務器
### 問題
你想在網絡上提供一個服務器。
### 解決方案
創建一個基本的 TCP 服務器。
### 在 Node.js 中
~~~
net = require 'net'
?
domain = 'localhost'
port = 9001
?
server = net.createServer (socket) ->
console.log "Received connection from #{socket.remoteAddress}"
socket.write "Hello, World!\n"
socket.end()
?
console.log "Listening to #{domain}:#{port}"
server.listen port, domain
~~~
### 使用示例
可訪問 [Basic Client](http://coffeescript-cookbook.github.io/chapters/networking/basic-client):
~~~
$ coffee basic-server.coffee
Listening to localhost:9001
Received connection from 127.0.0.1
Received connection from 127.0.0.1
[...]
~~~
### 討論
函數將為每個客戶端新連接的新插口傳遞給 @net.createServer@ 。基本的服務器與訪客只進行簡單地交互,但是復雜的服務器會將插口連上一個專用的處理程序,然后返回等待下一個用戶的任務。
另請參閱 [Basic Client](http://coffeescript-cookbook.github.io/chapters/networking/basic-client),[Bi-Directional Server](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-client) 和 [Bi-Directional Client](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-client)。
### 練習
- 為選定的目標域和基于命令行參數或配置文件的端口添加支持。
### 雙向客戶端
### 問題
你想通過網絡提供持續的服務,與客戶保持持續的聯系。
### 解決方案
創建一個雙向 TCP 客戶機。
### 在 Node.js 中
~~~
net = require 'net'
?
domain = 'localhost'
port = 9001
?
ping = (socket, delay) ->
console.log "Pinging server"
socket.write "Ping"
nextPing = -> ping(socket, delay)
setTimeout nextPing, delay
?
connection = net.createConnection port, domain
?
connection.on 'connect', () ->
console.log "Opened connection to #{domain}:#{port}"
ping connection, 2000
?
connection.on 'data', (data) ->
console.log "Received: #{data}"
?
connection.on 'end', (data) ->
console.log "Connection closed"
process.exit()
~~~
### 使用示例
可訪問 [Bi-Directional Server](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-server):
~~~
$ coffee bi-directional-client.coffee
Opened connection to localhost:9001
Pinging server
Received: You have 0 peers on this server
Pinging server
Received: You have 0 peers on this server
Pinging server
Received: You have 1 peer on this server
[...]
Connection closed
~~~
### 討論
這個特殊示例發起與服務器聯系并在 @connection.on 'connect'@ 處理程序中開啟對話。大量的工作在一個真正的用戶中,然而 @connection.on 'data'@ 處理來自服務器的輸出。@ping@ 函數遞歸是為了說明連續與服務器通信可能被真實的用戶移除。
另請參閱 [Bi-Directional Server](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-server),[Basic Client](http://coffeescript-cookbook.github.io/chapters/networking/basic-client) 和 [Basic Server](http://coffeescript-cookbook.github.io/chapters/networking/basic-server)。
### 練習
- 為選定的目標域和基于命令行參數或配置文件的端口添加支持。
### 雙向服務器
### 問題
你想通過網絡提供持續的服務,與客戶保持持續的聯系。
### 解決方案
創建一個雙向 TCP 服務器。
### 在 Node.js 中
~~~
net = require 'net'
?
domain = 'localhost'
port = 9001
?
server = net.createServer (socket) ->
console.log "New connection from #{socket.remoteAddress}"
?
socket.on 'data', (data) ->
console.log "#{socket.remoteAddress} sent: #{data}"
others = server.connections - 1
socket.write "You have #{others} #{others == 1 and "peer" or "peers"} on this server"
?
console.log "Listening to #{domain}:#{port}"
server.listen port, domain
~~~
### 使用示例
可訪問 [Bi-Directional Client](http://coffeescript-cookbook.github.io/chapters/networking/bi-directional-client):
~~~
$ coffee bi-directional-server.coffee
Listening to localhost:9001
New connection from 127.0.0.1
127.0.0.1 sent: Ping
127.0.0.1 sent: Ping
127.0.0.1 sent: Ping
[...]
~~~
### 討論
大部分工作在 @socket.on 'data'@ 中 ,處理所有的輸入端。真正的服務器可能會將數據傳給另一個函數處理并生成任何響應以便源程序處理。
### 練習
- 為選定的目標域和基于命令行參數或配置文件的端口添加支持。