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
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)
  3. request.end(body);

##request 第三方模块
简化http.request操作…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
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;
}