先复习一下HTTP~
HTTP
超文本传输协议,又称为HTTP,是一种web协议,属于TCP上层的协议。
HTTP协议构造在请求和响应的概念上,对应在Node.js中就是由http.ServerRequest和http.ServerResponse这两个构造器构造出来的对象。
头信息
HTTP协议和IRC一样流行, 其目的是为了进行文档交换。它在请求和响应消息前使用头信息(header)来描述不同的消息内容。
web页面会分发不同类型的内容:文本(Text)、HTML、XML、JSON、PNG以及JPEG图片,等等。
发送内容的类型(type)就是在著名的Content-Type头信息中标注的。
一个简单的实践:
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200);
res.end('Hello <b>World</b>');
}).listen(3000);
在终端通过telnet连接:
在浏览器中:
为什么浏览器没有正确解析文本呢?这是由于HTTP客户端(浏览器)并不知道服务器发过来的内容是什么类型,我们需要把这部分信息告诉浏览器。否则,浏览器以为它看到的内容是纯文本即text/plain,就不会将它作为HTML来渲染了。
解决方案——
加入正确头信息:
res.writeHead(200, {'Content-Type': 'text/html'});
浏览器中HTTP头部请求信息:
从上图中我们也可以看到,我们尽管只用writeHead API指定了一个头信息,但是,Node还是把另外两个头信息:Connection和Transfer-Encoding。
Transfer-Encoding的默认值是chunked,主要原因是Node天生的异步机制,这样响应就可以逐步发生。
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('Hello');
setTimeout(function() {
res.end('world');
},500);
}).listen(3000);
像上面的代码一样,在调用end前,我们可以多次调用write方法来发送数据。为了尽可能快地响应客户端,在首次调用write时,Node就能把所有的响应头信息以及第一块数据(Hello)发送出去。
随后,在执行setTimeot回调函数时,又写入另外一块数据。由于这次是使用end而不是write方法,因此,Node会结束响应,并不再允许往这次响应中发送数据了。
发送数据块的方式在设计文件系统的情况下会非常高效。Web服务器对硬盘是那个的文件进行托管服务是很常见的。因此Node允许以数据块的形式往响应中写数据,同时它又允许以数据块的形式读取文件,所以我们可以使用ReadStream文件系统API来实现。
下面这个例子用于读取image.png
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200,{'Content-Type': 'image/png'});
var stream = require('fs').createReadStream('image.png');
stream.on('data',function(data){
res.write(data);
});
stream.on('end',function(){
res.end();
});
}).listen(3000);
以数据块的形式将图片写到响应中,会有如下好处:
- 高效的内存分配。要是对每个请求在写入前都完全把图片信息读取完(通过fs.readFile),在处理大量请求时会消耗大量内存。
- 数据一旦就绪就可以立刻写入了。
上面的代码可以改为流的对接形式(把一个流接到另一个流上)
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200,{'Content-Type': 'image/png'});
require('fs').createReadStream('image.png').pipe(res);
}).listen(3000);
TCP服务器和HTTP服务器有相似的实现,但是它们有个本质的区别,即回调函数中对象的类型。在net服务器中,是个连接(connection)对象,而在HTTP服务器中,则是请求和响应对象。原因在于:
一、HTTP服务器是更高层的API,提供了控制和HTTP协议相关的一些功能。
Node在拿到浏览器发送的数据(代理的相关信息、是否缓存等信息)后,对其进行分析,然后构造一个JavaScript对象方便我们在脚本中使用。它甚至还将所有的信息都变成了小写,避免混淆记忆。
二、浏览器在访问站点时不会就只用一个连接。很多主流的浏览器为了更快地加载网站内容,能向一个主机打开八个不同的连接,并发送请求。
我们可以使用req.connection属性获得TCP连接对象,但是绝大多数我们还是和请求和响应的抽象打交道。
默认情况下,Node会告诉浏览器始终保持连接,通过它来发送更多的请求。
一个简单的Web服务器
定义需求
简单的表单提交和响应
细分步骤
- 创建模块
- 输出表单
- method和URL
- 数据
- 整合
- 让程序更健壮
创建模块
// package.json
{
"name": "http-form",
"description": "An HTTP server that processes forms",
"version": "0.0.1"
}
输出表单
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200,{'Content-Type': 'text/html'});
res.end([
'<form method="POST" action="/url">',
'<h1>My form</h1>',
'<fieldset>',
'<label>Personnal Information</label>',
'<p>What is your name?</p>',
'<input type="input" name="name">',
'<p><button>Submit</button></p>',
' </fieldset>',
'</form>'
].join(''));
}).listen(3000);
为了让HTML结构更加清楚,把响应文本写在一个数组中是比较直观的!
method和URL
// index.js
var http = require('http');
http.createServer(function(req,res){
if (req.url == '/') {
res.writeHead(200,{'Content-Type': 'text/html'});
res.end([
'<form method="POST" action="/url">',
'<h1>My form</h1>',
'<fieldset>',
'<label>Personnal Information</label>',
'<p>What is your name?</p>',
'<input type="input" name="name">',
'<p><button>Submit</button></p>',
' </fieldset>',
'</form>'
].join(''));
} else {
res.writeHead(200,{'Content-Type': 'text/html'});
res.end('You send a <em>' + req.method + ' </em> request');
}
}).listen(3000);
当浏览器提交表单之后,method是POST。
而直接在浏览器地址栏输入相同的url,显示method是GET。
Node会将主机名后的所有内容都放在url属性中
数据
我们需要告诉服务器我们将要请求信息的格式。添加:
// index.js
if (req.url == '/') {
res.writeHead(200,{'Content-Type': 'text/html'});
res.end([
'<form method="POST" action="/url">',
'<h1>My form</h1>',
'<fieldset>',
'<label>Personnal Information</label>',
'<p>What is your name?</p>',
'<input type="input" name="name">',
'<p><button>Submit</button></p>',
' </fieldset>',
'</form>'
].join(''));
} else if(req.url == '/url' && req.method == 'POST') {
var body = '';
req.on('data', function(chunk) {
body += chunk;
});
req.on('end', function() {
res.writeHead(200,{'Content-Type': 'text/html'});
res.end('<p>Content-Type: ' + req.headers['content-type'] + '</p>' + '<p>Data: </p><pre>' + body + '</pre>');
});
}
通过监听data和end事件。创建一个nody字符串来接收数据块,仅当end事件触发时,我们就知道数据接收完全了。
之所以这样逐块接收数据,是因为Nodejs允许在数据到达服务器时就可以对其进行处理。因为数据是以不同TCP包到达服务器的,这种情况也和现实情况完全匹配。
提交表单:
Node.js提供了一个称为querystring的模块,可以方便地对查询字符串进行解析。
require('querystring').parse('name=GUI')
querystring模块将一个字符串解析成一个对象。
整合
var http = require('http');
var qs = require('querystring');
http.createServer(function(req,res){
if (req.url == '/') {
res.writeHead(200,{'Content-Type': 'text/html'});
res.end([
'<form method="POST" action="/url">',
'<h1>My form</h1>',
'<fieldset>',
'<label>Personnal Information</label>',
'<p>What is your name?</p>',
'<input type="input" name="name">',
'<p><button>Submit</button></p>',
' </fieldset>',
'</form>'
].join(''));
} else if(req.url == '/url' && req.method == 'POST') {
var body = '';
req.on('data', function(chunk) {
body += chunk;
});
req.on('end', function() {
res.writeHead(200,{'Content-Type': 'text/html'});
res.end('<p>Your name is <b>' + qs.parse(body).name + '</b></p>'); // 补充添加
});
}
}).listen(3000);
让程序更健壮
如果URL没有匹配到任何判断条件,则服务器一直都没有响应,浏览器将一直处在挂起的状态。
为解决这个问题,我们可以在服务器不知道如何处理请求的情况时,发送404状态码给客户端。
// ...
if (req.url == '/') {
// ...
} else if(req.url == '/url' && req.method == 'POST') {
// ...
} else {
res.writeHead(404);
res.end('Not Found');
}
// ...