一、多路复用(重点)
1、同时监听多个套接字
阻塞IO---->只能同时监听一个套接字
非阻塞IO--->一直轮询,询问IO口有没有数据到来,非常浪费CPU资源
2、什么是多路复用
就是预先把需要监听的文件描述符加入到一个集合中,然后在规定的时间内 或者 无限时间阻塞等待。
如果在规定的时间内,集合中文件描述符没有数据变化,则说明超时接收,会进入下一次规定的时间内再次等待。
一旦集合中的文件描述符有数据变化,则其他没有数据变化的文件描述符就会被踢除到集合之外,并且会再次进入下一次的等待状态。
3、特点
同时监听多个套接字。
4、多路复用的函数接口 ---select
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#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);
参数:
nfds: 最大的文件描述符+1
readfds: 监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者说 读取是不是不阻塞 99%
writefds:监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写 ,如果不想监视可写,可设置为NULL
exceptfds: 用来监视发生错误 异常 一般设置为NULL
timeout: 设置最大的等待时间--》超时一次之后,需要重新设置一个时间值,再传递给select这个函数
--如果为NULL,则说明这个函数是阻塞(无限等待,直到有数据到来)
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
};
比如设置5秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
1秒 = 1000 000 微秒
返回值:
成功
有数据到达 ---》就绪的文件描述符的总数
没有数据到达 --》超时--》0
失败
-1
5、关于文件描述符集合的函数接口
fd_set---》文件描述符集合 数据类型
1)删除集合中某个文件描述符fd
void FD_CLR(int fd, fd_set *set);
2)判断某个文件描述符 fd 是否 在集合set中
int FD_ISSET(int fd, fd_set *set);
3)将文件描述符fd加入到集合中set
void FD_SET(int fd, fd_set *set);
4)清空这个集合set
void FD_ZERO(fd_set *set);
例子: 实现客户端 与 服务器 之间 进行收发数据,在5秒内必须有数据到达,否则超时。
客户端 服务器
"hai" ---> "hai"
"zouni" <---- "zouni"
使用select同时监听客户端两个文件描述符 使用select同时监听服务器两个文件描述符
标准输入(STDIN_FILENO) 标准输入(STDIN_FILENO)
socketfd newClientFd
6、使用多路复用实现TCP客户端/服务器
(1)02使用多路复用实现TCP客户端.c
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define OWNADDR "192.168.14.3" //我自己ubuntu的ip地址
#define OWNPORT 10000 //我自己ubuntu的该程序的端口号
#define SERVERADDR "192.168.14.3" //对方的 服务器的IP地址
#define SERVERPORT 20000 //对方的 服务器的端口号
//TCP客户端
int main()
{
printf("TCP客户端 IP:%s Port:%u\n",OWNADDR,OWNPORT);
//1、买手机(建立套接字)
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
perror("socket error");
return -1;
}
//2、可以不用绑定
//3、发起连接
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;/*地址族 IPV4*/
serverAddr.sin_port = htons(SERVERPORT); //htons 将本地端口号转为网络端口号
serverAddr.sin_addr.s_addr = inet_addr(SERVERADDR); //将本地IP地址转为网络IP地址
connect(socketfd,(struct sockaddr *)&serverAddr,sizeof(struct sockaddr_in));
//通过比较,获取最大的文件描述符
int maxfd = STDIN_FILENO > socketfd ? STDIN_FILENO:socketfd;
struct timeval timeout;
while(1)
{
//一定要注意:集合的初始化一定放在循环里面
fd_set set;//定义一个文件描述符集合
//清空这个集合set
FD_ZERO(&set);
//将标准输入添加到集合中
FD_SET(STDIN_FILENO,&set);
//将套接字文件描述符添加到集合中
FD_SET(socketfd,&set);
//时间的设置也要放到循环里面
timeout.tv_sec = 5;//设置5秒
timeout.tv_usec = 0;
//使用多路复用的方式同时监听 标准输入STDIN_FILENO 和 socketfd
int ret = select(maxfd+1, &set,NULL,NULL, &timeout);
if(ret == -1)
{
printf("select error\n");
break;
}
else if(ret == 0)//超时了
{
printf("timeout......\n");
continue;//如果超时了,那么回到循环开头 又重新开始 监视
}
//代码如果执行到下面,说明文件描述符有变化了
//那么,到底是哪个文件描述符有变化了
//1)如果是键盘有输入,那么标准输入就有变化
if(FD_ISSET(STDIN_FILENO, &set))
{
//可以从键盘中获取数据了,然后发送给服务器
char buf[1024]={0};
scanf("%s",buf);
//发送
send(socketfd, buf, strlen(buf), 0);
}
//2)如果是服务器发送数据过来,那么socketfd就会变化
if(FD_ISSET(socketfd, &set))
{
char buf[1024]={0};
int ret = recv(socketfd,buf,sizeof(buf),0);
if(ret == 0)
break;
printf("recv:%s\n",buf);
}
}
return 0;
}
(2)03使用多路复用实现TCP服务器.c
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define OWNADDR "192.168.14.3" //我自己ubuntu的ip地址
#define OWNPORT 20000 //我自己ubuntu的该程序的端口号
//TCP服务器
int main()
{
printf("TCP服务器 IP:%s Port:%u\n",OWNADDR,OWNPORT);
//1、买手机(建立套接字)
int socketfd = socket(AF_INET,SOCK_STREAM,0);
if(socketfd == -1)
{
perror("socket error");
return -1;
}
//因为服务器立马退出之后,端口号不会及时释放
//此时如果服务器又马上运行,那么端口号会被占用,导致服务器分配端口号失败,连接失败
//所以设置端口号可以复用
int optval = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,&optval, sizeof(optval));
//2、绑定自己的电话号码(绑定自己的IP地址 和端口号)
//定义一个IPV4结构体变量,初始化自己的IP地址和端口号
struct sockaddr_in ownAddr;
ownAddr.sin_family = AF_INET;/*地址族 IPV4*/
ownAddr.sin_port = htons(OWNPORT); //htons 将本地端口号转为网络端口号
ownAddr.sin_addr.s_addr = inet_addr(OWNADDR); //将本地IP地址转为网络IP地址
bind(socketfd, (struct sockaddr *)&ownAddr,sizeof(struct sockaddr_in));
//3、设置铃声(监听) listen
listen(socketfd,5);
//4、坐等电话(阻塞接收客户端的连接)
printf("等待客户端连接.......\n");
//定义一个IPV4结构体变量,存储连接上来的客户端的IP地址 和 端口号
struct sockaddr_in clientAddr;
//如果你要想要获取对方的IP地址和端口号,第三个参数必须把结构体的大小传递进去
int len = sizeof(struct sockaddr_in);
int newClientFd = accept(socketfd,(struct sockaddr*)&clientAddr,&len);
if(newClientFd != -1)
{
printf("有客户端连接上来了............\n");
//打印连接上来的客户端的IP地址和端口号,将网络字节序转为 本地字节序
printf("连接上来的客户端IP:%s 端口号:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
}
//通过比较,获取最大的文件描述符
int maxfd = STDIN_FILENO > newClientFd ? STDIN_FILENO:newClientFd;
struct timeval timeout;
while(1)
{
//一定要注意:集合的初始化一定放在循环里面
fd_set set;//定义一个文件描述符集合
//清空这个集合set
FD_ZERO(&set);
//将标准输入添加到集合中
FD_SET(STDIN_FILENO,&set);
//将套接字文件描述符添加到集合中
FD_SET(newClientFd,&set);
//时间的设置也要放到循环里面
timeout.tv_sec = 5;//设置5秒
timeout.tv_usec = 0;
//使用多路复用的方式同时监听 标准输入STDIN_FILENO 和 newClientFd
int ret = select(maxfd+1, &set,NULL,NULL, &timeout);
if(ret == -1)
{
printf("select error\n");
break;
}
else if(ret == 0)//超时了
{
printf("timeout......\n");
continue;
}
//代码如果执行到下面,说明文件描述符有变化了
//那么,到底是哪个文件描述符有变化了
//1)如果是键盘有输入,那么标准输入就有变化
if(FD_ISSET(STDIN_FILENO, &set))
{
//可以从键盘中获取数据了,然后发送给客户端
char buf[1024]={0};
scanf("%s",buf);
//发送
send(newClientFd, buf, strlen(buf), 0);
}
//2)如果是客户端发送数据过来,那么newClientFd就会变化
if(FD_ISSET(newClientFd, &set))
{
char buf[1024]={0};
int ret = recv(newClientFd,buf,sizeof(buf),0);
if(ret == 0)
break;
printf("recv:%s\n",buf);
}
}
//5、关闭
close(socketfd);
close(newClientFd);
return 0;
}
二、信号驱动
1、信号驱动原理是怎样的?
使用了系统编程中信号的机制,首先让程序安装SIGIO信号处理函数,通过监听文件描述符是否产生了SIGIO信号
我们就知道了文件描述符有没有数据到达。如果有数据到达(你朋友来了),则系统就会自动产生一个SIGIO信号(门铃响了),
我们只需要在信号响应函数中读取数据就可以了
//信号响应函数
void my_fun(int arg)
{
}
signal(SIGIO,my_fun);
2、信号驱动特点以及步骤?
特点:适用于UDP协议,不适用TCP协议。
1)由于数据不知道什么时候到达,我们是不知道的,所以我们需要设置一个信号响应函数,捕捉这个信号。
2)设置套接字的属主。--->告诉系统 这个套接字 对应的 进程ID是多少。实际上就是 将 这个套接字 和 进程ID号绑定起来
3)如果使用信号驱动,则需要给这个套接字添加 信号触发模式 属性。因为默认是没有的
3、如何设置套接字的属主 ---》fcntl
F_SETOWN --->用于设置属主
fcntl(socketfd,F_SETOWN, getpid());
4、如何给套接字 添加 信号触发模式属性
//得到这个套接字文件描述符的属性
int status = fcntl(socketfd,F_GETFL);
//将得到的文件描述符的全部属性 中的 其中一个属性设置成 信号触发模式
status |= O_ASYNC;
int ret = fcntl(socketfd,F_SETFL,status);//把变量status的状态设置到文件描述符中
5、例子
使用信号驱动IO模型 写一个UDP协议服务器,实现监听多个客户端 给我发信息。
UDP客户端1
UDP客户端2
UDP客户端3 ---》信号驱动 ----》UDP服务器 --》打印出来
UDP客户端4
UDP客户端5
//UDP发送端
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#define OWNADDR "192.168.14.3" //我自己ubuntu的ip地址
#define OWNPORT 20000 //我自己ubuntu的该程序的端口号
#define SERVERADDR "192.168.14.3"//服务器的IP地址 也就是 对方的
#define SERVERPORT 10000 //服务器的端口号 也就是 对方的
int main()
{
//1、建立套接字(选择UDP协议 一定要选择这个 SOCK_DGRAM)
int socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(socketfd == -1)
{
printf("socketfd error\n");
return -1;
}
//2、绑定自己的IP地址和端口号 ---这一步在客户端 可以省略
struct sockaddr_in ownAddr;//定义一个IPV4结构体变量,初始化自己的信息
ownAddr.sin_family = AF_INET;//IPV4
ownAddr.sin_port = htons(OWNPORT); /*端口号*/
ownAddr.sin_addr.s_addr = inet_addr(OWNADDR);//本地IP-->网络IP
bind(socketfd,(struct sockaddr *)&ownAddr,sizeof(struct sockaddr_in));
//定义一个IPV4结构体变量,存储对方的IP地址和端口号
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;//IPV4
serverAddr.sin_port = htons(SERVERPORT); /*端口号*/
serverAddr.sin_addr.s_addr = inet_addr(SERVERADDR);//本地IP-->网络IP
//3、直接发送(聊天)
while(1)
{
char buf[1024]={0};
printf("data:");
scanf("%s",buf);
sendto(socketfd,buf,strlen(buf),0,(struct sockaddr *)&serverAddr,sizeof(struct sockaddr_in));
}
//4、关闭
close(socketfd);
return 0;
}
//UDP接收端+信号驱动
#include<stdio.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#define OWNADDR "192.168.14.3" //我自己ubuntu的ip地址
#define OWNPORT 10000 //我自己ubuntu的该程序的端口号
#define SERVERADDR "192.168.14.3"//服务器的IP地址 也就是 对方的
#define SERVERPORT 10000 //服务器的端口号 也就是 对方的
int socketfd;
//当socketfd 有数据到来的时候,就会触发SIGIO,然后执行这个函数
//接收数据
void handler(int arg)
{
char buf[1024]={0};
//定义一个IPV4结构体变量,存储对方的IP地址和端口号
struct sockaddr_in otherAddr;
int len = sizeof(struct sockaddr_in);//一定要指定大小,作为参数传递进去
int ret = recvfrom(socketfd,buf,sizeof(buf),0, (struct sockaddr *)&otherAddr,&len);
if(ret <=0)
return;
printf("来自[%s]:[%u]:%s\n",inet_ntoa(otherAddr.sin_addr),ntohs(otherAddr.sin_port),buf);
}
int main()
{
printf("UDP服务器 IP地址:%s 端口号:%u\n",SERVERADDR,SERVERPORT);
//1、建立套接字(选择UDP协议 一定要选择这个 SOCK_DGRAM)
socketfd = socket(AF_INET,SOCK_DGRAM,0);
if(socketfd == -1)
{
printf("socketfd error\n");
return -1;
}
//2、绑定自己的IP地址和端口号 ---这一步在客户端 可以省略
struct sockaddr_in ownAddr;//定义一个IPV4结构体变量,初始化自己的信息
ownAddr.sin_family = AF_INET;//IPV4
ownAddr.sin_port = htons(OWNPORT); /*端口号*/
ownAddr.sin_addr.s_addr = inet_addr(OWNADDR);//本地IP-->网络IP
bind(socketfd,(struct sockaddr *)&ownAddr,sizeof(struct sockaddr_in));
//3、给信号SIGIO设置一个信号响应函数
signal(SIGIO, handler);
//4、设置套接字的属主 --也就是将 socketfd套接字 跟 进程ID绑定在一起
fcntl(socketfd,F_SETOWN, getpid());
//5、给套接字socketfd添加信号响应模式属性
int status = fcntl(socketfd,F_GETFL);//得到这个套接字文件描述符的属性
status |= O_ASYNC;//将得到的文件描述符的全部属性 中的 其中一个属性设置成 信号触发模式
int ret = fcntl(socketfd,F_SETFL,status);//把变量status的状态设置到文件描述符中
//主函数阻塞在里面
while(1)
{
pause();
}
//4、关闭
close(socketfd);
return 0;
}
扩展:
(使用UDP通信)
服务器作为 数据的 中转站 负责 转发 数据
比如 客户端 [li4] 给客户端[laowang] 发送信息"想你"
思路 : 首先客户端[li4] 给服务器发送数据 :laowang的IP地址和端口号 + 数据 ---》服务器收到之后 解析数据 ,把 laowang的IP地址和端口号 解析出来 ---》服务器 把 数据 转发 给 laowang