网络编程
构建tcp服务
TCP
tcp全名为传输控制协议。再osi模型中属于传输层协议。
tcp是面向连接的协议,在传输之前需要形成三次握手形成会话
只有会话形成了,服务端和客户端才能想发送数据,在创建会话的过程中,服务端和客户端分别提供一个套接字。两个套接字共同形成一个连接, 服务端和客服端通过套接字实现两者的操作
创建TCP服务端
var net = require('net')
var server = net.createServer(function (socket) {
// 新的连接
socket.on('data',function(data) {
socket.write('你好')
})
socket.on('end', function(data) {
console.log('连接断开')
})
socket.write('welcome to the new world')
})
// 监听方式
server.listen(8124, function() {
console.log('server bound')
})
// 新的连接
server.on('connection', function(socket) {
})
TCP服务的事件
服务器事件
对于net.createServer()创建的服务器而言,是一个eventEmitter
实例,自定义的事件有如下几种
- listening: server.listen()绑定端口。
- connection: 每个客户端套接字连接到服务器端的时候触发。
- close: 当服务器关系触发的时候,在调用server.close()
- error: 当服务器发生异常的时候, 会触发该事件。
连接事件
服务端可以同多个客户端保持连接。对于每个连接而言都是典型的可读可写的stream对象。stream对象
可以用于服务端和客户端之间的通信,既可以是通过data事件从一段到另一端发来的数据,也可以通过write方法从一段向另一端发送数据。
- data: 当一端通过write发送数据,另一端会触发data事件
- end: 当连接中的任意一端发送了FIN数据,当套接字和服务端成功会被触发
- connect: 该事件用于客户端,当套接字和服务端连接成功的时候会触发
- error: 当异常发生的时候,触发该事件
- close: 当套接字完全关闭的时候,触发该事件
- timeout: 当一定时间后连接不在活跃的时候,该事件会被触发。通知用户已经被闲置了
- drain: 当任意一段调用write发送数据时, 当前这端会触发该事件
tcp套接字是可写可读的stream对象,可以利用pipe方法巧妙的实现管道操作
var net = require('net')
var server = net.createServer(function(socket){
socket.write('Echo server')
socket.pipe(socket)
})
server.listen(1337, '127.0.0.1')
TCP针对网络中的小数据包有一定的优化策略:Nagle算法。如果每次只发送一个字节的内容而不优化,网络中将充满只有极少数有效数据的数据包,将十分浪费网络资源。Nagle算法针对这种情况,要求缓冲区的数据达到一定数量或者一定时间后才将其发出,所以小数据包将会被Nagle算法合并,以此来优化网络。
构建UDP服务
UDP不是面向连接的。一个套接字可以与多个UDP服务通信,它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题
创建UDP套接字
UDP套接字一旦创建,既可以作为客户端发送数据,也可以作为服务器端接收数据。
var dgram = require('dgram')
var socket = dgram.createSocket("udp4")
创建udp服务器端
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)
创建UDP客户端
var dgram = require('dgram')
var message = new Buffer('深入nodejs')
var client = dgram.createSocket("udp4")
client.send(message, 0, message.length, 41234, "localhost", function(err, bytes) {
client.close()
})
api: socket.send(buf, offset, length, port, address, [callback])
upd套接字事件
- message: 收到信息的时候触发该事件,携带数据为消息buffer和一个远程地址信息
- listening: upd套接字来来时监听该事件
- close: 调用close可以触发该事件。不在触发message事件。
- error: 发生异常的时候触发该事件,不监听,异常之间抛出。
HTTP
curl -v http://127.0.0.1:1337 // tcp三次握手
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
基于请求响应式的,以一问一答的方式实现服务,虽然基于TCP会话,但是本身却并无会话的特点。
浏览器构造HTTP报文发向图片服务器端;然后,服务器端判断报文中的要请求的地址,将磁盘中的图片文件以报文的形式发送给浏览器;浏览器接收完图片后,调用渲染引警将其显示给用户。
http模块
Node的http模块包含对HTTP处理的封装。在Node中,HTTP服务继承自TCP服务器(net块 ),它能够与多个客户端保持连接,由于其采用事件驱动的形式,并不为每一个连接创建额外的线程或进程,保持很低的内存占用,所以能实现高并发
在开启keepalive后,一个TCP会话可以用于多次请求和响应。TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务。http模块即是将connection到request的过程进行了封装
http模块讲连接所用的套接字的读取抽象为ServerRequest和ServerResponse对象。分别对应请求和响应操作。在请求产生的过程中,http模块拿到连接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报头之后,触发request时间,调用用户的业务逻辑。
function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'})
res.end('hello wrold\n')
}
HTTP请求
对于TCP连接的读操作,http模块将其封装为ServerRequest对象。让我们再次查看前面的请求报文,报文头部将会通过http_parser进行解析。请求报文的代码如下所示:
GET /HTTP 1.1
User-Agent: curl/7.24.0(x86 64-apple-darwin12.0)libcurl/7.24.0 0penssL/0.9.8r zlib/1.2.5
Host: 127.0.0.1:1337
Accept: */*
报文体部分抽象一个只读流对象,如果业务逻辑需要读取报文体中的数据,数据流结束后才能进行操作
function (req, res) {
var buffers = []
req.on('data', function(trunk) {
buffers.push(trunk);
}).on('end', function() {
var buffer = Buffer.concat(buffers)
res.end('Hello world')
})
}
HTTP请求对象和HTTP响应对象是相对较底层的封装,现行的Web框架如Connect和Express都是在这两个对象的基础上进行高层封装完成的。
HTTP 响应
再来看看HTTP响应对象。HTTP响应相对简单一些,它封装了对底层连接的写操作,可以将其看成一个可写的流对象。它影响响应报文头部信息的API为res.setHeader()和res.writeHead()。
res.writeHead(200, {'Content-Type': 'text/plain'})
HTTP/1.1 200 OK
Content-Type: text/plain
只有调用writeHead后,报文才会写入到连接中,除此之外,http模块会自动帮你设置一些头信息
Date: Sat, 06 Apr 2013 08:01:44 GMT
Connection:keep-alive
Transfer-Encoding:chunked
报文体部分则是调用res.write()和res.end()方法实现,后者与前者的差别在于res.end()会先调用write()发送数据,然后发送信号告知服务器这次响应结束
响应结束后,HTTP服务器可能会将当前的连接用于下一个请求,或者关闭连接。值得注意的是,报头是在报文体发送前发送的,一旦开始了数据的发送,writeHead()和setHeader()将不再生效。
无论服务器端在处理业务逻辑时是否发生异常,务必在结束时调用res.end()结束请求,否则客户端将一直处于等待的状态。当然,也可以通过延迟res.end()的方式实现客户端与服务器端之间的长连接,但结束时务必关闭连接。
HTTP服务的事件
HTTP服务也抽象了一些事件,提供给应用层使用。同样典型的是,服务器是一个EventEmitter实例
- connect事件:在开始HTTP请求和响应前,客户端与服务器端需要建立底层的TCP连接,这个连接可能因为开启了keep-alive,可以在多次请求响应之间使用;当这个连接建立时,服务器触发一次connection事件。
- request事件: 建立TCP连接后,http模块底层将在数据流中抽象出HTTP请求和HTTP响应,当请求数据发送到服务器端,在解析出HTTP请求头后,将会触发该事件;在res.end()后,TCP连接可能将用于下一次请求响应。
- close事件: 与TCP服务器的行为一致,调用server.close()方法停止接受新的连接,当已有的连接都断开时,触发该事件;可以给server.close()传递一个回调函数来快速注册该事件。
- checkContinue: 某些客户端在发送较大的数据时,并不会将数据直接发送,而是先发送一个头部带Expect:100-continue的请求到服务器,服务器将会触发checkContinue事件;如果没有为服务器监听这个事件,服务器将会自动响应客户端100Continue的状态码,表示接受数据上传;如果不接受数据的较多时,响应客户端400Bad Request拒绝客户端继续发送数据即可
- connect事件:当客户端发起CONNECT请求时触发,而发起CONNECT请求通常在HTTP代理时出现;如果不监听该事件,发起该请求的连接将会关闭。
- upgrade事件:当客户端要求升级连接的协议时,需要和服务器端协商,客户端会在请求头中带上Upgrade字段,服务器端会在接收到这样的请求时触发该事件。
- clientError事件:连接的客户端触发error事件时,这个错误会传递到服务器端,此时触发该事件。
http客户端
node提供了基础的http和https模块用于http和https的封装
HTTP请求
对于TCP连接的读操作http模块将其封装为ServerRequest对象。分解为这些属性: 1. req.method 2. req.url 3. req.httpVersion
HTTP响应
res.setHeader, 可以进行多次设置,只有调用res.writedHead之后,才会写入连接中
res.writeHeader
HTTP客户端
- host: 服务器的域名和ip地址
- hostname: 服务器名称
- port: 服务器端口
- localAddress: 建立网络连接的本地网卡
- socketPath: Domain套接字连接路径
- method: http请求方法
- path: 请求路径,默认为/
- header: 请求头对象
- auth:basic认证,用于计算请求头中的Authorization部分
HTTP响应
HTTP客户端的响应对象和服务器端较为相似。
function(res) {
res.setEncoding('utf-8')
res.on('data', function(chunk) {
console.log(chunk)
})
}
http代理
在keepalive的情况下,一个底层会话可以用于多次请求,为了重用tcp连接,http模块包含了一个默认的客户端代理对象Http.globalAgent,对于服务器端的创建的连接进行了连接,实际上是一个线程池。
调用HTTP客户端同时对一个服务器发起10个HTTP请求uid时候,实际上是只有5个请求处于并发状态,后续的请求需要等待某个请求完成服务后真正发出。
http客户端事件
- response
- socket:当底层连接池中建立的连接分配给当前请求对象时,触发该事件。
- connect:当客户端向服务器端发起CONNECT请求时,如果服务器端响应了200状态码,客户端将会触发该事件。
- upgrade:客户端向服务器端发起Upgrade请求时,如果服务器端响应了101 SwitchingProtocols状态,客户端将会触发该事件。
- continue:客户端向服务器端发起Expect:100-continue头信息,以试图发送较大数据量如果服务器端响应100 Continue状态,客户端将触发该事件。
构建websocket服务
- websocket客户端实现了基于事件的编程模型和node中自定义事件类似
- websocket客户端和服务器端之间的长连接,而node事件驱动的方式擅长大量的客户端保持高并发链接
- websocket服务器端可以推送数据到客户端,远比http请求相应模式更加灵活和高效
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
}
类似于TCP客户端,更能双向通信。浏览器一旦能够使用websocket,可以使用的想象空间使用空间极大。
在websocket之前,网页客户端和服务器之间使用的是通信最搞笑的come技术,实现come技术的细节是长轮询或者是iframe流,长轮询的原理是客户端向服务器发送请求,服务器只有在超时或者数据响应时断开连接,客户端在受到数据或者超时后重新发起请求。
websocket握手
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key
用于安全校验。它的值是随机生成的Base64编码的字符串,服务器端就接受之后和字符串相连接,形成字符串,然后通过sha1安全散列算法计算出结果后,进行base64编码,最后返回客户端。
var crypto = require('crypto')
var val = crypto.createHash('sha1').update(key).digest('base64')
// 下面的字段指定子协议和版本号
Sec-WebSocket-Protocol: chat, superchat
Sec-webSocket-Version: 13
// 服务器端在处理完请求之后,响应如下报文
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
上面的报文告之客户端正在更换协议,更新应用层协议为WebSocket协议,并在当前的套接字连接上应用新协议。剩余的字段分别表示服务器端基于Sec-Websocket-Key生成的字符串和选中的子协议。客户端将会校验Sec-Websocket-Accept的值,如果成功,将开始接下来的数据传输。
WebSocket 数据传输
在握手顺利完成之后,当前连接不在进行HTTP的交互,而是开始WebSocket的数据帧协议
服务器没有onopen方法而言,为了tcp套接字事件对websocket事件的封装,需要对封装数据进行处理,WebSocket的数据帧协议是在底层的data事件上封装的。
网络服务和安全
SSL作为一种安全协议,它在传输层提供对网络连接加密的功能。对于应用层而言,它是透明的,数据在传递到应用层之前就已经完成了加密和解密的过程。最初的SSL应用在Web上,被服务器端和浏览器端同时支持,随后IETF将其标准化,称为TLS(Transport LayerSecurity,安全传输层协议)。
Node在网络安全上提供了3个模块,分别为crypto、t1s、https。其中crypto主要用于加密解密,SHA1、MD5等加密算法都在其中有体现,,t1s模块提供了与net模块类似的功能,区别在于它建立在TLS/SSL加密的TCP连接上。对于https而言,它完全与http模块接口一致,区别也仅在于它建立于安全的连接之上
TLS/SSL
TLS/SSL是一个公钥/私钥的结构,它是一个非对称的结构,每个服务器端和客户端都有自己的公私钥。公钥用来加密要传输的数据,私钥用来解密接收到的数据。公钥和私钥是配对的,通过公钥加密的数据,只有通过私钥才能解密,所以在建立安全传输之前,客户端和服务器端之门需要互换公钥。客户端发送数据时要通过服务器端的公钥进行加密,服务器端发送数据时则需要客户端的公钥进行加密,如此才能完成加密解密的过程
公私钥的非对称加密也存在攻击,例如中间人攻击。客户端和服务器端在交换公钥的过程中,中间人对客户端扮演服务器端的角色,对服务器端扮演客户端的角色,因此客户端和服务器端几乎感受不到中间人的存在。如果不能保证这种认证,中间人可能会将伪造的站点响应给用户,从而造成经济损失。
TSL/SSL引入了数字证书进行认证,数字证书包含了服务器的名称和主机名,服务器的公钥,签名颁发机构的名称,来自签名办法机构的签名,在连接建立之前,会通过证书中的签名确认受到的公钥是来自目标服务器的。
数字证书
CA(数字证书认证中心)
服务器端: 服务器向ca机构申请自签名证书,在申请签名证书之前创建自己的csr文件
客户端:客户端在发起安全连接之前回去获取服务器端的证书,通过ca的证书验证服务器端证书的真伪,除了验证真伪以外,还会对服务器名称, ip名称进行验证
TLS服务
var tls = require('tls');
var fs = require('fs');
var options = {
key: fs.readFileSync('./keys/server.key'),
cert: fs.readFileSync('./keys/server.crt'),
requestCert: true,
ca: [ fs.readFileSync('./keys/ca.crt') ]
};
var server = tls.createServer(options, function (stream) {
console.log('server connected', stream.authorized ? 'authorized' : 'unauthorized');
stream.write("welcome!\n");
stream.setEncoding('utf8');
stream.pipe(stream);
});
server.listen(8000, function() {
console.log('server bound');
});
TLS客户端
var tls = require('tls');
var fs = require('fs');
var options = {
key: fs.readFileSync('./keys/client.key'),
cert: fs.readFileSync('./keys/client.crt'),
ca: [ fs.readFileSync('./keys/ca.crt') ]
};
var stream = tls.connect(8000, options, function () {
console.log('client connected', stream.authorized ? 'authorized' : 'unauthorized');
process.stdin.pipe(stream);
});
stream.setEncoding('utf8');
stream.on('data', function(data) {
console.log(data);
});
stream.on('end', function() {
server.close();
});
HTTPS服务
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('./keys/server.key'),
cert: fs.readFileSync('./keys/server.crt')
};
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
HTTPS客户端
var https = require('https');
var fs = require('fs');
var options = {
hostname: 'localhost',
port: 8000,
path: '/',
method: 'GET',
key: fs.readFileSync('./keys/client.key'),
cert: fs.readFileSync('./keys/client.crt'),
ca: [fs.readFileSync('./keys/ca.crt')]
};
options.agent = new https.Agent(options);
var req = https.request(options, function(res) {
res.setEncoding('utf-8');
res.on('data', function(d) {
console.log(d);
});
});
req.end();
req.on('error', function(e) {
console.log(e);
});