最常用,最简单,最普遍的 IO,但效率低。
目前,有阻塞功能的函数如下:
读阻塞: read,recv,recvfrom
写阻塞: write,send
其他: accept,connect
TCP (有连接):有发送缓存区,有接收缓存区,所以 TCP编程 使用 sendto 会发生阻塞。
UDP(无连接):没有发送缓存区,但有接收缓存区,所以 UDP编程 使用 sendto 不阻塞。
UDP 通信没有发送缓存区 是因为 UDP 是一种无连接的传输协议,它不保证数据的可靠性和顺序性。因此,UDP 通信不需要维护连接状态和缓存数据,而是将数据尽快发送出去,不关心数据是否到达目标主机或者是否按照发送顺序到达。
相比之下,TCP 是一种面向连接的传输协议,它需要维护连接状态、保证数据的可靠性和顺序性。为了实现这些功能,TCP 通信需要维护缓存区,用于存储已发送但未确认的数据、已接收但未交付的数据以及已交付但未被应用程序读取的数据。这样可以确保数据在网络中的可靠传输和正确的顺序交付。
#include
#include
int fcntl (int fd, int cmd, ... /* arg */);
功能:设置文件描述符的属性
参数:fd:文件描述符
cmd: 操作功能选项 (可以定义个变量,通过vi -t F_GETFL 来找寻功能赋值 )
F_GETFL: 获取文件描述符的状态信息
//不需要第三个参数,返回值为获取到的属性
F_SETFL: 设置文件描述符的状态信息 - 需要填充第三个参数
//需要填充第三个参数 O_RDONLY, O_RDWR, O_WRONLY, O_CREAT
// O_NONBLOCK 非阻塞,O_APPEND追加
// O_ASYNC 异步,O_SYNC 同步
// O_NOATIME 读取文件时不更新文件访问时间
arg:文件描述符的属性 如果需要设置(SET)文件描述符的状态,则需要该参数
返回值:
特殊选择:根据功能选择返回 (int 类型)
其他: 成功:0 失败: -1
1)获取文件描述符的原属性(以标准输入为例,标准输入原本具有阻塞的功能):
int flag = fcntl(0, F_GETFL); // 获取文件描述符原有信息后,保存在 flag 变量内
2)修改对应的位 为 nonblock(非阻塞,00004000)
flag |= O_NONBLOCK; ( flag = flag | O_NONBLOCK)
3)将修改好的属性写回去(将标准输入的 阻塞 改为 非阻塞)
fcntl (0, F_SETFL, flag); // 给文件描述符 添加的新属性
异步通知模式,需要底层驱动的支持。
1> 通过信号的方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO.
2> 应用程序收到信号后做异步处理即可,需要把自己的进程号告诉内核,并打开异步通知机制。
异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。接收方在自己的时间内处理通知。通常使用回调函数或事件驱动的方式实现。
// 1.设置将APP进程号提交给内核驱动
fcntl(fd, F_SETOWN, getpid()); // F_SETOWN 将进程号交给内核驱动
// getgid 获取进程号
// 2.设置异步通知
int flags;
flags = fcntl(fd, F_GETFL); // 获取原属性
flags |= O_ASYNC; // 设置异步 O_ASYNC 通知
fcntl(fd, F_SETFL, flags); // 修改属性设置
// 3.signal 捕捉 SIGIO 信号驱动(自定义信号驱动)
signal(SIGIO, handler);
F_SETOWN: 当文件描述符发生特定事件的时候,
内核会给该文件描述符的进程号发送SIGIO信号,使进程能够及时处理这些事件
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数(注册信号)
参数:int signum:要处理的信号(要修改的信号)
sighandler_t handler: 函数指针: void(*handler)(int)
SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号。
handler:------void handler(int num) 自定义的信号处理函数指针
返回值: 成功:设置之前的信号处理方式
失败:SIG_ERR
操作鼠标设备,当有输入的时候获取输入数据,没有输入时循环输出“Hola~”。
#include
#include
#include
#include
#include
#include
int fd;
void handler(int arg){
char buf[256] = {};
int len = read(fd, buf, sizeof(buf)-1);
buf[len] = '\0';
printf("Mouse: %s\n", buf);
sleep(2);
}
int main(int argc, char const *argv[])
{
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0){
perror("Failed to open");
return -1;
}
fcntl(fd, F_SETOWN, getpid()); // 将文件描述符的进程号告知底层驱动
int flag = fcntl(fd, F_GETFL); // 设置文件描述符的异步属性
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
while (1){
printf("Hola~\n");
signal(SIGIO, handler); // signal 捕捉 SIGIO 信号驱动
sleep(2);
}
close(fd);
return 0;
}
有三种方式: select、poll、epoll。
1、一个进程最多能监听 1024 个文件描述符( 0 - 1023),个数可通过内核源码修改
2、select 被唤醒之后要重新轮循一遍驱动(0 到 x, x ∈ \isin ∈(0, 1023]),效率低 且 消耗 CPU资源
3、select 每次都会清空表,每次都需要拷贝用户空间的表到内核空间,效率低 且 消耗 CPU资源
(0~3G 的用户态 和 3G~4G 的内核态,两个状态来回切换 、拷贝 是非常耗时、耗资源的)
1、头文件检测 1024 个文件描述符(0-1023)
2、在 select 中 0~2 存储标准输入、标准输出、标准出错
3、监测的最大文件描述个数为 fd+1(如果 fd = 3,则最大为 4)
4、产生事件的文件描述符被置1;没有产生事件的描述符被置0(轮循)
5、select 每次轮循都会清空表(将置零的描述符清空) // 需要在 select 前备份临时表
1)先构造一张有关文件描述符的表(集合、数组); fd_set readfds, tempfds;
2)清空表; FD_ZERO(&readfds);
3)将关心的文件描述符添加到这个表中; FD_SET(fd, &readfds); int maxfd = last_fd;
4)备份表; tempfds = readfds;
5)调用 select 函数,当这些文件描述符已准备好进行 I/O 操作的时候,函数才返回(阻塞);
select(maxfd+1, &tempfds, NULL, NULL, NULL);
6)判断是哪一个或哪些文件描述符产生了事件(IO操作),做对应的逻辑处理;
if (FD_ISSET(fd, &tempfds)){…}
/* According to POSIX.1-2001, POSIX.1-2008 */
#include
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,\
fd_set *exceptfds, struct timeval *timeout);
功能:监测哪些文件描述符产生事件,阻塞等待产生.
参数: nfds: 监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1)
readfds: 读事件集合; // 键盘鼠标的输入,客户端连接都是读事件
writefds: 写事件集合; // NULL表示不关心
exceptfds: 异常事件集合; // NULL表示不关心
timeout: 超时检测 1 // 如果不做超时检测:传 NULL
超时时间检测: 设定好时间,当程序执行到该语句时,如果规定时间内未完成函数功能,
返回一个超时的信息,可以根据该信息设定相应需求;
返回值: < 0 出错; > 0 表示有事件产生;
如果设置了超时检测时间:&tv
< 0 出错; > 0 表示有事件产生; == 0 表示超时,时间已到;
结构体如下:
struct timeval {
long tv_sec; // 以秒为单位,指定等待时间
long tv_usec; // 以毫秒为单位,指定等待时间
};
void FD_CLR (int fd, fd_set *set); // 将 set 集合中的 fd 清除掉
int FD_ISSET (int fd, fd_set *set); // 判断 fd 是否在set集合中产生事件
void FD_SET (int fd, fd_set *set); // 将fd加入到集合中
void FD_ZERO (fd_set *set); // 清空集合
输入鼠标响应鼠标事件, 输入键盘响应键盘事件。
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
int fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0){
perror("Failed to open");
return -1;
}
fd_set readfds, tempfds; // 1. 先构造一张有关文件描述符的表
FD_ZERO(&readfds); // 2. 清空表
FD_SET(0, &readfds); // 3. 将关心的文件描述符添加到表中
FD_SET(fd, &readfds);
int maxfd = fd;
char buf[256] = {};
while (1){
tempfds = readfds; // 4. 备份表
select(maxfd+1, &tempfds, NULL, NULL, NULL); // 5. 调用 select 函数
if (FD_ISSET(0, &tempfds)){ // 6. 产生事件,进行相应处理
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
printf("Keyboard: %s\n", buf);
}
if (FD_ISSET(fd, &tempfds)){
int len = read(fd, buf, sizeof(buf));
buf[len] = '\0';
printf("Mouse: %s\n", buf);
}
}
return 0;
}
同时检测 键盘输入 和 sockfd 事件 —— TCP实现同时连接多个客户端。
// ser_sele_ks.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
if (argc != 2){
printf("Please input %s . \n" , argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_port = htons(atoi(argv[1]));
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
if(listen(sockfd, 6) < 0){
perror("Failed to listen");
return -1;
}
fd_set readfd, tempfd;
FD_ZERO(&readfd);
FD_SET(0, &readfd);
FD_SET(sockfd, &readfd);
int maxfd = sockfd;
char buf[256] = {};
int length = sizeof(caddr);
int accfd;
while (1){
tempfd = readfd;
select(maxfd+1, &tempfd, NULL, NULL, NULL);
if (FD_ISSET(0, &tempfd)){
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] == '\0';
// printf("%s\n", buf);
for (int i = 4; i <= maxfd; i++)
if (FD_ISSET(i, &readfd))
send(i, buf, sizeof(buf), 0);
}
if (FD_ISSET(sockfd, &tempfd)){
accfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if (accfd < 0){
perror("Failed to accept");
return -1;
}
printf("Client IPv4: %s\t\tport:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
FD_SET(accfd, &readfd);
if (maxfd < accfd)
maxfd = accfd;
}
for (int i = 4; i <= maxfd; i++){
if (FD_ISSET(i, &tempfd)){
int receiver = recv(accfd, buf, sizeof(buf), 0);
if (receiver < 0){
perror("Failed to receive (server)");
return -1;
} else if (receiver == 0){
printf("Client[%d] exited. \n", i);
close(i);
FD_CLR(i, &readfd);
if(i == maxfd)
maxfd--;
} else {
printf("No_%d: %s", i, buf); // 自己打印
for(int j = 4; j <= maxfd; j++){ // 转发给除自己以外其他端
if (j == i)
continue;
if (FD_ISSET(j, &readfd))
send(j, buf, sizeof(buf), 0);
}
}
}
}
}
close(sockfd);
return 0;
}
// cli_sele_ks.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
if (argc != 3){
printf("Please input %s . \n" , argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(argv[1]);
saddr.sin_port = htons(atoi(argv[2]));
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to connect");
return -1;
}
fd_set readfd, tempfd;
FD_ZERO(&readfd);
FD_SET(0, &readfd);
FD_SET(sockfd, &readfd);
int maxfd = sockfd;
char buf[256] = {};
while (1){
tempfd = readfd;
select(maxfd+1, &tempfd, NULL, NULL, NULL);
if (FD_ISSET(0, &tempfd)){
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] == '\0';
send(sockfd, buf, sizeof(buf), 0);
}
if (FD_ISSET(sockfd, &tempfd)){
int receiver = recv(sockfd, buf, sizeof(buf), 0);
if (receiver < 0){
perror("Failed to receive (client)");
return -1;
} else {
printf("Server: %s", buf);
}
}
}
close(sockfd);
return 0;
}
1、取消 select 对文件描述符个数的限制;
(根据 poll 函数 的第一个参数来定,如果监听的事件为 n 个,则结构体数组元素个数为 n)
2、poll 被唤醒之后需要重新轮循一遍驱动,效率比较低(消耗CPU);
3、poll 不需重新构造文件描述符表(也不需清空表),只需要从用户空间向内核空间拷贝一次数据
(效率相对比较高)。
1)先创建 结构体数组; struct pollfd fds[N];
2)添加文件描述符以及触发方式; fds[i].fd = 文件描述符; fds[i].events = 宏事件;
3)保存数组内最后一个有效元素的下标; int last = …;
4)调用 poll 函数; poll(fds, last+1, -1);
5)判断表内的文件描述符是否被触发; if(fds[i].revents == 宏事件){…}
6)不同的文件描述符触发不同事件。 if(fds[i].fd == 文件描述符1){…} else if {…} …
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能: 监视并等待多个文件描述符的属性变化
参数:
1. struct pollfd *fds: 关心的文件描述符数组,大小自己定义
若想检测的文件描述符较多,则建立结构体数组 struct pollfd fds[N];
struct pollfd{
int fd; // 文件描述符
short events; // 等待的事件触发条件----POLLIN(读事件),POLLOUT(写)
short revents; // 实际发生的事件(未产生事件: 0 ))
}
2. nfds: 最大文件描述符个数
3. timeout: 超时检测 (毫秒级):1000 == 1s 如果-1,阻塞 如果0,不阻塞
返回值: < 0 出错 > 0 表示有事件产生;
如果设置了超时检测:&tv,
< 0 出错 > 0 表示有事件产生; == 0 表示超时时间已到;
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
if (argc != 2){
printf("Please input %s . \n" , argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
printf("sockfd: %d\n", sockfd);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
if (listen(sockfd, 6) < 0){
perror("Failed to listen");
return -1;
}
struct pollfd fds[64] = {}; // int last = -1;
fds[0].fd = 0; // fds[++last].fd = 0;
fds[0].events = POLLIN; // fds[last].events = POLLIN;
fds[1].fd = sockfd; // fds[++last].fd = sockfd;
fds[1].events = POLLIN; // fds[last].events = POLLIN;
int last = 1; // 此时 last == 1,此句省略
char buf[256] = {};
int accfd, receiver;
struct sockaddr_in caddr;
int length = sizeof(caddr);
while (1){
int getpoll = poll(fds, last+1, -1);
if (getpoll < 0){
perror("Failed to poll");
return -1;
}
for (int i = 0; i < last+1; i++){
if (fds[i].revents == POLLIN){
if (fds[i].fd == 0){
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
printf("Keyboard: %s\n", buf);
}
else if (fds[i].fd == sockfd){
accfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if (accfd < 0){
perror("Failed to accept");
return -1;
}
printf("Client IPv4: %s\t\tport:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
last++;
fds[last].fd = accfd;
fds[last].events = POLLIN;
}
else if (fds[i].fd == accfd){ // 可以写 else
receiver = recv(accfd, buf, sizeof(buf), 0); // recv(fds[i].fd...
if (receiver < 0){
perror("Failed to receive");
return -1;
} else if (receiver == 0){
printf("Client[%d] exited. \n", i);
close(fds[i].fd);
fds[i] = fds[last];
i--;
last--;
}
else {
printf("Client[%d]: %s\n", i, buf);
}
}
}
}
}
close(sockfd);
return 0;
}
所支持的文件描述符上限是系统可以最大打开的文件的数目,1GB 的机器上,上限在10万左右。
每个产生事件的 fd 会主动调用 callback 回调函数,不需要轮循。
1、红黑树: 是特殊的二叉树(每个节点带有属性),Epoll 怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个 fd,以结构体的形式存储(节点里面包含了 callback 函数)。
2、链表: 当某一个文件描述符产生事件后,会自动调用 callback函数,通过 callback 回调函数来找到链表对应的事件(读事件还是写事件),链表为事件链表。
1)创建红黑树(根节点); int epfd = epoll_create(999);
2)添加属性和文件描述符到树上;
struct epoll_event ev;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev);
4)阻塞等待事件的产生,一旦产生事件,则进行处理;
struct epoll_event eves[N];
int num = epoll_wait(epfd, struct epoll_event eves, N, -1);
5)对 链表 中相应的文件描述符 进行处理; if (eves[i].data.fd == 文件描述符){…}
#include
int epoll_create(int size);
功能:创建红黑树根节点(创建 epoll实例)
参数:从LINUX 2.6.8开始,该参数被忽略,大于 0 即可
返回值: 成功:epoll文件描述符,
失败:返回 -1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制 epoll属性
参数:
1. epfd: epoll_create函数 的返回句柄。 // 一个标识符
2. op: 表示动作类型,有三个宏:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中
EPOLL_CTL_MOD:修改已注册 fd 的监听事件
EPOLL_CTL_DEL:从 epfd 中删除一个 fd
3. 要操作的文件描述符
4. typedef union epoll_data {
int fd; // 要添加的文件描述符
uint32_t u32; // typedef unsigned int
uint64_t u64; // typedef unsigned long int
} epoll_data_t;
struct epoll_event {
uint32_t events; 事件
epoll_data_t data; //共用体(看上面)
};
event事件:
EPOLLIN: 表示对应文件描述符可读
EPOLLOUT: 可写
EPOLLPRI: 有紧急数据可读;
EPOLLERR: 错误;
EPOLLHUP: 被挂断;
EPOLLET: 触发方式,边缘触发;(默认使用边缘触发)
ET模式: 表示状态的变化;
NULL: 删除一个文件描述符使用,无事件
返回值: 成功:0,
失败:-1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件的产生,类似于select的用法
参数: epfd: 句柄;
events: 用来保存从链表中拿取响应事件的集合;
maxevents: 表示每次在链表中拿取响应事件的个数;
timeout: 超时时间(毫秒),0 立即返回;-1 阻塞
返回值: 成功:实际从链表中拿出的数目
失败:-1
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
if (argc != 2){
printf("Please input %s . \n" , argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
printf("sockfd: %d\n", sockfd);
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int length = sizeof(caddr);
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
if (listen(sockfd, 6) < 0){
perror("Failed to listen");
return -1;
}
int epfd = epoll_create(999);
if (epfd < 0){
perror("Failed to epcreate");
return -1;
}
struct epoll_event ev, eves[16];
ev.data.fd = 0;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev);
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
char buf[256] = {};
while (1){
int num = epoll_wait(epfd, eves, 16, -1);
if (num < 0){
perror("Failed to epwait");
return -1;
}
for (int i = 0; i < num; i++){
if (eves[i].data.fd == 0){
fgets(buf, sizeof(buf), stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
printf("%s\n", buf);
}
else if (eves[i].data.fd == sockfd){
int accfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if(accfd < 0){
perror("accept is err:");
return -1;
}
printf("Client IPv4: %s\t\tport: %d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
ev.data.fd = accfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, accfd, &ev);
}
else {
int receiver = recv(eves[i].data.fd, buf, sizeof(buf), 0);
if (receiver < 0){
perror("Failed to receive");
return -1;
} else if (receiver == 0){
printf("Client[%d] exited. \n", eves[i].data.fd);
close(eves[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, eves[i].data.fd, NULL);
} else {
printf("Client[%d]: %s\n", eves[i].data.fd, buf);
}
}
}
}
close(sockfd);
return 0;
}