1、http的各版本的区别
HTTP/0.9:
- 首个版本,只支持GET请求。
- 没有头部信息,只有HTML内容。
- 无状态,每个请求独立。
- HTTP/1.0:
引入了多种请求方法,包括GET、POST和HEAD。
引入了状态码,使服务器能够向客户端报告请求的成功或失败。
引入了头部字段,支持传递更多信息。
支持长连接(Keep-Alive),允许多个请求和响应通过同一连接传输。
HTTP/1.1:
- 引入了持久连接(Persistent Connections)作为默认行为,减少了连接建立和关闭的开销。
- 引入了管道化(Pipeline),允许在一个连接上发送多个请求,不需要等待响应。
- 引入了更多的缓存控制策略,包括强缓存和协商缓存。
- 引入了分块传输编码(Chunked Transfer Encoding)支持不同大小的消息。
- 支持虚拟主机(Virtual Hosting)。
HTTP/2:
-
通过二进制协议替代了文本协议,提高了解析效率。
-
引入了头部压缩(Header Compression),减小了数据传输的大小。
-
支持多路复用,允许多个请求和响应在同一连接上并行传输,提高性能。
-
引入了服务器推送(Server Push),服务器可以在客户端请求之前主动推送资源。
HTTP/3: -
使用了QUIC协议代替TCP作为传输层协议,提供更快的连接建立和可靠性。
-
引入了TLS 1.3作为安全传输的标准。
-
类似HTTP/2,支持多路复用和头部压缩。
2、https的加密过程
HTTPS(Hypertext Transfer Protocol Secure)是HTTP的安全版本,它使用SSL(Secure Sockets Layer)或其继任者TLS(Transport Layer Security)来加密通信。下面是HTTPS如何进行加密的基本过程:
握手阶段(Handshake):
客户端发起请求: 客户端通过向服务器发起连接请求,表明其支持SSL/TLS。
服务端回应: 服务器在收到请求后,如果支持SSL/TLS,将在响应中回传自己的证书,以及支持的加密算法和其他相关信息。
客户端验证服务器证书:
客户端会检查证书,收到证书过后,会检查其合法性,包括检查证书的颁发者是否可信(比如是否由受信任的证书颁发机构颁发)。如果通过过后,会随机生成随机对称公钥加密,然后发送到服务器
服务端解密
服务器收到加密后的“Pre-Master Secret”后,使用自己的私钥进行解密,得到“Pre-Master Secret”。
共享主密钥生成
客户端和服务器都使用协商好的“Pre-Master Secret”生成主密钥
建立安全通道:
- 双方通信:双方使用协商好的对称会话密钥进行加密和解密通信
- 数据传输:所有传输在这个阶段都是经过加密的,保证数据的隐私和完整性
3、Tcp
-
CLOSED(关闭): 初始状态,表示没有任何连接。
-
LISTEN(监听): 服务器等待接受传入的连接请求。
-
SYN_SENT(同步已发送): 客户端已发送连接请求,等待服务器的确认。
-
SYN_RECEIVED(同步已接收): 服务器已收到连接请求并发送确认。
-
ESTABLISHED(已建立): 连接已经建立,数据可以在客户端和服务器之间传输。
-
FIN_WAIT_1(终止等待1): 客户端发送连接终止请求(FIN),等待服务器的确认。
-
FIN_WAIT_2(终止等待2): 客户端等待服务器发送连接终止请求。
-
CLOSE_WAIT(关闭等待): 服务器已经关闭连接,但客户端仍然可以发送数据。
-
CLOSING(关闭中): 两端都发送连接终止请求,等待对方的确认。
-
LAST_ACK(最后确认): 客户端发送连接终止请求,并等待服务器的确认。
-
TIME_WAIT(时间等待): 连接已经关闭,等待足够的时间以确保远程端已经收到连接终止请求的确认。这是一种避免在网络中出现重复数据的机制。
建立连接过程(三次握手、四次挥手)
-
三次握手
一开始建立连接之间的服务器和客户端都是close状态,服务器创建socket开始监听,变为LISTEN状态,客户端建立连接后,向服务端发送SYN报文,则客户端则会变成SYN_SENT状态。服务器收到客户端的报文向客户端发送ACK和SYN报文,此时服务端为SYN_RECV状态。客户端收到ACK和SYN之后,就向服务器发送ACK,客户端状态就为ESTABLELISHED状态。服务器同样收到ACK之后变为ESTABLELISHED状态 -
四次挥手
第一步(FIN_WAIT_1): 一方(通常是客户端)发送连接终止请求,即发送一个带有FIN(Finish)标志的TCP段给对方。此时,该方进入FIN_WAIT_1状态,表示它不再发送数据,但仍然可以接收数据。第二步(CLOSE_WAIT和ACK): 接收到终止请求的一方(通常是服务器)收到FIN后,进入CLOSE_WAIT状态,表示它已经准备好关闭连接。然后,该方向对方发送一个确认(ACK)标志的TCP段,表示它收到了终止请求。
第三步(LAST_ACK和FIN): 接收到确认的一方(通常是客户端)进入FIN_WAIT_2状态,表示它同样准备关闭连接。然后,该方向对方发送一个带有FIN标志的TCP段,表示它也要关闭连接。此时,它等待对方的确认。
第四步(TIME_WAIT和ACK): 对方接收到最后的FIN后,进入LAST_ACK状态。它等待一段时间(等待足够长的时间确保对方已经收到了最后的FIN),然后发送一个确认(ACK)给对方。此时,该连接的一方(通常是服务器)进入TIME_WAIT状态,等待一段时间以确保对方接收到确认。然后它正式关闭连接。
在整个四次挥手的过程中,TIME_WAIT状态的作用是确保在网络中所有的数据都被正确接收,防止在下一个新的连接中出现冲突。这个等待时间通常是2倍的最大报文段寿命(Maximum Segment Lifetime,MSL),MSL通常为2分钟。
tcp为啥是三次握手,四次挥手呢(参考小林coding)
答:
1、因为tcp是全双工通信,要保证双端都具备收发功能
2、避免历史连接,因为可能网络拥塞导致客户端发送多次SYN之后,旧的SYN到服务端之后,服务端会回一个SYN+ACK给客户端,客户端收到之后,发现不是自己期望的,就会回的个RST,服务器收到之后就会释放掉。直到拿到自己期望的ACK和SYN。
3、同步双方的序列号:因为通信双方都需要维护一个序列号。它的作用:
-
接收方可以去除重复的数据;
-
接收方可以根据数据包的序列号按序接收;
-
可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道)
因此客户端带有序列号的SYN时候,服务器需要回个ACK的应当报文,那么客户端已被服务端成功接收了,那么服务端发送序列号给客户端。
4、避免资源浪费掉
如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。tcp挥手为啥四次
因为服务器收到了客户端发来的SYN报文之后,内核马上会回复一个ACK报文的,但是服务端的应用程序还有数据是否要发送,所以不能马上发送SYN,这个完全取决应用程序,把发送SYN的控制权交付给应用程序 -
如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
-
如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,
握手中第一次可以传输数据吗
答:第一次传递不可以携带数据。如果第一次握手可以携带数据的话,那么就会出现恶意攻击服务器的情况,每次都在第一次中SYN中放入大量的数据,疯狂的发送SYN,就会花费服务器的大量内存。
第二次可以携带数据吗
当返回ACK的时候,不知道客户端需要什么样的数据。
挥手的时候是立即断开的吗
不是,会进入到Time_WAIT状态。
- 防止最后一次的ACK丢失
如果没有TIME_WAIT状态,客户端就会收到服务端的ACK之后,直接进入到FIN_WAIT_2状态,进入close状态,并释放连接,如果最后ACK丢失的话,服务器重传的FIN无人应答,最后导致服务器长时间处于LAST_ACK而无法正常的关闭 - 防止新连接收到tcp旧的报文
tcp使用一个四元组来区分连接(源端口、目的端口、源IP、目的IP)如果新旧连接Ip与端口完全一致的话,则内核协议栈无法区分这两条连接
SYN什么情况下会被丢弃(参考小林coding)
在tcp挥手的过程中,主动断开的一方会有个TIME_WAIT状态,持续2ML才会变成CLOSE的状态。一般存在2种场景下才会出现SYN丢弃。
- 开启 tcp_tw_recycle 参数,并且在 NAT 环境下,造成 SYN 报文被丢弃
- TCP 两个队列满了(半连接队列和全连接队列),造成 SYN 报文被丢弃
如果time_wait状态过多的话,会存在端口占用的情况,过多的time_wait的状态会对系统的并发量造成影响。然后time_wait在于:
- 防止具有相同四元组的旧数据包被收到,也就是防止历史连接中的数据,被后面的连接接受,否则就会导致后面的连接收到一个无效的数据
- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
在Linux操作系统中,你提到了两个系统参数来快速回收TIME_WAIT状态的连接。这些可能是net.ipv4.tcp_tw_recycle和net.ipv4.tcp_tw_reuse,它们分别用于启用TCP连接的快速回收和允许地址重用。这些参数的调整可能对减少TIME_WAIT状态的影响有所帮助。他们的实现会开启PAWS机制
PAWS的工作原理如下:
时间戳选项: PAWS使用TCP选项中的时间戳选项。在TCP头部中,有一个称为"Timestamps"(时间戳)的选项,用于在数据包中包含发送端的时间戳。这个时间戳可以帮助接收端确定数据包的相对顺序。
时间戳差异检查: 接收端通过检查时间戳的差异来确定数据包的顺序。如果接收到的数据包的时间戳与先前接收到的数据包的时间戳之差超过一定阈值,那么这被认为是一个新的连接。
防止旧序列号攻击: PAWS机制的主要目标是防止旧的序列号被用于伪造连接。通过时间戳的比较,接收端可以确定是否接收到的数据包是来自一个新的连接,而不是之前的连接。这有助于保护TCP连接免受一些攻击,如“攻击者伪造旧的序列号”(Old Sequence Number Attack)。
accept队伍已经满了
在linux下,tcp三次握手的时候,Linux会维护2个队列
- 半连接队列,也称 SYN 队列;
- 全连接队列,也称 accepet 队列;
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
tcp的快重传和慢重传
tcp的重传机制主要是防止网路包丢失,重传的工作机制是借助tcp头部的序列号和确认号决定是否重传,触发方式有:
- 超时重传
- 快速重传
- SACK
- D-SACK
1、快速重传(Fast Retransmit):
-
触发条件: 当发送方连续收到三个相同的对同一个数据包的确认时,它会认为该数据包丢失,并立即进行重传。
-
作用: 快速重传的目标是快速恢复丢失的数据包,而不必等待超时。这可以提高数据传输的效率,尤其在网络中的一些瞬时故障引起的数据包丢失时。
2、慢启动(Slow Start):
-
触发条件: 当发送方初始发送数据或经历了超时后,TCP会进入慢启动状态。
-
作用: 在慢启动阶段,发送方逐渐增加发送的数据量,以便逐步探测网络的可用带宽。慢启动使用拥塞窗口(congestion window)来控制发送方可以发送的数据量。如果在慢启动过程中检测到丢失的数据包,TCP 将回退到慢启动的起始阈值,并重新进入慢启动。
4、五种IO模型参考
- 阻塞IO
- 非阻塞IO
- IO复用
- 信号驱动
- 异步IO
缓存IO
指的是通过使用缓存来改善输入/输出性能的一种技术。这种技术通过将数据暂时存储在内存中,以减少对慢速的物理设备(如硬盘或网络)的直接访问次数,从而提高程序的运行效率
在Linux的缓存IO模型中,操作系统会把IO数据缓存在文件系统的页缓存中,也就是说操作系统会把数据拷贝到内核中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
文件描述符
是操作系统中用于标识和访问文件或者输入输出资源的一种抽象概念。在Unix和类Unix系统中,包括Linux,每个正在运行的进程都维护一个文件描述符表,它记录了该进程打开的文件、网络套接字等资源。
而文件描述符是一个非负整数,通常是小于系统定义的最大文件描述符数的整数,指向内核中为每个进程所打开的文件的记录表。当程序打开一个现有文件或者创建一个新文件时, 内核向进程返回一个文件描述符
阻塞IO
其中进程在执行 I/O 操作时会被阻塞(即暂时停止执行),直到操作完成为止。在阻塞 I/O 中,当应用程序发起一个 I/O 操作(比如读取文件或从网络套接字接收数据)时,应用进程就会阻塞住,一直等数据直到拷贝到用户空间,这段时间内进程始终阻塞
//服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(clientAddr);
char buffer[BUFFER_SIZE];
// 创建服务器套接字
if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
// 将服务器套接字绑定到指定端口
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("Error binding socket");
close(serverSocket);
exit(EXIT_FAILURE);
}
// 监听连接请求
if (listen(serverSocket, 5) == -1) {
perror("Error listening on socket");
close(serverSocket);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 等待客户端连接
if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrLen)) == -1) {
perror("Error accepting connection");
close(serverSocket);
exit(EXIT_FAILURE);
}
printf("Client connected: %s\n", inet_ntoa(clientAddr.sin_addr));
// 从客户端接收数据并发送回应
ssize_t bytesRead;
while ((bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0)) > 0) {
buffer[bytesRead] = '\0'; // 添加字符串结束符
printf("Received from client: %s\n", buffer);
// 发送回应给客户端
send(clientSocket, buffer, bytesRead, 0);
}
if (bytesRead == 0) {
printf("Client disconnected.\n");
} else {
perror("Error receiving from client");
}
// 关闭套接字
close(clientSocket);
close(serverSocket);
return 0;
}
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int clientSocket;
struct sockaddr_in serverAddr;
char buffer[BUFFER_SIZE];
// 创建客户端套接字
if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(PORT);
// 连接到服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("Error connecting to server");
close(clientSocket);
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
// 发送消息给服务器
const char* message = "Hello, Server!";
send(clientSocket, message, strlen(message), 0);
printf("Sent to server: %s\n", message);
// 接收服务器的回应
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // 添加字符串结束符
printf("Received from server: %s\n", buffer);
} else {
perror("Error receiving from server");
}
// 关闭套接字
close(clientSocket);
return 0;
}
这只是个简单的阻塞IO模型的客户端和服务端,服务器接受客户端连接,然后通过阻塞 I/O 接收客户端发送的消息并发送回应。客户端连接到服务器,发送一条消息并等待服务器的回应
非阻塞IO
不管数据有没有返回,然后通过轮询的方式不停的与内核交互,通常非阻塞 I/O 通常与多路复用(multiplexing)机制一起使用,例如 select、poll、epoll(在 Linux 上)或 kqueue(在 BSD 系统上)等。这些机制允许应用程序同时监听多个 I/O 事件,并在任何一个事件就绪时进行处理。
//服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int serverSocket, clientSocket, maxSockets;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(clientAddr);
char buffer[BUFFER_SIZE];
// 创建服务器套接字
if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
// 将服务器套接字绑定到指定端口
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("Error binding socket");
close(serverSocket);
exit(EXIT_FAILURE);
}
// 监听连接请求
if (listen(serverSocket, 5) == -1) {
perror("Error listening on socket");
close(serverSocket);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 设置客户端套接字集合
fd_set readSockets, activeSockets;
FD_ZERO(&activeSockets);
FD_SET(serverSocket, &activeSockets);
maxSockets = serverSocket;
while (1) {
readSockets = activeSockets;
// 使用 select 多路复用等待事件就绪
if (select(maxSockets + 1, &readSockets, NULL, NULL, NULL) == -1) {
perror("Error in select");
exit(EXIT_FAILURE);
}
// 检查服务器套接字是否有新连接
if (FD_ISSET(serverSocket, &readSockets)) {
if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrLen)) == -1) {
perror("Error accepting connection");
continue;
}
printf("Client connected: %s\n", inet_ntoa(clientAddr.sin_addr));
FD_SET(clientSocket, &activeSockets);
if (clientSocket > maxSockets) {
maxSockets = clientSocket;
}
}
// 处理客户端发送的消息
for (int i = serverSocket + 1; i <= maxSockets; ++i) {
if (FD_ISSET(i, &readSockets)) {
ssize_t bytesRead = recv(i, buffer, sizeof(buffer), 0);
if (bytesRead > 0) {
buffer[bytesRead] = '\0';
printf("Received from client: %s\n", buffer);
// 发送回应给客户端
send(i, buffer, bytesRead, 0);
} else if (bytesRead == 0) {
printf("Client disconnected.\n");
close(i);
FD_CLR(i, &activeSockets);
} else {
perror("Error receiving from client");
}
}
}
}
// 关闭服务器套接字
close(serverSocket);
return 0;
}
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024
int setNonBlocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl");
return -1;
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl");
return -1;
}
return 0;
}
int main() {
int clientSocket;
struct sockaddr_in serverAddr;
char buffer[BUFFER_SIZE];
// 创建客户端套接字
if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(PORT);
// 连接到服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("Error connecting to server");
close(clientSocket);
exit(EXIT_FAILURE);
}
// 设置客户端套接字为非阻塞
if (setNonBlocking(clientSocket) == -1) {
close(clientSocket);
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
// 发送消息给服务器
const char* message = "Hello, Server!";
send(clientSocket, message, strlen(message), 0);
printf("Sent to server: %s\n", message);
// 使用 select() 函数等待服务器的回应
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(clientSocket, &readSet);
struct timeval timeout = {5, 0}; // 设置超时时间为 5 秒
if (select(clientSocket + 1, &readSet, NULL, NULL, &timeout) > 0) {
// 有数据可读
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // 添加字符串结束符
printf("Received from server: %s\n", buffer);
} else {
perror("Error receiving from server");
}
} else {
printf("Timeout: No response from server.\n");
}
// 关闭客户端套接字
close(clientSocket);
return 0;
}
异步IO
用户进程调用aio_read,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。内核收到请求,数据准备好以后,直接把数据拷贝到用户空间,然后再通知进程本次IO已经完成
在异步 I/O 中,通常使用事件驱动的方式,应用程序会注册一个回调函数或者使用事件轮询机制等待 I/O 操作完成。异步 I/O 可以提高系统的并发性和响应性
现在有比较好用的boost库,后面有时间更新boost库
//服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define PORT 8080
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int setNonBlocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl");
return -1;
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl");
return -1;
}
return 0;
}
int main() {
int serverSocket, clientSocket, epollFd;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(clientAddr);
char buffer[BUFFER_SIZE];
// 创建服务器套接字
if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
// 将服务器套接字绑定到指定端口
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("Error binding socket");
close(serverSocket);
exit(EXIT_FAILURE);
}
// 监听连接请求
if (listen(serverSocket, 5) == -1) {
perror("Error listening on socket");
close(serverSocket);
exit(EXIT_FAILURE);
}
// 设置服务器套接字为非阻塞
if (setNonBlocking(serverSocket) == -1) {
close(serverSocket);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 创建 epoll 实例
if ((epollFd = epoll_create1(0)) == -1) {
perror("Error creating epoll");
close(serverSocket);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 监听读取事件和边缘触发模式
event.data.fd = serverSocket;
// 将服务器套接字添加到 epoll 实例中
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, serverSocket, &event) == -1) {
perror("Error adding server socket to epoll");
close(epollFd);
close(serverSocket);
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
// 使用 epoll 实例进行事件轮询
while (1) {
int numEvents = epoll_wait(epollFd, events, MAX_EVENTS, -1);
if (numEvents == -1) {
perror("Error in epoll_wait");
break;
}
for (int i = 0; i < numEvents; ++i) {
if (events[i].data.fd == serverSocket) {
// 有新的连接请求
if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrLen)) != -1) {
printf("Client connected: %s\n", inet_ntoa(clientAddr.sin_addr));
// 设置客户端套接字为非阻塞
if (setNonBlocking(clientSocket) == -1) {
close(clientSocket);
continue;
}
// 将客户端套接字添加到 epoll 实例中
event.events = EPOLLIN | EPOLLET; // 监听读取事件和边缘触发模式
event.data.fd = clientSocket;
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientSocket, &event) == -1) {
perror("Error adding client socket to epoll");
close(clientSocket);
}
}
} else {
// 客户端套接字有数据可读
int sockfd = events[i].data.fd;
ssize_t bytesRead = recv(sockfd, buffer, sizeof(buffer), 0);
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // 添加字符串结束符
printf("Received from client: %s\n", buffer);
// 发送回应给客户端
send(sockfd, buffer, bytesRead, 0);
} else if (bytesRead == 0) {
// 客户端断开连接
printf("Client disconnected.\n");
epoll_ctl(epollFd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
} else {
perror("Error receiving from client");
}
}
}
}
// 关闭服务器套接字和 epoll 实例
close(serverSocket);
close(epollFd);
return 0;
}
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int setNonBlocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl");
return -1;
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl");
return -1;
}
return 0;
}
int main() {
int clientSocket, epollFd;
struct sockaddr_in serverAddr;
char buffer[BUFFER_SIZE];
// 创建客户端套接字
if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(PORT);
// 连接到服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
if (errno != EINPROGRESS) {
perror("Error connecting to server");
close(clientSocket);
exit(EXIT_FAILURE);
}
}
// 设置客户端套接字为非阻塞
if (setNonBlocking(clientSocket) == -1) {
close(clientSocket);
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
// 创建 epoll 实例
if ((epollFd = epoll_create1(0)) == -1) {
perror("Error creating epoll");
close(clientSocket);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.events = EPOLLOUT | EPOLLET; // 监听写入事件和边缘触发模式
event.data.fd = clientSocket;
// 将客户端套接字添加到 epoll 实例中
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientSocket, &event) == -1) {
perror("Error adding client socket to epoll");
close(clientSocket);
close(epollFd);
exit(EXIT_FAILURE);
}
// 使用 epoll 实例进行事件轮询
while (1) {
struct epoll_event events[MAX_EVENTS];
int numEvents = epoll_wait(epollFd, events, MAX_EVENTS, -1);
if (numEvents == -1) {
perror("Error in epoll_wait");
break;
}
for (int i = 0; i < numEvents; ++i) {
if (events[i].data.fd == clientSocket) {
// 客户端套接字可写
if (events[i].events & EPOLLOUT) {
const char* message = "Hello, Server!";
send(clientSocket, message, strlen(message), 0);
printf("Sent to server: %s\n", message);
// 修改事件为读取事件
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epollFd, EPOLL_CTL_MOD, clientSocket, &event) == -1) {
perror("Error modifying events for client socket");
close(clientSocket);
close(epollFd);
exit(EXIT_FAILURE);
}
}
} else {
// 客户端套接字有数据可读
int sockfd = events[i].data.fd;
ssize_t bytesRead = recv(sockfd, buffer, sizeof(buffer), 0);
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // 添加字符串结束符
printf("Received from server: %s\n", buffer);
} else if (bytesRead == 0) {
// 服务器断开连接
printf("Server disconnected.\n");
epoll_ctl(epollFd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
} else {
perror("Error receiving from server");
}
}
}
}
// 关闭客户端套接字和 epoll 实例
close(clientSocket);
close(epollFd);
return 0;
}
异步采用epoll模型,因为回调函数通常通过设置事件处理器(event handler)来实现。在示例中,通过 epoll 实例的事件轮询,通过检查事件触发时的事件类型(events[i].events)
通过检查 events[i].events 中的 EPOLLIN 事件,判断是否有数据可读。如果有数据可读,就执行相应的读取操作,并在读取完成后执行相应的业务逻辑。
IO多路复用模型(select、epoll,poll)
IO多路复用模型是一种通过单一的线程来监控多个文件描述符(sockets、文件等)的I/O事件的机制。这样可以在单个线程中处理多个连接,提高程序的并发性能。
select模型: select 函数是Unix/Linux下的一种 I/O多路复用机制,通过该函数可以监视多个文件描述符的可读、可写和异常等事件,当某个文件描述符就绪(即对应的文件描述符发生了某个事件),select 将返回,然后可以通过遍历文件描述符集合来找到就绪的文件描述符。
缺点:
- 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024。由于select用轮询的方式扫描文件描述符,文件描述符越多,性能越差;
- 内核用户空间内存拷贝问题,select需要⼤ᰁ句柄数据结构,产⽣巨⼤开销;
- Select返回的是含有整个句柄的数组,应⽤程序需要遍历整个数组才能发现哪些句柄发生了事件
- Select的触发方式是水平触发的,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么每次Select调用还会将这些描述符通知进程
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
poll模型: poll 函数与 select 类似,同样用于监视多个文件描述符的事件。相比 select,poll 使用一个更加灵活的 pollfd 结构体数组来传递描述符和事件。
缺点:
- 与select相比的话,保存文件描述符以链表的方式来保存,但三个缺点依然存在
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
epoll模型: epoll 是Linux特有的I/O多路复用模型,它提供了更加灵活和高效的事件通知机制。与 select 和 poll 不同,epoll 不需要遍历文件描述符集合,而是通过系统回调机制直接获取就绪的文件描述符。
上⾯所说的select缺点在epoll上不复存在,epoll使⽤一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的⼀个事件表中,这样在⽤户空间和内核空间的copy只需⼀次。Epoll是事件触发的,不是轮询查询的。没有最⼤的并发连接限制,内存拷贝,利用mmap()⽂件映射内存加速与内核空间的消息传递。
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
** 为啥epoll底层数据结构要用红黑树**
答:
快速查找: 红黑树的查找时间复杂度为O(log n),在红黑树中快速找到对应的文件描述符。
有序遍历: 红黑树的有序性允许 epoll 在需要的时候按照顺序遍历文件描述符。
高效插入和删除: 红黑树的自平衡性质确保了插入和删除操作的效率,这对于动态地管理文件描述符集合是非常重要的。
在 epoll 中,存在三种模式:EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,它们分别用于添加、修改和删除文件描述符。通过红黑树,epoll 能够高效地维护和管理文件描述符集合的变化。在红黑树的节点上存储了事件和状态信息,以支持 epoll_wait 等操作。
总的来说,红黑树作为 epoll 的底层数据结构,提供了高效的文件描述符管理和事件触发机制,使得 epoll 在高并发的网络应用中能够快速而有效地处理大量的I/O事件。
红黑树的缺点:
- 内存占用:红黑树相对于其他数据结构来说,会占用更多的内存空间
每个节点存储值和指针,还需要存储颜色,还增加了节点内存开销
对于大规模数据集或内存受限的系统,红黑树的占用可能存在问题 - 缓存性能:由于红黑树的节点在内存中存储不一定是连续的
因此访问节点可能出现缓存不命中的问题,从而影响访问性能
相比之下,其他数据结构如数组和链表在局部缓存中性能比较好
5、面试题随便整理(后面遇到随时更新)
DNS域名解析
-
DNS(Domain Name System)是用于将域名转换为 IP 地址的分布式命名系统。DNS解析过程通常涉及以下步骤:
-
本地域名解析器: 当用户在浏览器中输入一个域名时,首先会检查本地域名解析器(Local DNS Resolver)的缓存,看是否已经有该域名对应的IP地址。如果缓存中存在,则直接返回对应的IP地址。
-
根域名服务器: 如果本地域名解析器的缓存中没有找到对应的IP地址,它会向根域名服务器(Root DNS Server)发送查询请求。根域名服务器是整个DNS系统的起点,负责指向顶级域名服务器。
-
顶级域名服务器: 根域名服务器会返回顶级域名的IP地址给本地域名解析器。顶级域名服务器负责管理顶级域(如.com、.org、.net等)。
-
权威域名服务器: 本地域名解析器得到顶级域名的IP地址后,会继续向相应的顶级域名服务器发送查询请求,获取该域名下的权威域名服务器的IP地址。
-
目标域名服务器: 本地域名解析器接收到权威域名服务器的IP地址后,再次向权威域名服务器发送查询请求,获取最终目标域名服务器的IP地址。
-
解析目标域名: 本地域名解析器通过与目标域名服务器的通信,获取到最终目标域名对应的IP地址,并将这个IP地址存入缓存中。
-
返回IP地址给应用程序: 本地域名解析器将获取到的IP地址返回给用户的应用程序(例如,浏览器),应用程序使用这个IP地址建立与目标服务器的连接。
get请求post请求
定义: GET 方法用于从服务器获取资源。在 RFC 7231 中,GET 被定义为一种安全且幂等的方法,意味着它不应该引起服务器状态的变化,而且多次重复的请求应该具有相同的效果。
特点:
- 安全:不会引起服务器状态变化。
- 幂等:多次请求应该具有相同的效果。
- 可缓存:响应可以被缓存。
参数传递:
- 参数通常附加在 URL 后面,通过查询字符串传递。
- 数据通常不在请求体中,而是在 URL 中。
适用场景:
适用于获取资源、检索数据,不适合传递敏感信息,因为查询字符串可能会被保存在浏览器历史记录和服务器日志中。
POST 方法:
定义: POST 方法用于向服务器提交数据,以便创建新的资源。在 RFC 7231 中,POST 被定义为不安全和非幂等的方法,因为它可能引起服务器状态的变化,并且多次重复的请求可能产生不同的效果。
特点:
- 不安全:可能引起服务器状态变化。
- 非幂等:多次请求可能产生不同的效果。
- 不可缓存:响应不应被缓存。
参数传递:
- 参数通常在请求体(Body)中传递,而不是在 URL 中。
- 可以传递更多的数据,并且支持多种类型的数据。
适用场景:
适用于提交表单数据、上传文件等需要传递数据的场景