要开发 HTTP 服务器程序,从头处理 TCP 连接,解析 HTTP 是不现实的。这些工作实际上已经由 Node.js 自带的 http 模块完成了。应用程序并不直接和 HTTP 协议打交道,而是操作 http 模块提供的 requestresponse 对象。

1.1. request & response

  • request: 封装了 HTTP 请求,我们调用 request 对象的属性和方法就可以拿到所有 HTTP 请求的信息;
  • response: 封装了 HTTP 响应,我们操作 response 对象的方法,就可以把 HTTP 响应返回给浏览器。

下面是一个简单的服务器:

'use strict';
// 导入http模块:
var http = require('http');
// 创建http server,并传入回调函数:
var server = http.createServer(function (request, response) {
    // 回调函数接收request和response对象,
    // 获得HTTP请求的method和url:
    console.log(request.method + ': ' + request.url);
    // 将HTTP响应200写入response, 同时设置Content-Type: text/html:
    response.writeHead(200, {'Content-Type': 'text/html'});
    // 将HTTP响应的HTML内容写入response:
    response.end('<h1>Hello world!</h1>');
});

// 让服务器监听8080端口:
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');

在命令提示符下运行该程序,可以看到以下输出:

$ node web.js 
Server is running at http://127.0.0.1:8080/

1.2. 文件服务器

让我们继续扩展上面的 Web 程序。我们可以设定一个目录,然后让 Web 程序变成一个文件服务器。要实现这一点,我们只需要解析 request.url 中的路径,然后在本地找到对应的文件,把文件内容发送出去就可以了。

解析 URL 需要用到 Node.js 提供的 url 模块,它使用起来非常简单,通过 parse() 将一个字符串解析为一个 Url 对象:

'use strict';
var url = require('url');
var requestUrl = 'http://user:pass@host.com:8080/path/to/file?query=string#hash';
var urlObj = url.parse(requestUrl);

使用 url.parse() 解析获取到的 urlObj 对象包含如下内容:

  • protocol: 'http:',
  • slashes: true,
  • auth: 'user:pass',
  • host: 'host.com:8080',
  • port: '8080',
  • hostname: 'host.com',
  • hash: '#hash',
  • search: '?query=string',
  • query: 'query=string',
  • pathname: '/path/to/file',
  • path: '/path/to/file?query=string',
  • href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash'

处理本地文件目录需要使用 Node.js 提供的 path 模块,它可以方便地构造目录:

'use strict';
var path = require('path');
// 解析当前目录:
var workDir = path.resolve('.'); // '/Users/a284628487'
// 组合完整的文件路径:当前目录 + 'pub' + 'index.html':
var filePath = path.join(workDir, 'pub', 'index.html');
// '/Users/a284628487/pub/index.html'

使用 path 模块可以正确处理操作系统相关的文件路径。在 Windows 系统下,返回的路径类似于 C:\Users\a284628487\static\index.html,这样,我们就不关心怎么拼接路径了。

最后,我们实现一个文件服务器 file_server.js

'use strict';

var
    fs = require('fs'),
    url = require('url'),
    path = require('path'),
    http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');

console.log('Static root dir: ' + root);

// 创建服务器:
var server = http.createServer(function (request, response) {
    // 获得URL的path,类似 '/css/bootstrap.css':
    var pathname = url.parse(request.url).pathname;
    // 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
    var filepath = path.join(root, pathname);
    // 获取文件状态:
    fs.stat(filepath, function (err, stats) {
        if (!err && stats.isFile()) {
            // 没有出错并且文件存在:
            console.log('200 ' + request.url);
            // 发送200响应:
            response.writeHead(200);
            // 将文件流导向response:
            fs.createReadStream(filepath).pipe(response);
        } else {
            // 出错了或者文件不存在:
            console.log('404 ' + request.url);
            // 发送404响应:
            response.writeHead(404);
            response.end('404 Not Found');
        }
    });
});

server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');

注:没有必要手动读取文件内容。由于 response 对象本身是一个 Writable Stream,直接用 pipe() 方法就实现了自动读取文件内容并输出到 HTTP 响应。

在命令行运行node file_server.js /path/to/dir,把 /path/to/dir 改成本地的一个有效的目录,然后在浏览器中输入http://localhost:8080/index.html

只要当前目录下存在文件 index.html ,服务器就可以把文件内容发送给浏览器。观察控制台输出:

200 /index.html
200 /css/uikit.min.css
200 /js/jquery.min.js
200 /fonts/fontawesome-webfont.woff2

第一个请求是浏览器请求 index.html 页面,后续请求是浏览器解析 HTML 后发送的其它资源请求。

1.3. GET & POST

  • 获取 GET 请求内容

由于 GET 请求直接被嵌入在路径中,URL 是完整的请求路径,包括了 ? 后面的部分,因此可以手动解析后面的内容作为 GET 请求的参数。 url 模块中的 parse() 函数提供了这个功能。

var http = require('http');
var url = require('url');
var util = require('util');

http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end(util.inspect(url.parse(req.url, true)));
}).listen(3000);

在浏览器中访问http://localhost:3000/user?name=w3c&email=w3c@w3cschool.cc然后查看返回结果。

  • 获取 POST 请求内容

POST 请求的内容全部的都在请求体中,http.ServerRequest 并没有一个属性内容为请求体,原因是等待请求体传输可能是一件耗时的工作。比如上传文件,而很多时候我们可能并不需要理会请求体的内容,恶意的 POST 请求会大大消耗服务器的资源,所有 node.js 默认是不会解析请求体的,当你需要的时候,需要手动来做。

var http = require('http');
var querystring = require('querystring');
var util = require('util');

http.createServer(function(req, res){
    var post = ''; //定义了一个post变量,用于暂存请求体的信息

    req.on('data', function(chunk){ // 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
        post += chunk;
    });

    req.on('end', function(){ // 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
        post = querystring.parse(post);
        res.end(util.inspect(post));
    });
}).listen(3000);

1.4. Request

Request表示请求对象,它携带了客户端传递过来的信息。

1.4.1. headers

{ host: '127.0.0.1:8081',
  connection: 'keep-alive',
  'cache-control': 'max-age=0',
  'upgrade-insecure-requests': '1',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  'accept-encoding': 'gzip, deflate, sdch',
  'accept-language': 'zh-CN,zh;q=0.8,en;q=0.6',
  cookie: '_ga=GA1.1.988884254.1470363885; uid=1991; uname=ccf',
  'if-none-match': 'W/"2-156367aed28"',
  'if-modified-since': 'Fri, 29 Jul 2016 11:47:21 GMT' 
}

1.4.2. rawHeaders

[ 'Host',
  '127.0.0.1:8081',
  'Connection',
  'keep-alive',
  'Cache-Control',
  'max-age=0',
  'Upgrade-Insecure-Requests',
  '1',
  'User-Agent',
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
  'Accept',
  'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  'Accept-Encoding',
  'gzip, deflate, sdch',
  'Accept-Language',
  'zh-CN,zh;q=0.8,en;q=0.6',
  'Cookie',
  '_ga=GA1.1.988884254.1470363885; uid=1991; uname=ccf',
  'If-None-Match',
  'W/"2-156367aed28"',
  'If-Modified-Since',
  'Fri, 29 Jul 2016 11:47:21 GMT'
]
var cookie = req.headers.cookie;
// or
var cookie = req.headers['cookie'];

1.5. response

可以使用 response 对象的 setHeaderwriteHead 写入响应头信息。

设置响应头信息,可以通过writeHead和setHeader方法。

res.writeHead(200, {'Content-Type': 'text/plain'});
res.setHeader('Content-Type', 'text/plain');

1.5.2. write

write方法向客户端发送响应数据。

// 从文件系统中读取请求的文件内容
fs.readFile(pathname.substr(1), function (err, data) {
   if (err) {
      console.log(err);
      // HTTP 状态码: 404 : NOT FOUND
      // Content Type: text/plain
      response.writeHead(404, {'Content-Type': 'text/html'});
   } else {             
      // HTTP 状态码: 200 : OK
      // Content Type: text/plain
      response.writeHead(200, {'Content-Type': 'text/html'});
      // 响应文件内容
      response.write(data.toString());    
   }
   // 发送响应数据
   response.end();
});
resp.setHeader('Set-Cookie', "age=26");
resp.setHeader('Set-Cookie', ['a=000', 't=1111', 'w=2222']);

1.6. Client

Web 应用架构,目前最主流的三个 Web 服务器是ApacheNginxIIS

  • Client 客户端,一般指浏览器,浏览器可以通过 HTTP 协议向服务器请求数据。
  • Server 服务端,一般指 Web 服务器,可以接收客户端请求,并向客户端发送响应数据。
  • Business 业务层, 通过 Web 服务器处理应用程序,如与数据库交互,逻辑运算,调用外部程序等。
  • Data 数据层,一般由数据库组成。

使用 http 发起请求:

var http = require('http');

// 用于请求的选项
var options = {
   host: 'localhost',
   port: '8081',
   path: '/index.htm'  
};

// 处理响应的回调函数
var callback = function(response){
   // 不断更新数据
   var body = '';
   response.on('data', function(data) {
      body += data;
   });

   response.on('end', function() {
      // 数据接收完成
      console.log(body);
   });
}
// 向服务端发送请求
var req = http.request(options, callback);
req.end();

1.7. 端口

端口的作用:通过端口来区分出同一电脑内不同应用或者进程,从而实现一条物理网线(通过分组交换技术-比如internet)同时链接多个程序。端口号是一个 16位的uint, 所以其范围为 1 to 65535 (对TCP来说, port 0 被保留,不能被使用. 对于UDP来说, source端的端口号是可选的,为0时表示无端口). app.listen(3000),进程就被打标,电脑接收到的3000端口的网络消息就会被发送给我们启动的这个进程;

results matching ""

    No results matching ""