文章目录
一、常见的头文件
// myhead.h
#ifndef _MYHEAD_H
#define _MYHEAD_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/input.h> //跟输入子系统模型有关的头文件
#include <dirent.h>
#include <stdbool.h>
#include <strings.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netdb.h>
#include <poll.h>
#include <sys/epoll.h>
#endif
二、一连接一线程模型
简介:一连接一线程模型,就是为每一个客户端的连接创建一个线程来处理。
#include "myhead.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){ // 当客户端断开时,recv会返回0
printf("client disconnect: %d\n", clientfd);
close(clientfd);
break;
}
printf("RECV: %s\n", buffer); //打印数据
count = send(clientfd, buffer, count, 0);
printf("SEND: %d\n", count); //打印接收到的字节数
}
}
int main()[
// 创建服务器的套接字,AF_INIET:网际 SOCK_STREAM:字节流,代表TCP,
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servadder; //为了填入属性,用以和套接字绑定
servadder.sin_family = AF_INET;
servadder.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY: 0.0.0.0 代表任意可适配IP地址
servadder.sin_port = htons(2000); //端口号
// 绑定套接字
if( bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))== -1 ){
printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10); // 最大允许监听的客户端数量:10
printf("listen finished: %d\n", sockfd);
struct sockaddr_in clientaddr; // 存放客户端的属性,包括客户端的IP、端口
socklen_t len = sizeof(clientaddr);
while(1){
printf("accept\n");
// 服务器的套接字和客户端的套接字建立连接
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finished: %d\n", clientfd);
pthread_t thid;
// thid: 线程ID号,NULL:线程的属性,client_thread:任务函数,clientfd:函数参数
pthread_create(&thid, NULL, client_thread, &clientfd);
}
return 0;
}
优点:代码结构简单清晰;
缺点:不利于并发(试想百万并发,需要百万线程,这是非常消耗cpu的事情)
三、多路复用I/O模型
多路复用IO模型虽然也是阻塞的,但是阻塞的维度不一样,它是在一批文件描述符都没有置位的时候,才阻塞,而不是在某个套接字accept的时候,或者send、recv的时候阻塞。前者是批量处理,后者是单个处理。虽然都是同步阻塞的。但是多路复用中,我们会发现底层的accept、send、recv等变成了异步的,程序不需要阻塞在这些地方,如果需要程序处理,直接将相应的文件描述符置位通知程序即可,这明显提高了并发性。
这说明,有时候解决问题的高级手段是降维打击。
下面三个模型越来越先进
select模型
select的函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// nfds:需要检查的文件描述符的最大值加一。
// readfds:指向 fd_set 的指针,表示需要检查可读性的文件描述符集合。
// writefds:指向 fd_set 的指针,表示需要检查可写性的文件描述符集合。
// exceptfds:指向 fd_set 的指针,表示需要检查异常条件的文件描述符集合。
// timeout:指向 struct timeval 的指针,用于设置超时时间。如果设置为 NULL,则 select 将无限期地等待直到至少一个文件描述符准备好。
总结:
使用 select 函数时,通常会先设置好 fd_set 集合,然后调用 select。select 调用后,会阻塞直到以下任一条件满足:
- 至少一个在 readfds 中的文件描述符准备好读取。
- 至少一个在 writefds 中的文件描述符准备好写入。
- 至少一个在 exceptfds 中的文件描述符有异常条件。
- 超时时间到达。
select 调用返回后,可以通过 FD_ISSET 宏来检查哪些文件描述符发生了指定的I/O事件。例如,如果 readfds 中的文件描述符准备好读取,则可以使用 FD_ISSET 来检查哪些具体的文件描述符发生了可读事件,并对其进行处理。
例子:
#include "myhead.h"
int main() {
int sockfd = 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(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10);
printf("listen finshed: %d\n", sockfd); // 3
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while (1) {
rset = rfds; //这里
// 如果rset中有可读的文件描述符,就返回,没有可读,就阻塞
int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { // accept
int clientfd = accept(sockfd, (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 = sockfd+1; i <= maxfd;i ++) { // i fd
if (FD_ISSET(i, &rset)) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0) { // disconnect
printf("client disconnect: %d\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
getchar();
printf("exit\n");
return 0;
}
注释:不直接使用rfds, 新建一个 rset 是为了在每次循环中都能使用一个“干净”的文件描述符集合来调用 select 函数。这是因为在 select 函数调用之后,rset 中的文件描述符集合会被修改,以反映哪些文件描述符实际上发生了事件。如果在循环中直接使用 rfds,那么每次循环时都会基于上一次循环的状态,这可能会导致错误的行为。
poll模型
poll的函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:指向 struct pollfd 数组的指针,每个 struct pollfd 结构体用于指定一个要监视的文件描述符及其事件。
nfds:fds 数组中 struct pollfd 结构体的数量,即要监视的文件描述符的总数。
timeout:等待 I/O 事件发生的时间上限,单位是毫秒。如果设置为 -1,则 poll 将无限期地等待;如果设置为 0,则 poll 将立即返回。
poll 函数的返回值是以下几种情况之一:
大于0:表示在 fds 数组中至少有一个文件描述符准备就绪(即至少有一个事件发生)。
返回值是准备就绪的文件描述符数量。
你可以通过检查 fds[i].revents 来确定具体哪个文件描述符发生了什么事件。
0:表示在 timeout 指定的等待时间内没有文件描述符准备就绪。
如果 timeout 设置为 -1,则表示无限期等待,但这里返回 0 意味着没有事件发生。
小于0:表示发生错误。
在这种情况中,你应该检查 errno 来确定错误的类型。
struct pollfd 结构体的定义如下:
struct pollfd {
int fd; // 文件描述符
short events; // 请求的事件
short revents; // 实际发生的事件
};
events 和 revents 可以是以下常量的组合:
POLLIN:有数据可读。
POLLOUT:可以写入数据。
POLLERR:发生错误。
POLLHUP:发生挂起。
POLLNVAL:无效请求。
例子:
#include "myhead.h"
int main() {
int sockfd = 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(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10);
printf("listen finshed: %d\n", sockfd); // 3
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
// revents由系统写入
int maxfd = sockfd;
while (1) {
int nready = poll(fds, maxfd+1, -1);
if (fds[sockfd].revents & POLLIN) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
//FD_SET(clientfd, &rfds); //
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = sockfd+1; i <= maxfd;i ++) { // i fd
if (fds[i].revents & POLLIN) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0) { // disconnect
printf("client disconnect: %d\n", i);
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
getchar();
printf("exit\n");
return 0;
}
epoll模型
以下是使用 epoll 的基本步骤:
-
创建 epoll 实例:
使用 epoll_create 或 epoll_create1 函数创建一个 epoll 实例。
准备 struct epoll_event 结构体:
epoll 操作使用 struct epoll_event 结构体来指定要监视的事件和用户数据。 -
添加/修改要监视的文件描述符:
使用 epoll_ctl 函数将文件描述符添加到 epoll 实例中,并指定要监视的事件。 -
等待事件:
使用 epoll_wait 函数等待 epoll 实例上的事件。这个函数会阻塞,直到有事件发生或超时。 -
处理事件:
当 epoll_wait 返回时,遍历返回的 epoll_event 结构体数组,处理每个发生的事件。 -
重复步骤 4 和 5:
在处理完所有事件后,再次调用 epoll_wait 等待下一次事件。
#include "myhead.h"
int main() {
int sockfd = 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(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10);
printf("listen finshed: %d\n", sockfd); // 3
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int epfd = epoll_create(1); //参数无意义,传入非-1即可,历史遗留问题
struct epoll_event ev; // 属性配置
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
struct epoll_event events[1024] = {0}; // 创建时间数组,用来存放待响应的事件
// 如果epfd中有需要响应的事件,就会放置到events数组中,返回值是待响应的个数
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 == sockfd) {
int clientfd = accept(sockfd, (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) { // disconnect
printf("client disconnect: %d\n", connfd);
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
continue;
}
printf("RECV: %s\n", buffer);
count = send(connfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
getchar();
printf("exit\n");
return 0;
}
epoll_create
int epoll_create(int size);
- 返回值:成功时返回一个非负的文件描述符,指向新的
epoll
实例。出错时返回-1
并设置 errno。 - 参数:
size
:这个参数在早期的 Linux 内核中用于指示epoll
实例可以处理的文件描述符的数量。从 Linux 2.6.8 开始,这个参数被忽略,因为epoll
实例可以处理的最大文件描述符数量由内核参数/proc/sys/fs/epoll/max_user_instances
决定。
epoll_create1
int epoll_create1(int flags);
- 返回值:成功时返回一个非负的文件描述符,指向新的
epoll
实例。出错时返回-1
并设置 errno。 - 参数:
flags
:标志位,目前只支持EPOLL_CLOEXEC
。如果设置了这个标志,那么当执行execve
系统调用时,epoll
实例的文件描述符将自动关闭。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 返回值:成功时返回
0
。出错时返回-1
并设置 errno。 - 参数:
epfd
:epoll
实例的文件描述符。op
:操作码,指定要执行的操作。可以是以下之一:EPOLL_CTL_ADD
:将新的 fd 添加到 epfd 指示的epoll
实例中。EPOLL_CTL_MOD
:修改已添加的 fd 的监听事件。EPOLL_CTL_DEL
:从 epfd 指示的epoll
实例中移除 fd。
fd
:要操作的文件描述符。event
:指向epoll_event
结构体的指针,该结构体定义了要监视的事件和用户数据。
epoll_event
结构体的定义如下:
struct epoll_event {
uint32_t events; // 请求的事件
epoll_data_t data; // 用户数据
};
// epoll_data_t 是一个联合体,可以用来存储整数、指针或两者都有:
union epoll_data_t {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
events
字段可以是以下常量的组合:
EPOLLIN
:表示对应的文件描述符上有可读事件。EPOLLOUT
:表示对应的文件描述符上有可写事件。EPOLLRDHUP
:表示对端关闭连接。EPOLLPRI
:表示对应的文件描述符上有紧急数据可读。EPOLLERR
:表示对应的文件描述符上发生错误。EPOLLHUP
:表示对应的文件描述符上发生挂起。EPOLLET
:设置边缘触发(Edge Triggered)模式。(默认是水平触发,边缘触发即数据从无到有只触发一次,而水平触发阔以一直触发)EPOLLONESHOT
:设置一次性触发模式,触发后自动禁用该 fd,直到再次启用。
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 返回值:成功时返回就绪的文件描述符数量。出错时返回
-1
并设置 errno。如果超时时间到达且没有文件描述符准备好,返回0
。 - 参数:
epfd
:epoll
实例的文件描述符,由epoll_create
或epoll_create1
返回。events
:指向epoll_event
结构体数组的指针,用于接收就绪事件的详细信息。maxevents
:events
数组中可以返回的最大事件数量。这个值必须大于0
。timeout
:等待事件的超时时间,单位是毫秒。如果设置为-1
,则epoll_wait
将无限期地等待直到至少一个文件描述符准备好。如果设置为0
,则epoll_wait
将立即返回,不会等待。
这些函数是 epoll
API 的核心组成部分,它们允许用户创建和管理 epoll
实例,以及向其添加、修改或删除要监视的文件描述符及其相关事件。通过这种方式,epoll
提供了一种高效的方式来处理大量的并发 I/O 事件。
epoll模型机制详解(重要)
epoll
机制概述
- 绑定事件(注册事件):
- 当我们使用
epoll_ctl
注册一个文件描述符及其事件类型(例如EPOLLIN
,EPOLLOUT
)时,实际上是在告诉内核我们对该文件描述符的哪些事件感兴趣。 - 注册事件后,文件描述符会被添加到
epoll
实例中,并与相应的事件类型进行绑定。这一步并不会立即触发事件处理,只是设置了感兴趣的事件类型。
- 当我们使用
- 事件触发:
- 内核在监控文件描述符时,如果检测到与注册的事件类型匹配的事件(例如一个文件描述符上有可读数据,对应
EPOLLIN
事件),就会将该文件描述符标记为就绪,并将其加入到就绪队列中。 - 当我们调用
epoll_wait
时,内核会返回所有在就绪队列中的文件描述符及其触发的事件类型。
- 内核在监控文件描述符时,如果检测到与注册的事件类型匹配的事件(例如一个文件描述符上有可读数据,对应
- 事件处理:
- 我们在
epoll_wait
返回的文件描述符列表中进行遍历,检查每个文件描述符的实际触发事件类型(例如events[i].events & EPOLLIN
)。 - 根据触发的事件类型调用相应的回调函数进行处理。
- 我们在
总:这里有一个很重要的概念,就是绑定事件和事件发生是两码事,
EPOLLIN事件是因为文件描述符上有可读数据产生的。
EPOLLOUT事件是因为文件描述符的缓冲区可写时产生的。
如果你绑定了EPOLLOUT,然后文件描述符的缓冲区又可写,同时你又没有数据要发送,那么会一直无效调用回调函数,浪费资源,所以绑定和解绑EPOLLOUT的时机很重要。