2.4 HTTP服务
HTTP模块是Node的核心模块,主要提供了一系列用于网络传输的API,这些API大都位于比较底层的位置,可以让开发者自由地控制整个HTTP传输过程。
在HTTP模块中,Node定义了一些顶级的类、属性以及方法,如下所示:
每个类下面又定义了一些方法和事件,接下来我们会就其中常用的部分加以说明。
2.4.1 创建HTTP服务器
通常使用createServer方法创建HTTP服务器,下面是一个最简单的例子。
代码2.8 创建一个简单的HTTP服务器
上面的代码中,使用createServer方法创建了一个简单的HTTP服务器,该方法返回一个http.server类的实例,createServer方法包含了一个匿名的回调函数,该函数有两个参数req和res,它们是InComingMessage和ServerResponse的实例。分别表示HTTP的request和response对象,服务器创建完成后,Node进程开始循环监听3000端口(由listen方法实现)。
当浏览器访问localhost:3000时,Node返回“Hello Node”字符串。
http.server类定义了一系列的事件,在上面的代码中,HTTP请求会触发connection和request事件,将上面的代码稍微改造。
代码2.9 监听来自客户端的事件
当访问http://localhost:3000/时,控制台输出如下:
程序打印出两个request,代表触发了两次request事件(其中一个是favicon.ico的请求)。
下面的代码是一个简单的静态文件服务器,只支持文本文件,可以通过浏览器来查看服务器端的文件内容。
代码2.10 一个简单的静态文件服务器
2.4.2 处理HTTP请求
如图2-3所示,这是一个标准的HTTP报文格式。
图2-3
1.method,URL和header
当处理HTTP请求时,最先做的事就是获取请求的URL、method等信息。
Node将相关的信息都封装在一个对象(前面代码中的req)中,该对象是IncomingMessage的实例。
以获取method和URL为例:
对于HTTP请求来说,method的值通常是get、post、put、delete、update 5个关键字之一,以get和post最为常见,URL的值为除去网站服务器地址之外的完整值。
例如请求:
那么URL的值即为index.html?name=Lear。
2.header
http header通常为以下的形式:
可以使用Chrome控制台来查看具体信息,以localhost:3000的请求为例,HTTP header形式如下:
Node获取HTTP header信息也很简单,header是一个JSON对象,可以对属性名进行单独索引:
3.request body
Node使用stream来处理HTTP的请求体,这个stream注册了data和end两个事件。
下面的这段代码通常获取完整的HTTP内容体,在Buffer的一节我们已经提到过了。
目前我们还没有提到response对象,该对象是ServerResponse的一个实例,并且实现了一个writableStream,我们接下来会对其进行介绍。
2.4.3 Response对象
1.设置statusCode
状态码的设置在Web开发中常常被忽略,在Node中如果开发者不手动设置,那么状态码的值会默认为200。
但200并不适用所有场景,另一个常用的状态码是404,表示服务器没有对应的资源。
Web开发中如果遇到非法的路径访问,通常会返回一个404 not found的页面,但实际上,即使开发者返回一个200的状态码,也能将对应的页面返回,因此状态码的设置通常是一种最佳实践,而非强制的编码规范。
2.设置response header
通过setHeader方法可以设置response的头部信息。
代码2.11 设置响应头
setHeader方法只能设置response header单个属性的内容,如果想要一次性设置所有的响应头和状态码,可以使用writeHead方法。
response.writeHead
writeHead方法用于定义HTTP相应头,包括状态码等一系列属性,下面的例子我们会同时设置状态码和多个header字段。
调用该方法后,服务器向客户端发送HTTP响应头,后面通常会跟着调用res.write等方法,响应头不可重复发送。
有时开发者并不会显式调用该方法,当调用end方法时也会调用writeHead方法,此时statusCode会自动设置成200。
3.response body
response对象是一个writableStream实例,可以直接调用write方法进行写入,写入完成后,再调用end方法将该stream发送到客户端。
不过这样会显得有些烦琐,也可以直接将response body作为end方法的参数进行返回。
4.response.end
end方法在每个HTTP请求的最后都会被调用,当客户端的请求完成之后,开发者应该调用该方法来结束HTTP请求。通常情况下,如果不调用end方法,用户最直观的感受通常是浏览器(以Chrome为例)位于地址栏左边的叉号会一直存在,表示该请求尚未完成。
同样的,end方法支持一个字符串或者buffer作为参数,可以指定在HTTP请求的最后返回的数据,该数据会在浏览器页面上显示出来;如果定义了回调方法,那么会在end返回后调用。
2.4.4 上传数据
从概念上来说,本节的内容和上一节有重合之处,但数据上传相关的操作比较复杂,因此单独抽出为一节内容进行介绍。
在实际的业务开发中,用户除了接收数据外,往往还有上传数据的需求,例如提交表单、上传文件等。
在上面的代码中我们只处理了头部信息,头部信息之外的内容(body部分)需要开发者自行解析,否则这部分内容就会被Node程序丢弃。
在传统的Web开发中,最常用的HTTP请求只有get和post两种。get请求的报文内容很简单,只有请求行和请求头部;post请求由于要上传数据,因此需要包含请求体的内容,有两个相关的属性经常被用到,分别是content-type和content-length。
对于Node而言,可以通过req.method属性来判断请求方法的类型。
1.提交表单
表单的提交是post请求最常用的情景之一。
上面的HTML代码定义了一个简单的form表单,单击submit按钮后会将整个表单提交到“/login”路径下。
下面是Node的服务端代码:
代码2.12 server端的代码
如果不使用Express之类的Web框架,Node实现的服务器代码通常都是上面这种结构,获取请求的URL之后,再针对不同的HTTP method进行处理,缺点就是要写很多条件控制语句。
当用户在浏览器输入用户名、密码并提交后,浏览器向localhost:3000/login发起post请求,我们可以将头部信息打印出来。
可以看出,如果是以表单形式提交数据,请求头中的content-type为application/x-www(-?)form-urlencoded。
报文主体中的内容是通过数据流的形式来传输的,可以通过监听流事件的方式来获取数据,这一点在buffer一节已经介绍过了,读者可以参考代码2.4。
将表单中的body内容打印出来如下所示:
解析这样的字符串十分容易,读者可以自行实现这样的方法,也可以使用一些第三方模块来实现。
2.使用post上传文件
首先要构造一个用于上传文件的表单。
和只有字段值的表单不同的是,上传文件的表单要设置enctype="multipart/form-data"属性,同样地,文件上传时的header信息也有所不同:
服务器处理上传文件通常基于stream来实现,这里使用的是比较流行的第三方库formidable。formidable模块已经有些年头了,由于社区喜新厌旧的天性,模块版本更新可能不够及时,我们在第5章会进行进一步介绍。
下面是封装的一个处理上传文件的方法。
代码2.13 服务器处理上传文件
在回调方法中的files字段,将其打印出来:
如果想要获取files对象中一些属性,例如name,type的值,可以通过:
来获取,上面表达式的file字段即为form表单的name属性。
可以看出formidable是调用writeStream进行文件写入的,同样的,该模块还支持多个文件同时上传,读者可以自行实现。
2.4.5 HTTP客户端服务
HTTP模块除了能在服务端处理客户端请求之外,还可以作为客户端向服务器发起请求,例如通过http.get发起get请求,通过post方法上传文件等。这也是Node也能做出像electron那样的桌面软件的基础。
http.get的声明如下:
代码2.14 发起一个get请求
上面这段代码向http://blockchain.info/ticker发起了一个get请求。用来获得比特币当前的价格信息,该请求返回的结果如下:
2.4.6 创建代理服务器
代理服务器相当于在客户端和目标服务器之间建立了一个中转,所有的访问和流量都经过这个服务器进行中转,代理服务器在实际中运用十分广泛,例如,如果本地机器不能直接访问目标服务器,那么就在可以连通两端的机器上搭建一个代理服务器,就能通过间接的方式访问目标服务器了。
在本节中,我们会在本地搭建一个简单的代理服务器。
代码2.15 代理服务器的例子
在上面的例子中,我们在本地创建了一个HTTP服务器,请求经由localhost:8080进行转发,请求的URL也要改成形如localhost:8080/google.com的格式,也可以用一些其他配置省略掉开头的localhost:8080。
代理服务器可以有很多应用领域,例如使用它来缓存文件或者,很多企业都会使用代理服务器来过滤掉一些广告和垃圾网站的URL,或者限制员工使用公司网络访问社交网站。有的企业访问npm下载第三方模块也需要配置代理。
一些常用的屏蔽广告的浏览器插件大都也是依靠本地启动代理服务器来实现广告过滤的。
关于反向代理
如果一个代理服务器可以代理外部的访问来访问内部网络时,这种代理方式就被称为反向代理。
CDN就是一个反向代理的例子,如果一个网站购买了CDN服务,那么当有来自外部(客户端)的请求时,并没有直接访问服务器的内容,而是访问距离用户最近的CDN节点。对于服务器来说,CDN就起到了反向代理的功能。