目录
[1.1 网络协议](#1.1 网络协议)
[1.2 IP地址和端口号](#1.2 IP地址和端口号)
[1.3 Socket](#1.3 Socket)
[1.4 TCP协议的三次握手和四次挥手](#1.4 TCP协议的三次握手和四次挥手)
[TCP 三次握手流程图](#TCP 三次握手流程图)
[TCP 四次挥手流程图](#TCP 四次挥手流程图)
[1.5 详细介绍一下http协议](#1.5 详细介绍一下http协议)
[HTTP/1.1 vs HTTP/2 vs HTTP/3](#HTTP/1.1 vs HTTP/2 vs HTTP/3)
[2.1 创建Socket](#2.1 创建Socket)
[2.2 绑定地址和端口](#2.2 绑定地址和端口)
[2.3 监听和接受连接](#2.3 监听和接受连接)
[2.4 发送和接收数据](#2.4 发送和接收数据)
[2.5 关闭Socket](#2.5 关闭Socket)
[五、深入探讨 select 和 epoll,以及多客户端编程](#五、深入探讨 select 和 epoll,以及多客户端编程)
[5.1 select 的工作原理及使用](#5.1 select 的工作原理及使用)
[5.1.1 select 基本概念](#5.1.1 select 基本概念)
[5.1.2 select 函数原型](#5.1.2 select 函数原型)
[5.1.3 使用 select 的步骤](#5.1.3 使用 select 的步骤)
[5.1.4 select 示例](#5.1.4 select 示例)
[5.2 epoll 的工作原理及使用](#5.2 epoll 的工作原理及使用)
[5.2.1 epoll 基本概念](#5.2.1 epoll 基本概念)
[5.2.2 epoll 函数](#5.2.2 epoll 函数)
[5.2.3 使用 epoll 的步骤](#5.2.3 使用 epoll 的步骤)
[5.2.4 epoll 示例](#5.2.4 epoll 示例)
[5.3、select 和 epoll 的比较](#5.3、select 和 epoll 的比较)
[5.3.1 select 的优缺点](#5.3.1 select 的优缺点)
[5.3.2 epoll 的优缺点](#5.3.2 epoll 的优缺点)
网络编程是后端开发中不可或缺的一部分,尤其是在构建需要与其他系统或设备通信的应用程序时。本文将从基础概念开始,逐步深入网络编程的各个方面,帮助读者建立全面的网络编程知识体系。
一、网络编程基础概念
1.1 网络协议
网络协议是计算机网络中进行数据交换的规则。常见的网络协议包括:
- TCP/IP:传输控制协议/互联网协议,是互联网的核心协议。TCP提供可靠的、面向连接的通信,而IP负责数据包的路由和传输。
- UDP:用户数据报协议,是一种无连接的协议,适用于实时应用,如视频流和在线游戏。
- HTTP/HTTPS:超文本传输协议/安全超文本传输协议,用于浏览器和服务器之间的通信。
- FTP:文件传输协议,用于文件的上传和下载。
1.2 IP地址和端口号
- IP地址 :标识网络中的每个设备,如
192.168.1.1
。 - 端口号 :用于区分同一设备上的不同服务,范围为
0
到65535
。
1.3 Socket
Socket是网络编程的基石,提供了应用层与TCP/IP协议栈通信的接口。主要类型有:
- 流式套接字(Stream Socket):基于TCP,提供可靠的数据传输。
- 数据报套接字(Datagram Socket):基于UDP,适用于无连接的数据传输。
1.4 TCP协议的三次握手和四次挥手
TCP的三次握手
三次握手(Three-way Handshake)是TCP建立连接的过程,确保双方能够正确接收和发送数据。三次握手的步骤如下:
-
第一次握手(SYN):
-
客户端向服务器发送一个SYN(Synchronize)报文,表示请求建立连接。
-
报文头中的SYN标志位被置为1,同时生成一个初始序列号(Sequence Number),假设为x。
客户端 -> 服务器:SYN=1, Seq=x
-
-
第二次握手(SYN-ACK):
-
服务器收到客户端的SYN报文后,确认连接请求,并向客户端发送一个SYN-ACK(Synchronize-Acknowledgment)报文。
-
报文头中的SYN和ACK标志位都被置为1,ACK号为x+1(确认已收到客户端的SYN),并生成一个自己的初始序列号(假设为y)。
服务器 -> 客户端:SYN=1, ACK=1, Seq=y, Ack=x+1
-
-
第三次握手(ACK):
-
客户端收到服务器的SYN-ACK报文后,向服务器发送一个ACK(Acknowledgment)报文,表示确认连接建立。
-
报文头中的ACK标志位被置为1,ACK号为y+1(确认已收到服务器的SYN),序列号为x+1。
客户端 -> 服务器:ACK=1, Seq=x+1, Ack=y+1
-
完成三次握手后,客户端和服务器之间的TCP连接正式建立,可以开始数据传输。
TCP的四次挥手
四次挥手(Four-way Handshake)是TCP断开连接的过程,确保双方都能正常关闭连接。四次挥手的步骤如下:
-
第一次挥手(FIN):
-
一方(通常是客户端)向另一方发送一个FIN(Finish)报文,表示希望关闭连接。
-
报文头中的FIN标志位被置为1,序列号为u。
客户端 -> 服务器:FIN=1, Seq=u
-
-
第二次挥手(ACK):
-
另一方(通常是服务器)收到FIN报文后,向发送方发送一个ACK报文,确认已经收到关闭连接的请求。
-
报文头中的ACK标志位被置为1,ACK号为u+1。
服务器 -> 客户端:ACK=1, Seq=v, Ack=u+1
-
-
第三次挥手(FIN):
-
服务器也向客户端发送一个FIN报文,表示同意关闭连接。
-
报文头中的FIN标志位被置为1,序列号为w。
服务器 -> 客户端:FIN=1, Seq=w
-
-
第四次挥手(ACK):
-
客户端收到服务器的FIN报文后,向服务器发送一个ACK报文,确认已经收到关闭连接的请求。
-
报文头中的ACK标志位被置为1,ACK号为w+1。
客户端 -> 服务器:ACK=1, Seq=u+1, Ack=w+1
-
完成四次挥手后,客户端和服务器之间的TCP连接正式关闭。
通过三次握手和四次挥手机制,TCP协议能够确保可靠地建立和关闭连接,使得数据传输变得可靠和有序。这是网络编程中非常重要的部分,理解和掌握这些概念对于实现高效和稳定的网络通信至关重要。
整个流程更通俗易懂
TCP 三次握手流程图
客户端 服务器
| |
| --------- SYN, Seq=x ------------> |
| |
| <------ SYN, ACK, Seq=y, Ack=x+1 --|
| |
| --------- ACK, Seq=x+1, Ack=y+1 -->|
| |
- 解释:
-
第一次握手:客户端发送SYN:
- 客户端向服务器发送一个SYN(同步)包,表明客户端想要建立连接,并且包含一个初始序列号(Seq=x)。
-
第二次握手:服务器回应SYN-ACK:
- 服务器收到SYN包后,回应一个SYN-ACK包,表示同意连接。这个包包含服务器的初始序列号(Seq=y)和对客户端SYN包的确认(Ack=x+1)。
-
第三次握手:客户端发送ACK:
- 客户端收到SYN-ACK包后,回应一个ACK包,确认服务器的SYN包(Ack=y+1),此时连接建立,双方可以开始传输数据。
TCP 四次挥手流程图
客户端 服务器
| |
| --------- FIN, Seq=u ------------> |
| |
| <--------- ACK, Seq=v, Ack=u+1 ----|
| |
| <--------- FIN, Seq=w ------------ |
| |
| --------- ACK, Seq=u+1, Ack=w+1 -->|
| |
- 解释:
-
第一次挥手:客户端发送FIN:
- 客户端发送一个FIN(终止)包,表示客户端要关闭连接,并且包含当前序列号(Seq=u)。
-
第二次挥手:服务器回应ACK:
- 服务器收到FIN包后,回应一个ACK包,确认客户端的FIN包(Ack=u+1),此时客户端到服务器的连接关闭,但服务器到客户端的连接仍然存在。
-
第三次挥手:服务器发送FIN:
- 服务器也发送一个FIN包,表示服务器也要关闭连接,并且包含当前序列号(Seq=w)。
-
第四次挥手:客户端回应ACK:
- 客户端收到服务器的FIN包后,回应一个ACK包,确认服务器的FIN包(Ack=w+1),此时整个连接正式关闭。
这些流程图展示了TCP协议在建立和关闭连接时的具体步骤,帮助理解TCP三次握手和四次挥手的机制。这样,你可以更直观地看到每一步是如何进行的,以及每一步的作用。
1.5 详细介绍一下http协议
HTTP(HyperText Transfer Protocol,超文本传输协议)是用于分布式、协作和超媒体信息系统的应用层协议,是万维网数据通信的基础。HTTP起初由蒂姆·伯纳斯-李(Tim Berners-Lee)为万维网设计,现由互联网工程任务组(IETF)和万维网联盟(W3C)共同维护。HTTP协议定义了浏览器(客户端)与Web服务器之间的通信规则。
HTTP协议的主要特点
-
简单快速:
- 客户端向服务器请求服务时,只需传送请求方法和路径。
- 由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
-
灵活:
- HTTP允许传输任意类型的数据对象。通过Content-Type头,可以表示具体的数据类型。
-
无连接:
- 无连接的意思是限制每次连接只处理一个请求。服务器处理完客户端的请求并收到客户端的应答后,就断开连接。但这种方式能节省传输时间。
-
无状态:
- HTTP协议是无状态协议,即对事务处理没有记忆能力。每次请求都是独立的,服务器不保留任何会话信息。
HTTP请求
一个HTTP请求由以下部分组成:
-
请求行:包括请求方法、请求URL和HTTP版本。
- 示例:
GET /index.html HTTP/1.1
- 示例:
-
请求头:包括各种头部信息,用于客户端向服务器传递附加信息。
-
示例:
Host: www.example.com User-Agent: Mozilla/5.0 Accept: text/html
-
-
空行:用于分隔请求头和请求体。
-
请求体:包含客户端发送给服务器的数据(仅在POST、PUT等请求方法中使用)。
HTTP请求方法
常见的HTTP请求方法包括:
- GET:请求获取指定资源。常用于请求数据。
- POST:向指定资源提交数据。常用于提交表单或上传文件。
- PUT:上传指定资源的最新内容。
- DELETE:删除指定资源。
- HEAD:类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头。
- OPTIONS:返回服务器支持的HTTP请求方法。
- PATCH:对资源进行部分修改。
HTTP响应
一个HTTP响应由以下部分组成:
-
状态行:包括HTTP版本、状态码和状态描述。
- 示例:
HTTP/1.1 200 OK
- 示例:
-
响应头:包括各种头部信息,用于服务器向客户端传递附加信息。
-
示例:
Content-Type: text/html Content-Length: 1234
-
-
空行:用于分隔响应头和响应体。
-
响应体:包含服务器返回给客户端的数据。
常见的HTTP状态码
-
1xx(信息性状态码):表示请求已被接收,继续处理。
- 100 Continue
- 101 Switching Protocols
-
2xx(成功状态码):表示请求已成功被服务器接收、理解并接受。
- 200 OK
- 201 Created
-
3xx(重定向状态码):表示需要客户端采取进一步的操作以完成请求。
- 301 Moved Permanently
- 302 Found
- 304 Not Modified
-
4xx(客户端错误状态码):表示请求包含错误或无法完成。
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
-
5xx(服务器错误状态码):表示服务器在处理请求的过程中发生了错误。
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable
HTTP/1.1 vs HTTP/2 vs HTTP/3
HTTP/1.1
- 长连接:默认使用长连接,允许多个请求/响应在同一连接上进行,减少了连接的建立和关闭次数,提高了传输效率。
- 管道化:允许客户端在收到HTTP响应之前发送多个HTTP请求,但服务器仍然会按顺序响应。
HTTP/2
- 二进制分帧:使用二进制格式传输数据,更高效和更容易解析。
- 多路复用:允许同时通过单一的HTTP/2连接发送多个请求和响应。
- 头部压缩:使用HPACK算法对头部信息进行压缩,减少带宽占用。
- 服务器推送:服务器可以主动向客户端推送资源,而不需要客户端明确请求。
HTTP/3
- 基于QUIC:HTTP/3基于QUIC协议,使用UDP传输,减少了连接建立的延迟。
- 更快的连接建立:由于QUIC在单个数据包内完成握手,大大减少了连接建立时间。
- 改进的多路复用:避免了HTTP/2中的队头阻塞问题,更高效。
HTTP安全
HTTP本身是明文传输的,容易被窃听和篡改。为了提高安全性,通常使用HTTPS(HTTP Secure),即HTTP over TLS(或SSL),加密HTTP通信,保证数据的机密性和完整性。
- HTTPS:通过使用TLS(传输层安全性)来加密数据传输,确保数据在传输过程中不被窃听和篡改。
- SSL/TLS:安全套接字层/传输层安全性,是一种加密协议,用于保护互联网通信安全。
总结来说,HTTP协议是万维网的基石,通过明确的请求和响应机制,客户端和服务器可以高效地进行通信。随着技术的发展,HTTP协议也在不断演进,以提高性能、安全性和用户体验。
二、Socket编程
2.1 创建Socket
在C++中,可以使用socket()
函数创建Socket。语法如下:
int socket(int domain, int type, int protocol);
domain
:地址族,如AF_INET
(IPv4)或AF_INET6
(IPv6)。type
:Socket类型,如SOCK_STREAM
(TCP)或SOCK_DGRAM
(UDP)。protocol
:协议,一般设为0
,由系统自动选择。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
2.2 绑定地址和端口
使用bind()
函数将Socket绑定到特定的IP地址和端口。语法如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
示例:
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
perror("socket bind failed");
exit(EXIT_FAILURE);
}
2.3 监听和接受连接
使用listen()
函数使Socket进入监听状态,并使用accept()
函数接受客户端连接。
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
示例:
if (listen(sockfd, 5) != 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
int connfd = accept(sockfd, (struct sockaddr *)&cli, &len);
if (connfd < 0) {
perror("server accept failed");
exit(EXIT_FAILURE);
}
2.4 发送和接收数据
使用send()
和recv()
函数在服务器和客户端之间传输数据。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
示例:
char buffer[1024] = {0};
recv(connfd, buffer, sizeof(buffer), 0);
send(connfd, "Hello from server", strlen("Hello from server"), 0);
2.5 关闭Socket
使用close()
函数关闭Socket。
close(sockfd);
三、简单的服务器示例
以下是一个简单的C++服务器示例,使用TCP协议,监听8080端口,并向每个连接的客户端发送一条欢迎消息。
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
int sockfd, connfd;
struct sockaddr_in servaddr, cli;
// 创建Socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
std::cerr << "Socket creation failed\n";
exit(EXIT_FAILURE);
}
// 绑定IP和端口
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
std::cerr << "Socket bind failed\n";
close(sockfd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(sockfd, 5) != 0) {
std::cerr << "Listen failed\n";
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len = sizeof(cli);
connfd = accept(sockfd, (struct sockaddr *)&cli, &len);
if (connfd < 0) {
std::cerr << "Server accept failed\n";
close(sockfd);
exit(EXIT_FAILURE);
}
// 发送欢迎消息
const char *message = "Hello from server";
send(connfd, message, strlen(message), 0);
// 关闭连接
close(connfd);
close(sockfd);
return 0;
}
四、结语
网络编程是后端开发中的重要技能,掌握基础概念和Socket编程方法是深入学习和实践的第一步。本文介绍了网络协议、IP地址、端口号以及Socket编程的基本操作。希望通过这篇文章,读者能对网络编程有一个初步的了解,并能够编写简单的网络应用程序。
在后续的文章中,我们将深入探讨高级网络编程技术,包括多线程服务器、异步I/O、网络安全等内容,敬请期待。
五、深入探讨 select
和 epoll
,以及多客户端编程
在网络编程中,处理多个客户端连接是一项常见且重要的任务。为了有效地管理多个连接,操作系统提供了多种I/O多路复用机制,如select
、poll
和epoll
。本文将详细讲解select
和epoll
,并展示如何使用它们实现多客户端编程。
5.1 select
的工作原理及使用
5.1.1 select
基本概念
select
是一种I/O多路复用机制,用于监视多个文件描述符(如Socket)上的事件,如数据可读、可写或异常状态。当任何一个文件描述符上的事件发生时,select
返回,程序可以对这些事件进行处理。
5.1.2 select
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:所有文件描述符中最大值加一。readfds
:可读事件的文件描述符集合。writefds
:可写事件的文件描述符集合。exceptfds
:异常事件的文件描述符集合。timeout
:超时时间,NULL
表示永不超时。
5.1.3 使用 select
的步骤
- 初始化文件描述符集合。
- 将需要监视的文件描述符添加到集合中。
- 调用
select
函数,等待事件发生。 - 遍历文件描述符集合,处理已发生的事件。
5.1.4 select
示例
下面是一个使用 select
实现的简单多客户端服务器示例:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#define PORT 8080
#define MAX_CLIENTS 30
int main() {
int server_fd, new_socket, client_socket[MAX_CLIENTS], max_clients = MAX_CLIENTS, activity, i, valread, sd;
int max_sd;
struct sockaddr_in address;
char buffer[1025];
fd_set readfds;
// 初始化所有客户端socket为0
for (i = 0; i < max_clients; i++) {
client_socket[i] = 0;
}
// 创建服务器socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
int addrlen = sizeof(address);
std::cout << "Listening on port " << PORT << std::endl;
while (true) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 添加服务器socket到集合
FD_SET(server_fd, &readfds);
max_sd = server_fd;
// 添加客户端socket到集合
for (i = 0; i < max_clients; i++) {
sd = client_socket[i];
if (sd > 0)
FD_SET(sd, &readfds);
if (sd > max_sd)
max_sd = sd;
}
// 等待活动事件
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
std::cerr << "select error" << std::endl;
}
// 处理新连接
if (FD_ISSET(server_fd, &readfds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
std::cout << "New connection, socket fd is " << new_socket << ", ip is: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;
// 添加新socket到客户端数组
for (i = 0; i < max_clients; i++) {
if (client_socket[i] == 0) {
client_socket[i] = new_socket;
std::cout << "Adding to list of sockets as " << i << std::endl;
break;
}
}
}
// 处理已连接的客户端的IO操作
for (i = 0; i < max_clients; i++) {
sd = client_socket[i];
if (FD_ISSET(sd, &readfds)) {
if ((valread = read(sd, buffer, 1024)) == 0) {
// 某客户端断开连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
std::cout << "Host disconnected, ip: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;
close(sd);
client_socket[i] = 0;
} else {
buffer[valread] = '\0';
std::cout << "Received message: " << buffer << std::endl;
send(sd, buffer, strlen(buffer), 0);
}
}
}
}
return 0;
}
5.2 epoll
的工作原理及使用
5.2.1 epoll
基本概念
epoll
是Linux特有的一种I/O多路复用机制,提供比 select
和 poll
更高效的事件通知。epoll
使用事件驱动模型,通过内核维护一个事件表,减少用户空间和内核空间之间的拷贝开销。
5.2.2 epoll
函数
epoll
主要包含以下三个函数:
epoll_create1
:创建一个epoll
实例。epoll_ctl
:控制epoll
实例,添加、删除或修改事件。epoll_wait
:等待事件发生,并返回已触发的事件。
5.2.3 使用 epoll
的步骤
- 创建
epoll
实例。 - 使用
epoll_ctl
函数将文件描述符添加到epoll
实例中。 - 调用
epoll_wait
函数等待事件发生。 - 处理已触发的事件。
5.2.4 epoll
示例
下面是一个使用 epoll
实现的简单多客户端服务器示例:
#include <iostream>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#define PORT 8080
#define MAX_EVENTS 10
#define MAX_CLIENTS 30
int main() {
int server_fd, new_socket, epoll_fd, event_count, i, valread;
struct sockaddr_in address;
struct epoll_event ev, events[MAX_EVENTS];
char buffer[1025];
// 创建服务器socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket为非阻塞
fcntl(server_fd, F_SETFL, O_NONBLOCK);
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
// 创建epoll实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(server_fd);
exit(EXIT_FAILURE);
}
// 添加服务器socket到epoll实例
ev.events = EPOLLIN;
ev.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
perror("epoll_ctl: server_fd");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
std::cout << "Listening on port " << PORT << std::endl;
while (true) {
// 等待事件发生
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if
(event_count == -1) {
perror("epoll_wait");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
for (i = 0; i < event_count; i++) {
if (events[i].data.fd == server_fd) {
// 处理新连接
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket == -1) {
perror("accept");
continue;
}
std::cout << "New connection, socket fd is " << new_socket << ", ip is: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;
// 设置新socket为非阻塞
fcntl(new_socket, F_SETFL, O_NONBLOCK);
// 添加新socket到epoll实例
ev.events = EPOLLIN;
ev.data.fd = new_socket;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {
perror("epoll_ctl: new_socket");
close(new_socket);
}
} else {
// 处理客户端IO操作
valread = read(events[i].data.fd, buffer, 1024);
if (valread == -1) {
perror("read");
close(events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
} else if (valread == 0) {
// 客户端断开连接
getpeername(events[i].data.fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
std::cout << "Host disconnected, ip: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;
close(events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
} else {
buffer[valread] = '\0';
std::cout << "Received message: " << buffer << std::endl;
send(events[i].data.fd, buffer, strlen(buffer), 0);
}
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}
5.3、select
和 epoll
的比较
5.3.1 select
的优缺点
优点:
- 跨平台,几乎在所有操作系统上都支持。
- 简单易用,适合小规模应用。
缺点:
- 每次调用
select
都需要重新设置文件描述符集合,效率较低。 - 支持的文件描述符数量有限(一般为1024)。
5.3.2 epoll
的优缺点
优点:
- 高效,支持大量并发连接,适用于高并发场景。
- 事件驱动模型,减少了无效的系统调用。
缺点:
- 仅支持Linux,跨平台性较差。
- 相对复杂,需要更多的代码来管理事件。
5.4、结语
上面详细介绍了 select
和 epoll
的工作原理及其使用方法,并通过示例展示了如何实现多客户端编程。对于小规模的网络应用,可以选择简单易用的 select
;而对于高并发、高性能的应用,epoll
则是更好的选择。
通过学习和实践这些I/O多路复用技术,读者可以更好地理解和掌握网络编程中的并发处理,为构建高效稳定的网络应用奠定坚实的基础。