网络编程三:多路复用+信号驱动

一、多路复用(重点)

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    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值