目录
一、相关概念简介
1、网络I/O
网络I/O指的是在网络编程中,数据在网络中的输入与输出过程,主要涉及到数据的发送与接收。在Linux系统中,有几种常用的I/O模型,包括阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O和异步I/O。其中,I/O复用是非常关键的一种技术,它允许单个线程同时监视多个文件描述符,以检查一个或多个文件描述符是否就绪(即它们是否已准备好进行非阻塞性读或写操作)。
2、Select
select
是最初UNIX系统支持的I/O多路复用接口。它允许程序监视多个文件描述符,等待直到一个或多个文件描述符就绪(可读、可写或异常),或者直到超时。
优点:
- 简单易用,广泛支持于各种操作系统。
缺点:
- 文件描述符数量受限于
FD_SETSIZE
,通常是1024。 - 每次调用
select
时,都需要重新传入文件描述符集合,这增加了开销。 - 内部实现使用线性结构存储文件描述符,因此每次都需要遍历整个集合,效率低下。
3、Poll
poll
函数与select
类似,但它没有文件描述符数量的限制,因为它使用了一种不同的方式来存储和查找监视的文件描述符。
优点:
- 不受文件描述符数量限制。
- 与
select
相比,管理文件描述符的方式更灵活。
缺点:
- 虽然解决了文件描述符数量的限制,但在文件描述符多的情况下,性能仍然不是很高,因为它依然需要遍历所有文件描述符。
4、Epoll
epoll
是Linux特有的I/O复用机制,性能比select
和poll
更高。它不仅支持大量的文件描述符,而且只会激活那些真正发出I/O通知的描述符。
优点:
- 支持的文件描述符数量远大于
select
。 - 使用事件通知方式,只处理活跃的文件描述符,效率高。
- 文件描述符的添加、修改和删除都有对应的API,管理更高效。
缺点:
- 仅在Linux系统上可用。
这三种技术各有利弊,选择哪一种主要取决于应用程序的需求和运行环境。对于需要处理大量连接的高性能服务器应用,epoll
通常是最佳选择。
二、代码实践
1、网络I/O
1)基础搭建
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main()
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(socketfd, 10);
printf("listen finshed\n");
getchar();
printf("exit\n");
return 0;
}
使用gcc编译运行
travis@Travis-Ubuntu:~/share/network-io$ gcc -o networkio networkio.c
travis@Travis-Ubuntu:~/share/network-io$ ./networkio
listen finshed
终端输入指令,查看端口2000的状态
netstat -anop | grep 2000
终端输入信息:
travis@Travis-Ubuntu:~/share/network-io$ netstat -anop | grep 2000
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp 1 0 0.0.0.0:2000 0.0.0.0:* LISTEN 8340/./networkio 关闭 (0.00/0/0)
此时再开一个终端再次运行将会绑定失败,输入如下信息:
travis@Travis-Ubuntu:~/share/network-io$ ./networkio
bind failed: Address already in use
listen finshed
总结:
- 端口被绑定之后不能再次被绑定
- 执行了listen,可以通过netstat看见端口的状态
- 进入listen就可以被连接,并且会产生新连接状态
- io与tcp连接
2)io与tcp连接关系
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main()
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(socketfd, 10);
printf("listen finshed\n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV: %s\n", buffer);
send(clientfd, buffer, count, 0);
getchar();
printf("exit\n");
return 0;
}
编译运行,当前代码会阻塞在accept, 因为 accept 是一个阻塞调用,它会等待客户端的连接。如果没有客户端尝试连接到服务器,程序就会在 accept 调用处停止执行,直到一个连接到来。
注意:(监听)在调用 accept 之前,需要在 socketfd 上调用 listen 函数,使其能够接受来自客户端的连接请求。否则accept将执行错误,不会阻塞。
使用NetAssist网络调试助手连接后,再次查看端口2000信息,会发现多出一条信息,
travis@Travis-Ubuntu:~/share/network-io$ netstat -anop | grep 2000
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp 0 0 0.0.0.0:2000 0.0.0.0:* LISTEN 9768/./networkio 关闭 (0.00/0/0)
tcp 0 0 192.168.1.132:2000 192.168.1.108:50385 ESTABLISHED 9768/./networkio 关闭 (0.00/0/0)
这里第一个对应的是socketfd,第二个对应的是clientfd 。
总结:fd与tcp连接信息一对一的关系
一请求一线程的方式
为了连接多个客户端,需要多次调用accept,为每一个clientfd单独开一个线程去读取接收信息
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
void *client_thread(void *arg)
{
int clientfd = *(int *)arg;
while (1)
{
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
if (count == 0)
{
// 客户端关闭了连接
printf("Client closed the connection\n");
break;
}
else if (count < 0)
{
// 发生错误
perror("Error receiving data");
break;
}
send(clientfd, buffer, count, 0);
}
close(clientfd); // 确保关闭客户端文件描述符
return NULL;
}
int main()
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(socketfd, 10);
printf("listen finshed\n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
#if 0
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV: %s\n", buffer);
send(clientfd, buffer, count, 0);
#elif 0
while (1)
{
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
char buffer[1024] = {0};
int count = recv(clientfd, buffer, 1024, 0);
printf("RECV: %s\n", buffer);
send(clientfd, buffer, count, 0);
}
#else
while (1)
{
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
pthread_t thid;
pthread_create(&thid, NULL, client_thread, &clientfd);
}
#endif
getchar();
printf("exit\n");
return 0;
}
在clientfd正常连接状态,每个线程中循环会阻塞在recv函数中。
当连接关闭时,recv
函数会停止阻塞并返回 0
。这意味着没有数据被接收,且连接已经被对端(如客户端)正常关闭。在这种情况下,你的服务器线程应该识别到这个返回值,合适地处理这种情况,通常是通过结束循环并执行必要的清理工作,比如关闭线程使用的套接字。
在阻塞模式下,recv
主要在以下几种情况中停止阻塞:
- 数据接收:收到数据,
recv
读取这些数据并返回读取的字节数。 - 连接关闭:对端关闭连接,
recv
返回0
。 - 错误发生:如果发生接收错误(例如因网络问题),
recv
返回-1
并设置errno
以指示错误类型。
因此,当你的应用程序检测到 recv
返回 0
,这通常意味着应当终止对该连接的进一步读取和写入操作,关闭套接字,并适当地管理线程的退出。
2、select
1)函数原型
select
的函数原型定义在 <sys/select.h>
头文件中,其基本形式如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
2)参数解释
- nfds: 监视的文件描述符的最大数目加一。通常设为最大文件描述符编号加一。
- readfds: 指向
fd_set
结构的指针,该结构指定哪些文件描述符需要检测读取就绪状态。 - writefds: 指向
fd_set
结构的指针,该结构指定哪些文件描述符需要检测写入就绪状态。 - exceptfds: 指向
fd_set
结构的指针,该结构指定哪些文件描述符需要检测异常条件。 - timeout: 指向
timeval
结构的指针,用于指定等待就绪文件描述符的最大时间。如果为NULL
,则无限等待。
3
)fd_set
操作
select
使用 fd_set
数据结构来管理文件描述符集合。有几个宏用于操作 fd_set
:
FD_ZERO(fd_set *set)
: 初始化文件描述符集合,将集合清空。FD_SET(int fd, fd_set *set)
: 将指定的文件描述符加入集合。FD_CLR(int fd, fd_set *set)
: 将指定的文件描述符从集合中删除。FD_ISSET(int fd, fd_set *set)
: 检查指定的文件描述符是否在集合中。
4)示例代码
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(socketfd, 10);
printf("listen finshed: %d\n", socketfd);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(socketfd, &rfds);
int maxfd = socketfd; // fd集合的最大值
while (1)
{
rset = rfds;
int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(socketfd, &rset)) // accept
{
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd)
maxfd = clientfd;
}
// recv
int i = 0;
for (i = socketfd + 1; i <= maxfd; i++)
{
if (FD_ISSET(i, &rset))
{
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0)
{
// 客户端关闭了连接
printf("Client closed the connection: %d\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
else if (count < 0)
{
// 发生错误
perror("Error receiving data");
break;
}
send(i, buffer, count, 0);
}
}
}
getchar();
printf("exit\n");
return 0;
}
- 每次調用需要把fd_set集合, 从用户空间copy到内核空间
- maxfd,遍历到最大的maxfd for(int i = 0; i < maxfd + 1; i++)
3、poll
1)函数原型
poll
函数的原型定义在 <poll.h>
头文件中,其基本形式如下:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2)参数解释
- fds: 指向一个
pollfd
结构数组的指针,每个结构体用于指定监视的文件描述符和感兴趣的事件。 - nfds: 指定数组
fds
中结构体的数量,告诉poll
监视多少个文件描述符。 - timeout: 指定等待事件发生的超时时间(以毫秒为单位)。如果设置为 -1,则无限等待直到某个事件发生。设置为 0 则表示非阻塞模式,即
poll
调用立即返回,不管是否有事件发生。
3
)pollfd
结构体
pollfd
结构体定义了要监视的文件描述符和事件类型,其定义如下:
struct pollfd {
int fd; // 文件描述符
short events; // 监视的事件
short revents; // 实际发生的事件,由 poll() 填充
};
4)事件类型
- POLLIN: 数据可读。
- POLLOUT: 数据可写。
- POLLERR: 错误条件。
- POLLHUP: 挂起条件。
- POLLPRI: 有紧急数据可读。
5)示例代码
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>
int main()
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(socketfd, 10);
printf("listen finshed: %d\n", socketfd);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
struct pollfd fds[1024] = {0};
fds[socketfd].fd = socketfd;
fds[socketfd].events = POLLIN;
int maxfd = socketfd;
while (1)
{
int nready = poll(fds, maxfd + 1, -1);
if (fds[socketfd].revents & POLLIN)
{
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd)
maxfd = clientfd;
}
int i = 0;
for (i = socketfd + 1; i < maxfd + 1; i++)
{
if (fds[i].revents & POLLIN)
{
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0)
{
// 客户端关闭了连接
printf("Client closed the connection: %d\n", i);
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
else if (count < 0)
{
// 发生错误
perror("Error receiving data");
break;
}
send(i, buffer, count, 0);
}
}
}
getchar();
printf("exit\n");
return 0;
}
4、epoll
1)epoll_create
- 功能:创建一个
epoll
的实例。 - 参数:指定
epoll
实例能处理的最大文件描述符数量。 - 返回值:返回一个文件描述符,这个描述符用于所有后续对
epoll
接口的调用。 - 实例代码:
int epfd = epoll_create(256); // 创建一个新的epoll实例,可以监控最多256个描述符
2)epoll_ctl
- 功能:用于控制某个文件描述符上的事件,可以注册、修改或删除。
- 参数:
epfd
:epoll_create
返回的文件描述符。op
:要进行的操作,如EPOLL_CTL_ADD
,EPOLL_CTL_MOD
,EPOLL_CTL_DEL
。fd
:相关联的文件描述符。event
:指向epoll_event
结构的指针,该结构指定了对应的事件类型和用户数据。
- 返回值:成功时返回 0,失败时返回 -1。
- 示例代码:
struct epoll_event ev; ev.events = EPOLLIN; // 监控输入事件 ev.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 添加 sockfd 到 epoll 实例中
3)epoll_wait
- 功能:等待注册的文件描述符上的事件发生。
- 参数:
epfd
:epoll_create
返回的文件描述符。events
:用于从内核得到事件的集合。maxevents
:告诉内核这个events
数组可以接收多少事件。timeout
:等待的超时时间(毫秒),如果设置为 -1 表示无限等待。
- 返回值:有事件发生的文件描述符数量,0 表示超时,-1 表示错误。
- 示例代码:
struct epoll_event events[20]; int nfds = epoll_wait(epfd, events, 20, 500); // 等待直到有事件发生或500毫秒超时
4)epoll_event 结构
epoll_event
结构定义了事件类型和用户数据:
struct epoll_event {
uint32_t events; // Epoll 事件
epoll_data_t data; // 用户数据
};
5)事件类型
epoll
支持多种事件类型,包括:
- EPOLLIN:表示对应的文件描述符可读(包括普通文件、TCP套接字等)。
- EPOLLOUT:表示对应的文件描述符可写。
- EPOLLERR:表示对应的文件描述符出现错误。
- EPOLLHUP:表示对应的文件描述符被挂断。
- EPOLLET:设置边缘触发模式,事件只会被报告一次。
- EPOLLONESHOT:一个文件描述符只通知其注册的事件一次,直到重新充使。
6)示例代码
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>
#include <sys/epoll.h>
int main()
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(2000); // 0-1023
if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(socketfd, 10);
printf("listen finshed: %d\n", socketfd);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = socketfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, socketfd, &ev);
while (1)
{
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (i = 0; i < nready; i++)
{
int connfd = events[i].data.fd;
if (connfd == socketfd)
{
int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
}
else if (events[i].events & EPOLLIN)
{
char buffer[1024] = {0};
int count = recv(connfd, buffer, 1024, 0);
if (count == 0)
{
// 客户端关闭了连接
printf("Client closed the connection: %d\n", connfd);
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev);
continue;
}
else if (count < 0)
{
// 发生错误
perror("Error receiving data");
break;
}
printf("RECV: %s\n", buffer);
count = send(connfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
getchar();
printf("exit\n");
return 0;
}
1.整集选择什么数据结构存储
2.选择什么数据结构做就绪