目录
阻塞式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 64
char 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>
#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 0
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#else
saddr.sin_addr.s_addr = INADDR_ANY;
#endif
if (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");
while (1)
{
// 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));
// 5.发送
#define N 64
char buf[N];
while (1)
{
// 第一步:建表初始化
fd_set readfds, tempfds;
FD_ZERO(&readfds);
// 第二步:填表
FD_SET(0, &readfds);
FD_SET(acceptfd, &readfds);
// 第三步:监听表
while (1)
{
memset(buf, 0, N);
tempfds = readfds;
int ret = select(5, &tempfds, NULL, NULL, NULL);
// 第四步:判断,操作
if (ret == -1)
{
perror("select失败");
return -1;
}
if (FD_ISSET(0, &tempfds))
{
scanf("%s", buf);
send(acceptfd, buf, N, 0);
}
if (FD_ISSET(acceptfd, &tempfds))
{
int ret = recv(acceptfd, buf, N, 0);
printf("客户端:%s\n", buf);
}
}
}
close(sockfd);
return 0;
}
}