Node -- 网络编程

终于,到了重头戏!!!加油啊~~~

Node是一个面向网络而生的平台,它具有事件驱动、无阻塞、单线程等特性。

利用Node可以十分方便的搭建网络服务器。在web领域,大多数的编程语言需要专门的Web服务器作为容器,但是对于Node而言,只需要几行代码就可以构建服务器,无需额外的容器。

构建TCP服务

TCP

TCP全名为传输控制协议,在OSI模型中属于传输层协议。许多应用层协议都是基于TCP构建,例如HTTP,SMTP,IMAP等。

这里写图片描述

TCP是面向连接的协议,其显著特征是在传输之前需要3次握手。只有会话形成之后,服务器端和客户端之间才能互相发送数据。在创建会话的过程中,服务器端和客户端分别提供了一个套接字,这两个套接字共同形成一个连接。服务器端和客户端则通过套接字实现两者之间连接的操作。

创建TCP服务器端

创建一个TCP服务器:

var net = require('net');
var server = net.createServer(function (socket) {
    socket.on('data', function (data) {
        socket.write("你好"); 
    });
    socket.on('end', function () { 
        console.log('连接断开');
    });
    socket.write("HelloWorld:\n"); 
});
server.listen(8124, function () {
    console.log('server bound');
});

通过代码net.createServer(listener) 即可创建一个TCP服务器,listener是连接事件connection的侦听器。

通过net模块自行构造客户端进行会话,测试上面创建的TCP服务的代码:

var net = require('net');
var client = net.connect({port: 8124}, function () { //'connect' listener
    console.log('client connected');
    client.write('world!\r\n'); 
});
client.on('data', function (data) { 
    console.log(data.toString()); 
    client.end();
});
client.on('end', function () {  
    console.log('client disconnected');
});

TCP服务的事件

在上述的示例中,分为服务器事件和连接事件。

服务器事件

net.createServer()创建服务器,它是一个EventEmitter实例,它的事件有如下几种:

1、listening:调用server.listen()绑定端口:server.listen(port,listeningListener)

2、connection:每个客户端套接字连接到服务器端时触发,简洁写法为:net.create- Server(),通过最后一个参数传递。

3、close:当服务器关闭时触发,在调用server.close()后服务器将停止接收新的套接字连接,但会保持当前存在的连接。等待所有连接都断开后,会触发该事件。

4、error:当服务器发生异常时,将会触发该事件。如果不侦听error事件,服务器将会抛出异常。

连接事件

服务器可以和多个客户端同时保持连接,对于每个连接而言时典型的可写可读stream对象。stream对象可以用于服务器和客户端之间的通信。它有如下自定义事件:

1、data:当一端调用write发送数据时,另一端会触发data事件,事件传递的数据即为write发送的数据。

2、end:当连接中的任意一端发送了FIN数据时,将会触发该事件。

3、connect:该事件用于客户端,当套接字与服务器端连接成功时就会被触发。

4、drain:当任意一端调用write发送数据时,当前这端将会触发该事件。

5、error:异常发生时触发该事件。

6、close:当套接字完全关闭时,触发该事件。

7、timeout:当一定时间后连接不活跃时,该事件被触发,通知用户当前连接已经被闲置了。

值得注意的是,TCP针对网络中的小数据包有一定的优化策略:Nagle算法。

如果每次只发送一个字节的内容而不优化,网络中将充满只有极少数有效数据的数据包,将十分浪费网络资源。Nagle算法针对这种情况,要求缓冲区的数据达到一定数量或者一定时间后才发出,所以小的数据包将会被Nagle算法合并,以此来优化网络。

如果要禁止Nagle算法,可以调用方法:socket.setNoDelay(true)
这样,通过write方法发送的数据将会立即进入网络。

构建UDP服务

UDP又称用户数据包协议,也属于网络传输层。UDP不是面向连接的,一个套接字可以和多个UDP服务通信。DNS服务是基于UDP实现的。

创建UDP套接字

var dgram = require('dgram');
var socket = dgram.createSocket("udp4");

UDP套接字一旦创建,既可以作为客户端发送数据,也可以作为服务器端接收数据。

创建UDP服务器端

若需要UDP套接字接收网络消息,只要调用dgram.bind(port, [address]) 方法对网卡和端口进行绑定即可。

var dgram = require("dgram");

var server = dgram.createSocket("udp4");

server.on("message", function (msg, rinfo) { 
    console.log("server got: " + msg + " from " +
        rinfo.address + ":" + rinfo.port); 
});
server.on("listening", function () { 
    var address = server.address();
    console.log("server listening " +
     address.address + ":" + address.port);
});  
server.bind(41234);

该套接字将接收所有网卡上41234端口上的消息。绑定完成后将会触发listening事件。

创建UDP客户端

var dgram = require('dgram');
var message = new Buffer("Node.js");
var client = dgram.createSocket("udp4");
client.send(message, 0, message.length, 41234, "localhost", function(err, bytes) {
    client.close(); 
});

将上面的代码保存为client.js并执行,在服务器端命令行将会输出:

$ node server.js
server listening 0.0.0.0:41234
server got: Node.js from 127.0.0.1:58682

也可以使用socket套接字的方法send:

socket.send(buf, offset, length, port, address, [callback])

参数:要发送的Buffer、Buffer偏移、Buffer长度、目标端口、目标地址、发送完成后的回调函数。

与TCP的套接字write方法相比,send的参数比较复杂,但是它更灵活的地方在于,可以随意发送数据到网络中的服务器端,而TCP如果要发送数据给服务器端,则需要重新通过套接字构造新的连接。

UDP套接字事件

UDP的套接字只是一个EventEmitter实例,而不是Stream实例,它具有以下事件:

1、message:当UDP套接字侦听网卡端口后,接收到消息时触发该事件,触发携带的数据为消息Buffer对象和一个远程地址信息。

2、listening:当UDP套接字开始侦听时触发该事件。

3、close:调用close方法时触发该事件,并不再触发message事件。

4、error:当异常发生时触发该事件,如果不侦听,异常将会直接抛出,使进程退出。

构建HTTP服务

Node提供了基本的http和https模块用于HTTP和HTTPS的封装,用几行代码就可以实现一个简单的HTTP服务器:

var http = require('http'); 
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

HTTP

HTTP,即超文本传输协议。它构建在TCP之上,属于应用层协议。在HTTP的两端是服务器和浏览器,如今精彩纷呈的Web就是HTTP的应用。

HTTP报文

一个报文的例子:

$ curl -v http://127.0.0.1:1337
* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337 > Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked <
Hello World
* Connection #0 to host 127.0.0.1 left intact 
* Closing connection #0

第一部分,TCP的3次握手:

* About to connect() to 127.0.0.1 port 1337 (#0) 2
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)

客户端发送给服务器的请求报文:

> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */* 

服务器处理完成后,发送给客户端的响应:

< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked 
<
Hello World

最后是结束会话的信息:


* Connection #0 to host 127.0.0.1 left intact 
* Closing connection #0

可以总结出HTTP的特点:基于请求响应式的,以一问一答的方式实现服务。

现在的应用,例如浏览器,其实是一个HTTP代理,用户的行为将会通过它转化为HTTP请求报文发送给服务器,服务器端在处理请求后,发送响应报文给代理,代理解析报文后,将用户的需要内容呈现在界面上。

HTTP模块

Node的http模块包含对HTTP处理的封装。HTTP服务继承自TCP服务器,并且能够和多个客户端保持连接,由于采用事件驱动的方式,因此并不会为每一个连接创建额外的线程或进程,以保持很低的内存占用,并实现高并发。

HTTP请求

对于TCP连接的读操作,http模块将其封装为ServerRequest对象。报文头部将会通过http_parser进行解析。

报文头第一行GET / HTTP/1.1被解析之后可以分解为如下属性:

1、req.method: Get
2、req.url: /
3、req.httpVersion: 1.1

其余的报头则是很规律的key:value格式,被解析后放置在req.headers属性上传递给业务逻辑以供调用:

headers:
{ 'user-agent': 'curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5',
host: '127.0.0.1:1337', accept: '*/*' }

报文体部分则抽象为一个只读流对象。如果业务逻辑需要读取报文体中的数据,则要在这个数据流结束后才能进行操作。

function (req, res) {
    // console.log(req.headers);
    var buffers = [];
    req.on('data', function (trunk) {
        buffers.push(trunk); 
    }).on('end', function () {
        var buffer = Buffer.concat(buffers); 
        // TODO
        res.end('Hello world');
    }); 
}

HTTP响应

HTTP响应封装了对底层连接的写操作,可以将其看成一个可写流对象。它能影响报文头部信息的API为res.setHeader()和res.writeHead()。

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

它将生成如下报文:

< HTTP/1.1 200 OK
< Content-Type: text/plain

我们可以调用setHeader进行多次设置,但只有调用writeHead后,报头才会写入连接。

报文体部分则是用res.write和res.end方法实现的,后者与前者的差别在于end方法会先调用write方法发送数据,然后发送信号告知服务器这次响应结束。

无论服务器在处理业务逻辑的时候是否发生异常,务必在结束时调用res.end结束请求,否则客户端将一直处于等待的状态。通过延迟res.end的方式可以实现客户端和服务器的长连接, 但结束的时候务必关闭连接。

HTTP服务事件

HTTP服务器是一个EventEmitter实例,它具有以下事件:

1、connection事件

2、request事件

3、close事件

4、checkContinue事件

5、connect事件

6、upgrade事件

7、clientError事件

HTTP客户端

HTTP客户端就是服务器端服务模型的一部分,处于HTTP的另一端,在整个报文的参与中,报文头和报文体由它产生。http模块提供了一个底层API:http.request(options, connect) 用于构造HTTP客户端。

其中options参数决定了这个HTTP请求头中的内容,它的选项包括:
1、hostname:服务器名称
2、host:服务器的域名或IP地址
3、port:端口,默认80
4、localAddress:建立网络连接的本地网卡
5、socketPath:Domain套接字路径
6、method:HTTP请求方法
7、path:请求路径,默认为/
8、header:HTTP请求方法,默认为GET
9、auth:Basic认证

构建WebSocket服务

WebSocket协议与Node之间的配合堪称完美:
1、WebSocket客户端基于事件的编程模型与Node中自定义事件几乎相差无几;
2、WebSocket实现了客户端与服务器端之间的长连接,而Node事件驱动的方式十分擅长与大量的客户端保持高并发的连接。

WebSocket的优势:
1、客户端与服务器只建立一个TCP连接
2、WebSocket服务器端可以推送数据到客户端
3、更轻量级的协议头

例子:

var socket = new WebSocket('ws://127.0.0.1:12010/updates'); 
socket.onopen = function () {
    setInterval(function() {
        if (socket.bufferedAmount == 0)
            socket.send(getUpdateData()); 
    }, 50);
};
socket.onmessage = function (event) {
    // TODO:event.data 
};

这端代码中,浏览器与服务器创建web socket请求,在请求完成后连接打开,每50毫秒想服务器端发送一次数据,同时,通过onmessage接收服务器传来的数据。相比于HTTP,它能够双向通信。

使用WebSocket,网页客户端只需要一个TCP连接即可完成双向通信,在客户端和服务器端频繁通信时,无需频繁断开连接和重发请求。

WebSocket协议主要分为两个部分:握手和数据传输。

WebSocket握手

WebSocket数据传输

网络服务与安全

我们需要对服务器和客户端之间传递的信息进行加密以保证用户的信息安全。

安全协议:SSL(安全套接层)。它在传输层提供对网络链接加密的功能,对于应用层来说是透明的。之后SSL协议被标准化,称为TLS,即安全传输层协议。

Node在网络安全上提供了3个模块,crypto、tls、https。

真正用于网络的是tls、https这两个模块:tls模块提供了和net模块类似的功能,区别在于它建立在TLS/SSL加密的TCP链接上。对于https而言,完全于http模块接口一致,仅在于它建立在安全的链接之上。

TLS/SSL

密钥

TLS/SSL是一个公钥/私钥的结构,非对称,每个服务器和客户端都有自己的公私钥。

公钥用来加密要传输的数据,私钥用来解密接收到的数据。

公钥和私钥是配对的,通过公钥加密的数据,只有通过私钥才能解密,所以在建立安全传输之前,客户端和服务器之间需要互换公钥。

客户端发送数据时,要通过服务器端的公钥进行加密,服务器端发送数据的时候则要用客户端的公钥进行加密。

Node在底层采用的是openssl实现的TLS/SSL,为此要生成公钥和私钥可以通过openssl完成。

// 生成服务器端私钥
$ openssl genrsa -out server.key 1024 
// 生成客户端私钥
$ openssl genrsa -out client.key 1024

上述命令生成了两个1024位长的RSA私钥文件,我们可以通过它继续生成公钥。

$ openssl rsa -in server.key -pubout -out server.pem
$ openssl rsa -in client.key -pubout -out client.pem

公私钥的非对称加密虽然好,但是网络中依然可能存在窃听的情况。比如中间人攻击。为了解决这种问题,数据传输的过程中,还需要对得到的公钥进行认证, 以确认得到的公钥匙出自目标服务器。

TLS/SSL引入了数字证书来进行认证。与直接使用公钥不同,数字证书中包含了服务器的名称和主机名、服务器公钥、签名办法机构的名称,来自签名颁发机构的签名。在建立连接之前,会通过证书中的签名确认收到的公钥是来自目标服务器的,从而产生信任关系。

数字证书

为了确保数据安全,我们引入了第三方CA(数字证书认证中心)。CA的作用是为站点颁发证书,而且这个证书中具有CA通过自己的公钥和私钥实现的签名。

通过CA机构颁发证书通常是一个繁琐的过程,需要付出一定的精力和费用。对于中小型企业而言,多半采用自签名证书来构建安全网络。也就是自己扮演CA机构,给自己的服务器端颁发签名证书。

TLS服务

HTTPS服务

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值