目录
阻塞式IO(BIO)
特点
阻塞原因与阻塞反应
TCP流式套接字缓冲区
非阻塞式IO(NIO)
特点
设置非阻塞
1.通过对参数的修改实现
2.通过对文件描述符的属性进行设置 fcntl
信号驱动IO (异步IO模型)
IO多路复用 select、poll、epoll
IO多路复用机制
1.select
2.fd_set表的结构与机制
3.流程
阻塞式IO(BIO)
特点
最简单、最常用;但是相对于进程来说效率低
阻塞原因与阻塞反应
1.当程序调用某些接口时,如果期望的动作无法触发,那么进程会进入阻塞态(等待状态,让出CPU的调度),当期望动作可以被触发了,那么会被唤醒,然后处理事务。
2.阻塞I/O模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O,缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O模式。
3.之前学习的很多读写函数在调用过程中会发生阻塞。例如:
(1)读操作中的read、recv、recvfrom
读阻塞——>需要读缓冲区中有数据可读,读阻塞解除
(2)写操作中的write、send
写阻塞——>阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。
注意:sendto没有写阻塞
无sendto函数的原因:
sendto不是阻塞函数,本身udp通信不是面向连接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。
UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。
其他阻塞函数操作:accept、connect
TCP流式套接字缓冲区
非阻塞式IO(NIO)
特点
可以处理多路IO;但是需要轮询,浪费CPU资源
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试一个文件描述符是否有数据可读,这步操作叫做轮询(polling)。
应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
这种模式使用中不普遍。
设置非阻塞
1.通过对参数的修改实现
例如recv()最后的参数:
char buf[N];while (1){memset(buf, 0, N);int ret = recv(acceptfd, buf, N, 0);//当是0的时候阻塞,当是MSG_DONTWAIT的时候非阻塞if (ret < 0){if (errno == 11){printf("读缓存区内没数据\n");continue;}else{perror("recv失败\n");return -1;}}else if (ret == 0){printf("客户端ip:%s退出\n", inet_ntoa(caddr.sin_addr));close(acceptfd);break;}else{printf("客户端%s\n", buf);}}
2.通过对文件描述符的属性进行设置 fcntl
由于阻塞基本都是对有缓冲区的文件进行操作导致的,修改文件描述符的属性,就可以使read等的时候实现非阻塞。
#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ...);
功能:
获取/改变文件属性(linux中一切皆文件)
参数:
fd:文件描述符
cmd:设置的命令
F_GETFL //获取文件的属性
F_SETFL //设置文件的属性
第三个参数:由第二个参数决定,set时候写需要设置的值,get时候写0返回值:
成功:文件状态标志(文件的属性)
失败: -1
置0:目标位和0相与,其他位和1相与
置1:目标位和1相或,其他位和0相或
设置阻塞还是非阻塞
int flag; //文件状态的标志
flag = fcntl(fd, F_GETFL); //读 fd是文件描述符
flag |= O_NONBLOCK; //改 O_NONBLOCK = 0x00004000
fcntl(fd, F_SETFL, flag); //写
例如:标准输入非阻塞设置
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>#define N 32
int main(int argc, char const *argv[])
{int flag; flag = fcntl(0, F_GETFL); flag |= O_NONBLOCK; fcntl(0, F_SETFL, flag); char buf[N];while (1){memset(buf, 0, N);gets(buf);printf("buf:%s\n", buf);if (strlen(buf) > 0){sleep(5);}}return 0;
}
信号驱动IO (异步IO模型)
特点:异步通知模式,需要底层驱动的支持
IO多路复用 select、poll、epoll
案例分析:键盘鼠标事件
同时对键盘和鼠标进行监听,当敲击键盘按下回车,就打印键盘输入的东西,动鼠标就要打印鼠标写入的内容。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>#define N 64
int main(int argc, char const *argv[])
{int mouse = open("/dev/input/mouse0", O_RDONLY);if (mouse < 0){perror("open失败");return -1;}char buf[N];while (1){memset(buf, 0, N);gets(buf);printf("buf:%s\n", buf);int ret = read(mouse, buf, N);if (ret < 0){perror("read失败");return -1;}else{printf("mouse:%s\n", buf);}}
}
IO多路复用机制
使用I/O多路复用技术。其基本思想是:
1.先构造一张有关描述符的表,然后调用一个函数。
2.当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
3.函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。
1.select
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds(要轮询的文件描述符个数), fd_set *readfds(阻塞的读文件描述符), fd_set *writefds(阻塞的写文件描述符),fd_set *exceptfds(阻塞的异常文件描述符), struct timeval *timeout(超时时长));
一般写:select(int nfds,fd_set *readfds,NULL,NULL,NULL);
void FD_CLR(int fd, fd_set *set); //将某一文件描述符在表里去除
int FD_ISSET(int fd, fd_set *set); //判断某一文件描述符是否在表里
void FD_SET(int fd, fd_set *set); //将某一文件描述符放入表中
void FD_ZERO(fd_set *set); //将表置零
2.fd_set表的结构与机制
3.流程
第一步:建表初始化
第二步:填表
第三步:监听表
第四步:判断,操作
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>#define N 64
int main(int argc, char const *argv[])
{int mouse = open("/dev/input/mouse0", O_RDONLY);if (mouse < 0){perror("open失败");return -1;}char buf[N];// 第一步:建表初始化fd_set readfds, tempfds;FD_ZERO(&readfds);// 第二步:填表FD_SET(0, &readfds);FD_SET(mouse, &readfds);// 第三步:监听表while (1){memset(buf, 0, N);temphfds = readfds;int ret = select(4, &tempfds, NULL, NULL, NULL);// 第四步:判断,操作if (ret == -1){perror("select失败");return -1;}if (FD_ISSET(0, &tempfds)){gets(buf);printf("buf:%s\n", buf);}if (FD_ISSET(mouse, &tempfds)){int n = read(mouse, buf, N);if (n < 0){perror("read失败");return -1;}else{printf("mouse:%s\n", buf);}}}return 0;
}
例题:创建全双工客户端(既能接收也能发送)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <errno.h>int main(int argc, char const *argv[])
{// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);printf("sorkfd:%d\n", sockfd);// 2.连接unsigned short post = 0;char ip[15];printf("请输入ip地址");scanf("%s", ip);getchar();printf("请输入端口号");scanf("%hd", &post);getchar();struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(post);saddr.sin_addr.s_addr = inet_addr(ip);socklen_t addrlen = sizeof(saddr);if (connect(sockfd, (struct sockaddr *)&saddr, addrlen) < 0){perror("connect失败\n");return -1;}// 3.接收
#define N 64char buf[N];while (1){// 第一步:建表初始化fd_set readfds, tempfds;FD_ZERO(&readfds);// 第二步:填表FD_SET(0, &readfds);FD_SET(sockfd, &readfds);// 第三步:监听表while (1){memset(buf, 0, N);tempfds = readfds;int ret = select(4, &tempfds, NULL, NULL, NULL);// 第四步:判断,操作if (ret == -1){perror("select失败");return -1;}if (FD_ISSET(0, &tempfds)){scanf("%s", buf);send(sockfd, buf, N, 0);}if (FD_ISSET(sockfd, &tempfds)){int ret = recv(sockfd, buf, N, 0);printf("服务端:%s\n", buf);}}}close(sockfd);return 0;
}
例题:创建全双工服务端(既能接收也能发送)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/select.h>#define N 64
char buf[N];
#define ERR_MSG(msg) \do \{ \fprintf(stderr, "line:%d ", __LINE__); \perror(msg); \} while (0)int main(int argc, char const *argv[])
{if (argc != 2){printf("用法:<port>\n");return -1;}// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);printf("sorkfd:%d\n", sockfd);// 2.bind绑定IP和Port端口号struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));// saddr.sin_addr.s_addr = inet_addr("192.168.50.213");socklen_t addrlen = sizeof(saddr);
#if 0saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#elsesaddr.sin_addr.s_addr = INADDR_ANY;
#endifif (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0){ERR_MSG("bind失败");return -1;}printf("bind成功\n");// 3.监听listen将主动套接字变为被动套接字if (listen(sockfd, 7) < 0){ERR_MSG("lisren失败");return -1;}printf("listen成功\n");// 第一步:建表初始化fd_set readfds, tempfds;FD_ZERO(&readfds);FD_SET(sockfd, &readfds);FD_SET(0, &readfds);int max = sockfd;while (1){memset(buf, 0, N);tempfds = readfds;int ret = select(max + 1, &tempfds, NULL, NULL, NULL);if (ret == -1){perror("select失败");return -1;}if (FD_ISSET(sockfd, &tempfds)){// 4.accept阻塞等待链接int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);if (acceptfd < 0){ERR_MSG("accept失败\n");return -1;}printf("acceptfd:%d\n", acceptfd);printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));FD_SET(acceptfd, &readfds);if (max < acceptfd){max = acceptfd;}}// 5.发送else if (FD_ISSET(0, &tempfds)){scanf("%s", buf);for (int i = 4; i <= max; i++){if (FD_ISSET(i, &readfds)){send(i, buf, N, 0);}}}// 6.接收for (int n = 4; n <= max; n++){if (FD_ISSET(n, &tempfds)){int ret = recv(n, buf, N, 0);if (ret < 0){perror("recv失败");return -1;}else if (ret > 0){printf("客户端:%s\n", buf);}else{printf("客户端acceptfd:%d退出\n", n);FD_CLR(n, &readfds);close(n);while (!FD_ISSET(max, &readfds)){max--;}}}}}close(sockfd);return 0;
}