0%

pro-node 之 http & 简单静态服务器

HTTP

HTTP 构建在 TCP 基础之上,以前用 C#的 Socket 实现过简单地只能响应首页的 Server,这次用 net.createServer 来实现,但是 writeHeader 一直不起作用,暂时无果,再探索探索

更新: 使用 TCP socket 实现最最最基本的 web server,不起作用是我把 header 写错了,header 是

1
2
3
4
HTTP/1.1 200 OK\r\n
Content-Type: text/html

xxx // <- 响应内容

注意键值中间是冒号分开,不是等号,键+冒号+空格+值,header 写完加一个空行,不懂什么 http 规范搞的焦头烂额…

响应内容是 request 的 path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var net = require("net");
var fs = require("fs");
var util = require("util");

var server = net.createServer(function(socket) {
var name = util.format("%s:%s", socket.remoteAddress, socket.remotePort);

var buf = new Buffer(0);
socket.on("data", function(data) {
buf = Buffer.concat([buf, data]);
if (!has_header()) return;

var method = buf.toString().split(" ")[0];
var url = buf.toString().split(" ")[1]; // GET /xxx

console.log("%s %s %s...", name, method, url);
socket.removeAllListeners("data");
socket.write("\
HTTP/1.1 200 OK\r\n\
Content-Type: text/plain\r\n\
charset: utf-8\r\n\
\r\n\
");
socket.end(url);
//fs.createReadStream("index.html").pipe(socket);

function has_header() {
var end = "\r\n\r\n"; // 13d 10a
var return_val = false;
[].forEach.call(buf, function(cur, i, arr) {
if (cur === 0xd && buf.binarySlice(i, i + 4) === end) {
return (return_val = true);
has_header.index = i;
}
});
return return_val;
}
});

socket.on("end", function() {
console.log("%s end...", name);
socket.removeAllListeners("data");
//socket.end("hello");
});
});

server.listen(5000, function() {
console.log(server.address());
});

简单地 server,用 http.createServer 就简单多了,端口 1337,是 nodejs 官网首页上的例子

1
2
3
4
5
6
var http = require("http");
var server = http.createServer(function(request, response) {
response.write("Hello World!");
response.end();
});
server.listen(1337);

浏览器访问得到 Hello World

  • server 是 http.Server 实例,传进去的回调是 server 的 request 事件的 listener

创建好 server 之后使用 listen 方法,就与 net.createServer.listen 一样了,参数里面的回调是 listening 事件的 handler,表示成功绑定端口,正在监听

http.IncomingMessage req

request 是 http.IncomingMessage 实例
Doc : http.IncomingMessage api

http.request()的 response 也是 IncomingMessage 类型,这个类型的某些属性只在一种情况有效

请求报文

1
2
GET / HTTP/1.1
Host: localhost:8000

第一行 : 请求方法(GET) 请求路径(/) 请求协议(HTTP)/HTTP 版本(1.1)
下面就是请求 headers,其中 Host 是 HTTP 1.1 强制的,咋能没有了~

request.method

HTTP 1.1 请求方法一览,HTTP 1.0 只支持 GET/POST/HEAD

request.headers

包含请求的 headers,请求可以包含数据,如 post data 上去

request.url

包含所有的东西,像 http/https 啊,host,path,querystring,hash 就是#xxx 那些
可以使用 require(‘url’).parse(url),关心哪个部分直接取就行,pathname 就是 request 的 path,我做静态服务器有用到…

request.socket

它包含的 socket,是net.Socket实例

request.on data

对于客户端 POST 得到的数据

1
2
3
4
5
6
7
8
var buf = new Buffer(0);
req.on("data", function(data) {
Buffer.concat([buf, data]); //一直接受数据,直到end
});
req.on("end", function() {
var s = buf.toString();
//这就是POST 到Server的数据
});

http.ServerResponse res

res 是 http.ServerResponse
Doc : http.ServerResponse api

响应报文

1
2
3
4
HTTP/1.1 200 OK
Date: Sun, 21 Jul 2013 22:14:26 GMT
Connection: keep-alive
Transfer-Encoding: chunked

第一行 : 回应 HTTP 协议&版本 状态码 StatusCode(200) 原因说明 reason-phrase(OK)
后面是 response-headers

res.statusCode 可读可写

表示响应状态码

http.STATUS_CODE 属性表示了状态码与对于的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
> http.STATUS_CODES
{ '100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Moved Temporarily',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'307': 'Temporary Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Time-out',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Request Entity Too Large',
'414': 'Request-URI Too Large',
'415': 'Unsupported Media Type',
'416': 'Requested Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Unordered Collection',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',

响应头 headers

  1. 输出 headerres.writeHead(statusCode, [reasonPhrase], [headers])
  2. 输出/修改 header
    getHeader(name)/setHeader(name,value)/removeHeader(name)
  3. 使用 headerSent 来检查 header 有没有被发送出去

Cookie 数据,通过 Set-Cookie 头来发送 Cookie 到客户端

1
2
3
4
5
response.setHeader("Set-Cookie", [
"name=Colin; Expires=Sat, 10 Jan 2015 20:00:00 GMT;Domain=foo.com; HttpOnly; Secure",
"foo=bar; Max-Age=3600"
]);
//使用string 数组来设置多个cookie

响应

  • res.write(data,[encoding]);
  • res.end([data],[encoding]);

必须显示调用 res.end() 表示服务器对本次响应的输出结束了

data 可以是 string 或 Buffer 数据,encoding 默认是 utf8

中间件 middleware

实现类似 connect 那种形式,将 middleware 换成 connect 就一样了
不到 30 行的代码,可以实现,也就是拦截一下,没有错误处理的部分,太复杂不搞了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var http = require("http");

function middleware() {
var cur;
var middlewares = [];
var req, res;

function real_handler(_req, _res) {
cur = -1; //记住每次重新赋值啊,总是忘~~~
req = _req;
res = _res;
next();
}

real_handler.use = function(middle) {
//app.use
middlewares.push(middle);
};

function next() {
cur++;
if (cur === middlewares.length) return res.end(); //no middleware left

middlewares[cur](req, res, next);
}

return real_handler; //app
}

var app = middleware();
app.use(function(req, res, next) {
res.end(req.url);
});
http.createServer(app).listen(1234);
console.log("端口 1234 !");

像 query 实现的是:将 url 中的 querystring 解析后绑定至res.query字段中

发起请求 http.request

作为 http 客户端发起请求

1
http.request(options, callback);

option 字段表示请求的详细内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var http = require("http");
var request = http.request(
{
hostname: "localhost",
port: 8000,
path: "/",
method: "GET",
headers: {
Host: "localhost:8000"
}
},
function(response) {
var statusCode = response.statusCode;
var headers = response.headers;
var statusLine =
"HTTP/" +
response.httpVersion +
" " +
statusCode +
" " +
http.STATUS_CODES[statusCode];
console.log(statusLine);
for (header in headers) {
console.log(header + ": " + headers[header]);
}
console.log();

response.setEncoding("utf8");
response.on("data", function(data) {
process.stdout.write(data);
});
response.on("end", function() {
console.log("end ...");
});
}
);

request.end();
  • 正如前文所说,这个 response 是 http.IncomingMessage 实例
  • request 必须显式调用 end()表示数据发送结束,例如 post 数据
  • option 可以用一个 string 代替,不过不能表示 request 的方法,默认为 get,就和 http.get 一样了

使用 POST 数据

  1. 准备 post 数据,var body = "name=zhangsan&age=18";
  2. request 并设置 headers
1
2
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": Buffer.byteLength(body)
  1. request.end(body);

request 第三方模块

简化 http.request 操作…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var request = require("request");
request(
{
uri: "http://localhost:8000/",
method: "POST",
headers: {
Host: "localhost:8000"
},
form: {
foo: "bar",
baz: [1, 2]
}
},
function(error, response, body) {
console.log(body);
}
);

将要 POST 的数据,直接写在 form 字段里,不需要手动 write/end,支持的字段如下:

request 对 Cookie 的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
var request = require("request");
var jar = request.jar();
var cookie = request.cookie("count=1");
jar.add(cookie);
request(
{
url: "http://localhost:8000/",
jar: jar
},
function(error, response, body) {
console.log(jar);
}
);

创建了一个”罐子”jar 来装 cookie,server setcookie 也会帮我们更新

SServer 实现简单静态服务器

了解 http 之后,可以实现一个小的 server 玩玩,使用fs.createReadStream.pipe(res)做法,无缓存,静态服务器,实测比 Express 使用 static 还是快点的,毕竟没那么多逻辑,用来浏览 hexo 生成的静态页,也很快…

支持

  • html/htm
  • js/json/xml
  • css
  • jpg/jpeg/png/bmp/gif 图片
  • 其他全视为 text/plain

没有使用什么 mime type 库,小工具不应该有什么依赖的才对嘛 *^_^*

使用

1
2
3
4
5
6
Usage :
node SServer [option]

h|help 帮助信息
-p|-port 指定端口 默认5000
-r|-root 指定根目录 默认当前目录

实现 SServer.js (static server)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
var http = require("http");
var fs = require("fs");
var util = require("util");
var pathFn = require("path");
var parse = require("url").parse;

var arg = parseArgv();
var PORT = arg.port || 5000;
var ROOT = arg.root || process.cwd();
ROOT = pathFn.resolve(ROOT);

if (arg.help) {
console.log(
"\
Usage :\n\
node SServer [option]\n\
\n\
h|help 帮助信息\n\
-p|-port 指定端口 默认5000\n\
-r|-root 指定根目录 默认当前目录\n\
\n"
);
return;
}

http
.createServer(function(req, res) {
//request event
req.url = decodeURI(req.url); //中文解码
getFile(req, res, function(file) {
fs.exists(file, function(exists) {
if (exists) {
var content_type = getContentType(file);
res.writeHead(200, {
"Content-Type": content_type,
"Powerd-By": "SServer"
});
fs.createReadStream(file).pipe(res);
} else {
console.error("%s 请求失败,找不到 %s\n", req.url, file);
res.writeHead(404);
res.end(util.format(getNotFound(), req.url));
}
});
});
})
.listen(PORT, function() {
//listening event
process.title = "静态服务器SServer@localhost:" + PORT;
console.log(
"静态服务器SServer : \n 根目录 : %s \n访问地址 : http://localhost:%d\n",
pathFn.resolve(ROOT),
PORT
);
require("child_process").exec("start http://localhost:" + PORT);
});

function getFile(req, res, callback) {
var url = decodeURI(parse(req.url).pathname); //本来已经decode过了,但是parse里面会把空格变成 %20

if (url.slice(-1) === "/") {
// /abc/
fs.exists(ROOT + url + "index.html", function(exists) {
if (exists) {
callback(ROOT + url + "index.html");
} else {
callback(ROOT + url + "index.htm");
}
});
} else if (url.slice(url.lastIndexOf("/")).indexOf(".") === -1) {
// /abc => redirect 至 /abc/
// 否则在 /abc/index.html 中的 def.html链接
// 本来指向/abc/def/html
// 浏览器认成/def.html
res.writeHead(301, {
Location: url + "/"
});
res.end();
} else {
//普通的文件
callback(ROOT + url);
}
}

function getContentType(file) {
var ext = pathFn.extname(file).slice(1);
switch (ext) {
case "html":
case "htm":
return "text/html";
case "js":
return "application/javascript";
case "json":
return "application/json";
case "xml":
return "application/xml";
case "css":
return "text/css";

case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "bmp":
return "image/bitmap";
default:
return "text/plain";
}
}

function getNotFound() {
return "\
<html>\
<head>\
<title>Page Not Found</title>\
<style>\
*{\
font-family : 'YaHei Consolas Hybrid',微软雅黑,'Microsoft YaHei','Microsoft YaHei UI';\
}\
</style>\
</head>\
<body>\
<div style='font-size:100px;margin:100px 0 40px 100px;'>失败!</div>\
<h1 style='margin:20px 0 0 100px;'>找不到 %s</h1>\
<h3 style='margin:20px 0 0 100px;'>Powerd-By SServer(Static Server) 2014</h3>\
</body>\
</html>\
";
}

function parseArgv() {
var result = {};
var args = process.argv.slice(2);
if (!args) return result;

for (var i = 0; i < args.length; i++) {
var arg = args[i];

if (arg == "help" || arg == "h") {
result.help = true;
} else if (arg == "-port" || arg == "-p") {
result.port = parseInt(args[++i]);
continue;
} else if (arg == "-r" || arg == "-root") {
result.root = args[++i];
continue;
}
}

return result;
}