1.socket 中有四种IO模型
- 阻塞型IO
- 最常用/最简单/效率低
- 函数本身不具备阻塞属性,而是由于文件描述符本身导致函数阻塞。
- 在默认情况下Linux建立的socket套接都是阻塞的
- 非阻塞
- 可以设置进程非阻塞在IO操作上,需要轮询
- 占用CPU资源较大
- 多路复用IO
- 同时对多个IO进行操作
- 可以设置在规定的时间内检测数据是否到达
- 信号驱动型IO
- 属于一步通信方式
- 当socket中有数据到达时,通过发送信号告知用户
1.1阻塞型IO
- 读阻塞
- 当套接字接收缓冲区中没有数据可以读取时调用 如 read/recv/recvfrom就会导致阻塞
- 当有数据到达时,内核便会去唤醒进程,通过read等函数来访问数据
- 如果进程阻塞过程中意外,那么进程将永远阻塞下去。
- 写阻塞
- 发生写阻塞的机会比较少,一般出现在写缓冲区无法写入即将写入的数据时
- 当无法写入数据时便会进入阻塞等待
- 一旦发送的缓冲区拥有足够的空间,则内核会唤醒对应的进程进行写入操作
- 而UDP协议中并不存在发送缓冲区满的情况,UDP套接字执行写操作时永远不会发生阻塞。
1.2非阻塞IO
- 如果有一个IO操作不能马上完成则系统则会让我们的进程进入睡眠状态等待
- 当我们将一个套接字设置为非阻塞模式时,则系统不会让我们的进程进入睡眠等待而是直接返回错误
- 当一个应用使用了非阻塞模式的套接字,他需要使用循环来不断检查文件描述如是否有数据可读
- 应用程序不同循环判断将会消耗非常大的CPU资源,一般不推荐使用
1.3非阻塞实现方法:
当我们一开始建立一个套接字描述符时,系统内核会默认设置为阻塞型IO,我们可以使用函数来设置套接字为非阻塞状态。
- fcntl
-
fcntl ( 文件描述词操作 ) 头文件: #include <unistd.h> #include <fcntl.h> 定义函数 : int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock * lock); 参数分析: fd --> 需要设置的文件描述符 cmd --> 设置的功能 F_DUPFD 用来查找大于或等于参数 arg 的最小且仍未使用的文件描述词, 并且复制参数 fd 的文件描述词. 执行成功则返回新复制的文件描述词. 请参考 dup2(). F_GETFD 取得 close-on-exec 旗标. 若此旗标的 FD_CLOEXEC 位为 0, 代表在调用 exec()相关函数时文件将不会关闭. F_SETFD 设置 close-on-exec 旗标. 该旗标以参数 arg 的 FD_CLOEXEC 位决定. F_GETFL 取得文件描述词状态旗标, 此旗标为 open()的参数 flags. F_SETFL 设置文件描述词状态旗标, 参数 arg 为新旗标, 但只允许 O_APPEND、O_NONBLOCK 和 O_ASYNC 位的改变, 其他位的改变将不受影响. F_GETLK 取得文件锁定的状态. F_SETLK 设置文件锁定的状态. 此时 flcok 结构的 l_type 值必须是 F_RDLCK、F_WRLCK 或 F_UNLCK. 如果无法建立锁定, 则返回-1, 错误代码为 EACCES 或 EAGAIN. F_SETLKW 同 F_SETLK 作用相同, 但是无法建立锁定时, 此调用会一直等到锁定动作成功为止. 若在等待锁定的过程中被信号中断时, 会立即返回-1, 错误代码为 EINTR. 参数 lock 指针为 flock 结构指针 返回值: 成功返回 0 失败返回 -1
注意结构体定义:
-
struct flcok { short int l_type; //锁定的状态 short int l_whence; //决定 l_start 位置 off_t l_start; //锁定区域的开头位置 off_t l_len; //锁定区域的大小 pid_t l_pid; //锁定动作的进程 }; l_type 有三种状态: F_RDLCK 建立一个供读取用的锁定 F_WRLCK 建立一个供写入用的锁定 F_UNLCK 删除之前建立的锁定 l_whence 也有三种方式: SEEK_SET 以文件开头为锁定的起始位置. SEEK_CUR 以目前文件读写位置为锁定的起始位置
操作例子:
int socket_fd = socket(...........); // 创建一个套接字描述符 int state = fcntl(socket_fd , F_GETFL , 0) ; // 获得当前描述符的旗标 state |= O_NONBLOCK ; // 在原基础上增加非阻塞属性 fcntl(scoket_fd , F_SETFL , state ); // 把配置的好的旗标重新设置回描述符中
2.多路复用
- 当应用程序同时处理多路数据的输入或输出时,若采用非阻塞模式,将达不到预期的效果
- 如果采用非阻塞模式,对多个输入进行轮询可以实现,但CPU的消耗非常大
- 如果使用多进程/多线程,将产生进程与线程同步互斥的问题使得程序变得非常复杂
- 使用多路复用则是最佳的选择,他的基本思想是:
- 先把所有需要监听等待的文件描述符添加到一个集合中,
- 在规定的时间内等待集合中所有描述符数据的变化,如果超时则跳出或进入下一次等待
- 如果在规定时间内文件描述符的数据有发生变化则把其他没有数据变化的描述符提出到集合之外等待进行下一次的等待状态。
1.接口API
select (I/O 多工机制/用来等待文件描述词状态的改变 )
头文件:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
定义函数:
int select(int n, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout);
参数说明:
n --> 当前最大的描述符+1
readfds --> 读取描述如组
writefds --> 写入描述如组
exceptfds --> 其他描述如组
timeout --> 超时时间
返回值:
如果参数 timeout 设为 NULL 则表示 select ()没有 timeout.
超时 返回 0
发生错误 返回 -1
注意:
struct timeval
{
time_t tv_sec;
time_t tv_usec;
};
FD_CLR(inr fd, fd_set* set); 用来清除描述词组 set 中相关 fd 的位
FD_ISSET(int fd, fd_set *set); 用来测试描述词组 set 中相关 fd 的位是否为真
FD_SET(int fd, fd_set*set); 用来设置描述词组 set 中相关 fd 的位
FD_ZERO(fd_set *set); 用来清除描述词组 set 的全部位
2.多路复用关键代码:
// 配置超时时间
struct timeval time_val ;
time_val.tv_sec = 5 ;
time_val.tv_usec = 0 ;
// 设置多路复用集合
fd_set set ;
FD_ZERO(&set); // 清空 集合
FD_SET(connect_fd , &set); // 添加 套记字到集合中
FD_SET(STDIN_FILENO , &set); // 添加标准输入到集合中
// 找到描述符最大值
max_fd = connect_fd > STDIN_FILENO ? connect_fd : STDIN_FILENO ;
// 等待描述符状态变化并设置超时 5秒
select(max_fd+1, &set , NULL ,NULL, &time_val);
// 等待客户端发话
bzero(msg , 1024);
if(FD_ISSET(STDIN_FILENO, &set)) //检查时候标准输入描述符有数据到达
{
fgets(msg , 1024 , stdin); // 获取标准输入数据
send(connect_fd , msg , strlen(msg), 0 ); // 发送数据
}
if(FD_ISSET(connect_fd, &set)) // 检查是否套接字描述符有数据到达
{
recv(connect_fd , msg , 1024 , 0); // 从套接字获得数据并发送
printf("msg :%s \n");
}
3.信号驱动
信号驱动其实就是涉及到的Systrm_V的信号,通过监听文件描述符的状态(是否有产生信号), 当文件描述符有数据到达时就会产生一个IO信号(SIGIO),来通知用户进行IO操作。
特点:
- 信号驱动一般用于UDP协议中,很少用于TCP协议中, 因为TCP协议中会有多次IO状态的改变,所以会有非常多的SIGIO信号产生,非常难捕捉到哪一个时数据到达产生的。
- 由于数据变化时,产生SIGIO信号,所以必须体现设置好信号捕获,并设置其对应的响应函数。
- 必须给文件描述符设置信号触发模式
3.1操作步骤:
- 建立套接字
- 绑定端口和地址信息
- 设置捕获信号并设置响应函数
- 设定套接字拥有者,用于捕获信号的到来
- 给套接字添加信号触发模式
3.2关键代码:
// 设置套接字的拥有者
fcntl(sock_fd ,F_SETOWN, getpid());
// 添加信号触发
int state;
state = fcntl(sock_fd,F_GETFL);
state |= O_ASYNC;
fcntl(sock_fd,F_SETFL,state);
while(1)// 循环挂起不让程序退出
{
printf("__%d__\n" , __LINE__);
pause();
}