1、Socket中的四种IO模型
-
阻塞型:
- 最常用、最简单、效率低
- 函数本身不具备阻塞属性,而是由于文件描述符本身导致函数阻塞
- 在默认情况下,Linux建立的Socket都是阻塞的
- 非阻塞
- 可以设置进程不阻塞在IO进程上,需要轮询
- 占用CPU资源较大
-
多路复用
- 同时对多个IO进行操作
- 可以设置在规定的时间内检查数据是否到达
-
驱动型IO
- 属于异步通信方式
- 当Socket中有数据到达时,通过发送信号告知用户
2、阻塞型IO
-
读阻塞
- 当套接字接收到缓冲区中没有数据可以读取时调用 如: read/recv/recvfrom就会导致阻塞
- 当有数据到达时,内核便会去唤醒进程,通过read等函数来访问数据
- 如果进程阻塞中发送意外,那么进程永远阻塞下去
-
写阻塞
- 发送写阻塞的机会比较少,一般出现在写缓冲区无法写入或者即将写入时
- 但无法写入数据时变回阻塞等待
- 一旦发送的缓存区拥有足够的空间,则内核会唤醒对应的进程进行写入操作
- 而UDP协议中并不存在发送缓冲区满的情况,UDP套接字执行时写操作永远不会发生阻塞
3、非阻塞IO
- 如果有一个IO操作不能马上完成,则系统会让进程进入睡眠状态等待
- 当我们将一个套接字设置为非阻塞模式时,则进程不会让进程进入睡眠等待,而是直接返回错误
- 当一个程序使用非阻塞模式的套接字,它需要使用循环来不断检查文件描述符是否有数据可读
-
应用程序不停循环判断将会消耗非常大的CPU资源,一般不推荐使用
非阻塞的实现方法:- 当我们一开始建立一个套接字描述符时,内核系统会默认设置为阻塞型IO,我们可以使用函数来设置套接字为非阻塞状态
4、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
F_GETFD
F_SETFD
F_GETFL:取得文件描述词状态旗标, 此旗标为 open()的参数 flags.
F_SETFL:设置文件描述词状态旗标, 参数 arg 为新旗标
F_GETLK
F_SETLK
F_UNLCK
F_SETLKW
返回值:
成功:0
失败:-1
结构体定义:
struct flock
{
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 以目前文件读写位置为锁定的起始位置.
SEEK_END 以文件结尾为锁定的起始位置.
操作例子:
int socket_fd = socket();//创建套接字描述符
int state = fcntl(socket_fd, F_GETFL, 0);//获得当前描述符的旗标
state |= O_NONBLOCK;//在原基础上增加非阻塞属性
fcntl(socket_fd, F_SETFL, state);//把设置好的旗标设置回描述符中
5、多路复用
- 当程序同时处理多路数据的输入或输出时,若采用非阻塞模式,将达不到预期的效果
- 如果采用非阻塞模式,对多个输入进行轮询可以实现,但是CPU的消耗非常大
- 如果采用多线程/多进程,将会产生进程与线程同步互斥的问题,使得程序变得非常复杂
- 使用多路复用则是最佳的选择,基本思想为:
- 先把所有需要监听等待的文件描述符添加到一个集合中
- 在规定的时间内等待集合中所有描述符数据的变化,路过超市则会跳出或者进入下一次等待。
- 如果在规定的时间内文件描述符的数据有发送变化,则把其他没有数据变化的描述符剔除到集合之外进行下一次的等待状态
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 的全部位
多路复用例子:
//配置超时时间
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); // 添加标准输入到集合中
//找到文件描述符最大值
maxfd = connect_fd > STDIN_FILENO ? connect_fd : STDIN_FILENO ;
// 等待描述符状态变化并设置超时 5秒
select(maxfd + 1, &set, NULL, NULL, &time_val );
//等待客户端发话
bzero(buf, 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");
]
练习:实现服务端和客户端的相互通信
服务端:server.c
#include "stdio.h"
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
int main()
{
//配置超时时间
struct timeval time_val;
time_val.tv_sec = 5;
time_val.tv_usec = 0;
//创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//绑定IPV4结构体
struct sockaddr_in serveraddr;
int serverlen = sizeof(serveraddr);
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8088);
serveraddr.sin_addr.s_addr = inet_addr("172.26.178.56");
bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
//设置监听套接字
listen(sockfd, 5);
//等待连接请求
int con_fd = accept(sockfd, (struct sockaddr *)&serveraddr, &serverlen);
printf("连接成功\n");
//添加描述符集合
fd_set set;
//清空集合
FD_ZERO(&set);
//添加网络套接字进入集合
FD_SET(con_fd, &set);
//将标准输入添加进集合
FD_SET(STDIN_FILENO, &set);
//找到最大的文件描述符
int maxfd = con_fd > STDIN_FILENO ? con_fd : STDIN_FILENO;
char msg[100] = {0};
char buf[100] = {0};
while(1)
{
fd_set readset = set;
//等待文件描述符状态变化,并设置超时时间
select(maxfd + 1, &readset, NULL, NULL, &time_val);
if(FD_ISSET(con_fd, &readset))//检查该文件描述符是否有请求到达
{
bzero(msg, 100);
//接收信息
recv(con_fd, msg, 100, 0);
printf("收到信息:%s\n", msg);
}
if(FD_ISSET(STDIN_FILENO, &readset))
{
bzero(buf, 100);
fgets(buf, 100, stdin);
send(con_fd, buf, strlen(buf), 0);
printf("发送信息\n");
}
}
}
客户端:client.c
#include "stdio.h"
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>
int main()
{
//配置超时时间
struct timeval time_val;
time_val.tv_sec = 5;
time_val.tv_usec = 0;
//创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//绑定IPV4结构体
struct sockaddr_in clientaddr;
int clientlen = sizeof(clientaddr);
clientaddr.sin_family = AF_INET;
clientaddr.sin_port = htons(8088);
clientaddr.sin_addr.s_addr = inet_addr("172.26.178.56");
bind(sockfd, (struct sockaddr *)&clientaddr, sizeof(clientaddr));
//连接服务端
connect(sockfd, (struct sockaddr *)&clientaddr, clientlen);
printf("连接成功\n");
//添加描述符集合
fd_set set;
//清空集合
FD_ZERO(&set);
//添加网络套接字进入集合
FD_SET(STDIN_FILENO, &set);
//添加网络套接字进入集合
FD_SET(sockfd, &set);
//找到最大的文件描述符
int maxfd = sockfd > STDIN_FILENO ? sockfd : STDIN_FILENO;
char msg[100] = {0};
char buf[100] = {0};
while(1)
{
fd_set readset = set;
//等待文件描述符状态变化,并设置超时时间
select(maxfd + 1, &readset, NULL, NULL, &time_val);
if(FD_ISSET(STDIN_FILENO, &readset))//检查该文件描述符是否有请求到达
{
bzero(msg, 100);
//发送信息
fgets(msg, 100, stdin);
send(sockfd, msg, strlen(msg), 0);
}
if(FD_ISSET(sockfd, &readset))//检查该文件描述符是否有请求到达
{
bzero(buf, 100);
//接收信息
recv(sockfd, buf, 100, 0);
printf("收到信息:%s\n", buf);
}
}
}
6、信号驱动
-
信号驱动其实就是涉及到我们前面所学到的System—V的信号,通过监听文件描述符的状态(是否产生信号),当文件描述符有数据到达时就会产生一个信号(SIGIO),来通知用户进行IO操作
-
特点
- 信号驱动一般用于UDP协议中,很少用于TCP协议中。因为TCP协议中会有很多次IO改变状态,所以会有非常多的信号产生,非常难捕捉到是哪一个数据到达时产生的
- 由于数据变化时,产生SIGIO信号,所以必须提前设置好信号捕获,并设置其对应的响应函数
- 必须给文件描述符设为为信号触发模式
-
操作步骤
- ①、建立套接字
- ②、绑定端口和套接字信息
- ③、设置捕获信号和响应函数
- ④、设定套接字拥有者,用于捕获信号的到来
- ⑤、给套接字添加信号触发模式
关键代码
//设置套接字的拥有者
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)
{
pause();
}