HTTP服务器开发之 Socket
【前言】以下源于《从0到1 实现HTTP服务器开发》课程笔记
-
web http 服务器:Apache、IIS、Nginx、Tomcat
-
理解HTTP服务器工作原理
- 如何接受请求
- 如何定位资源
- 如何应答客户端
- 增进对TCP协议、HTTP协议的理解
- TCP协议的原理和基于TCP协议的编程
- HTTP协议的原理和报文结构
- 从零到一实现HTTP服务器开发
认识传输层TCP协议
协议理论基础
TCP(Transmission Control Protocal: 传输控制协议)
功能/特点:
- TCP协议可靠传输
保证数据准确和安全 - TCP协议流量控制
感知对方压力并控制流量(端对端) - TCP协议拥塞控制
感知网络压力并控制发送速度(整个互联网) - 面向字节流的协议
TCP报文:TCP协议传输的数据,包括 TCP首部和TCP数据两部分。其中 TCP数据 的数据来自于应用层。
面向字节流:应用层的数据不管是 uft-8编码的文字、图片、音视频,在进行TCP的通信时,都需要转换成字节流,才可以使用TCP协议来进行传输。
应用场景:
- 微信/QQ等APP消息发送接收
- 浏览器-服务器通信
- 其他可靠通信的场景
套接字通信
网络套接字与通信过程?
1.浏览器与Web服务器时通过TCP协议来进行通信的。浏览器浏览网站,相当于进程与进程之间的通信。
2.如何识别进程?
计算机是一个多进程系统,那么如何计算机辨识是哪个进程在进行网络通信?
-端口(Port)用来标记不同的网络进程
-端口使用16比特位标识(0~65535)
-常用协议端口举例(FTP:21、HTTP:80、HTTPS:443、DNS:53、TELNET:23)
3.套接字(Socket)
{ Port } => 标记不同的进程
{ IP } => 标记 不同的计算机
{ IP :Port } => 套接字
套接字是一个抽象的概念,表示TCP连接的一端。(它并不是一个新的东西,它只是IP和端口的组合,来表示TCP连接的一端)
通过套接字可以进行数据发送或接收。(TCP连接是端到端的通信,所以有两端,一个Socket表示TCP连接的一端,因此浏览器与Web服务器进行通信时,会建立TCP连接,这个TCP连接是由两个Socket组成的,分别是客服端Socket和服务端Socket)
4.套接字(Socket)的编程
服务器端:创建套接字 =》 绑定(bind)套接字 =》监听(listen)套接字 =》处理信息
客户端: 创建套接字 =》连接(connect)套接字 =》处理信息
TCP协议服务端编程
面向TCP协议的套接字实现:
TCP协议的套接字服务器的多线程处理模型:
单线程 多线程一些例子
- 输入URL打开网页
- AJAX获取数据
- img标签加载图片
浏览器与服务器之间怎样去进行交互?有哪些因素会影响到他的加载速度和数据传输效率?
TCP/IP与socket的关系
- socket是TCP/IP协议的API(或者说,TCP是socket上的一种通信协议.)。
TCP通信过程:
应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口,区分不同应用程序进程间的网络通信和连接。两个不同主机上的进程要实现通信的话,要有ip地址(指明要传送的主机是哪一个),还要有套接字socket(绑定端口号)用来标识是主机的哪一个进程。
TCP/UDP是属于传输层的,传输层关心的是端到端之间的通信,也就是不同主机进程之间的通信,它不关心或者不用关心具体的点到点之间的通信是怎样实现的,这是由通信子网(网络层,数据链路层,物理层)完成的。
端到端与点到点:
点到点是物理拓扑,如光纤,就必须是点到点连接。点到点是网络层的。
端到端是网络连接。网络要通信,必须建立连接,不管有多远,中间有多少机器,都必须在两头(源和目的)间建立连接,一旦连接建立起来,就说已经是端到端连接了,即端到端是逻辑链路,这条路可能经过了很复杂的物理路线,但两端主机不管,只认为是有两端的连接,而且一旦通信完成,这个连接就释放了,物理线路可能又被别的应用用来建立连接了。TCP就是用来建立这种端到端连接的一个具体协议。
端到端是传输层的,比如要将数据从A传送到E,中间可能经过A->B->C->D->E,对于传输层来说他并不知道b,c,d的存在,他只认为我的报文数据是从a直接到e的,这就叫做端到端。
总之,一句话概括就是端到端是由无数的点到点实现和组成的。
关于Socket(套接字)
现在我们了解到TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如Win32编程接口一样,TCP/IP也必须对外提供编程接口,这就是Socket。现在我们知道,Socket跟TCP/IP并没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,Socket的出现只是可以更方便的使用TCP/IP协议栈而已,其对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。
不同语言都有对应的建立Socket服务端和客户端的库,下面举例Nodejs如何创建服务端和客户端:
服务端:
const net = require('net');
const server = net.createServer();
server.on('connection', (client) => {
client.write('Hi!\n'); // 服务端向客户端输出信息,使用 write() 方法
client.write('Bye!\n');
//client.end(); // 服务端结束该次会话
});
server.listen(9000); //服务监听9000端口
客户端
const client = new net.Socket();
client.connect(9000, '127.0.0.1', function () {
});
client.on('data', (chunk) => {
console.log('data', chunk.toString())
//data Hi!
//Bye!
});
Socket长连接
所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接(心跳包),一般需要自己做在线维持。 短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。比如Http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
通常的短连接操作步骤是:
连接→数据传输→关闭连接;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理 速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成Socket错误,而且频繁的Socket创建也是对资源的浪费。
什么是心跳包为什么需要:
心跳包就是在客户端和服务端间定时通知对方自己状态的一个自己定义的命令字,按照一定的时间间隔发送,类似于心跳,所以叫做心跳包。网络中的接收和发送数据都是使用Socket进行实现。但是如果此套接字已经断开(比如一方断网了),那发送数据和接收数据的时候就一定会有问题。可是如何判断这个套接字是否还可以使用呢?这个就需要在系统中创建心跳机制。其实TCP中已经为我们实现了一个叫做心跳的机制。如果你设置了心跳,那TCP就会在一定的时间(比如你设置的是3秒钟)内发送你设置的次数的心跳(比如说2次),并且此信息不会影响你自己定义的协议。也可以自己定义,所谓“心跳”就是定时发送一个自定义的结构体(心跳包或心跳帧),让对方知道自己“在线”,以确保链接的有效性。
实现:
服务端:
const net = require('net');
let clientList = [];
const heartbeat = 'HEARTBEAT'; // 定义心跳包内容确保和平时发送的数据不会冲突
const server = net.createServer();
server.on('connection', (client) => {
console.log('客户端建立连接:', client.remoteAddress + ':' + client.remotePort);
clientList.push(client);
client.on('data', (chunk) => {
let content = chunk.toString();
if (content === heartbeat) {
console.log('收到客户端发过来的一个心跳包');
} else {
console.log('收到客户端发过来的数据:', content);
client.write('服务端的数据:' + content);
}
});
client.on('end', () => {
console.log('收到客户端end');
clientList.splice(clientList.indexOf(client), 1);
});
client.on('error', () => {
clientList.splice(clientList.indexOf(client), 1);
})
});
server.listen(9000);
setInterval(broadcast, 10000); // 定时发送心跳包
function broadcast() {
console.log('broadcast heartbeat', clientList.length);
let cleanup = []
for (let i=0;i<clientList.length;i+=1) {
if (clientList[i].writable) { // 先检查 sockets 是否可写
clientList[i].write(heartbeat);
} else {
console.log('一个无效的客户端');
cleanup.push(clientList[i]); // 如果不可写,收集起来销毁。销毁之前要 Socket.destroy() 用 API 的方法销毁。
clientList[i].destroy();
}
}
//Remove dead Nodes out of write loop to avoid trashing loop index
for (let i=0; i<cleanup.length; i+=1) {
console.log('删除无效的客户端:', cleanup[i].name);
clientList.splice(clientList.indexOf(cleanup[i]), 1);
}
}
服务端输出结果:
客户端建立连接: ::ffff:127.0.0.1:57125
broadcast heartbeat 1
收到客户端发过来的数据: Thu, 29 Mar 2018 03:45:15 GMT
收到客户端发过来的一个心跳包
收到客户端发过来的数据: Thu, 29 Mar 2018 03:45:20 GMT
broadcast heartbeat 1
收到客户端发过来的数据: Thu, 29 Mar 2018 03:45:25 GMT
收到客户端发过来的一个心跳包
客户端建立连接: ::ffff:127.0.0.1:57129
收到客户端发过来的一个心跳包
收到客户端发过来的数据: Thu, 29 Mar 2018 03:46:00 GMT
收到客户端发过来的数据: Thu, 29 Mar 2018 03:46:04 GMT
broadcast heartbeat 2
收到客户端发过来的数据: Thu, 29 Mar 2018 03:46:05 GMT
收到客户端发过来的一个心跳包
客户端代码:
const net = require('net');
const heartbeat = 'HEARTBEAT';
const client = new net.Socket();
client.connect(9000, '127.0.0.1', () => {});
client.on('data', (chunk) => {
let content = chunk.toString();
if (content === heartbeat) {
console.log('收到心跳包:', content);
} else {
console.log('收到数据:', content);
}
});
// 定时发送数据
setInterval(() => {
console.log('发送数据', new Date().toUTCString());
client.write(new Date().toUTCString());
}, 5000);
// 定时发送心跳包
setInterval(function () {
client.write(heartbeat);
}, 10000);
客户端输出结果:
发送数据 Thu, 29 Mar 2018 03:46:04 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:04 GMT
收到心跳包:HEARTBEAT
发送数据 Thu, 29 Mar 2018 03:46:09 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:09 GMT
发送数据 Thu, 29 Mar 2018 03:46:14 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:14 GMT
收到心跳包:HEARTBEAT
发送数据 Thu, 29 Mar 2018 03:46:19 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:19 GMT
发送数据 Thu, 29 Mar 2018 03:46:24 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:24 GMT
收到心跳包:HEARTBEAT