网络编程知识四之IO模型 设置获取socket属性

一.I/O模型讲解
1)阻塞(block)IO :
当资源不满足条件。 此时进程阻塞。进程休眠,不会浪费CPU。最简单效率低。并且进程最终会阻塞在其中一个阻塞函数上,而其它的函数没法及时调用。

常见的阻塞I/O:read / write fgets/scanf send/recv accept

2)非阻塞的方式调用(noblock)
问题:需要不断轮询每个函数,浪费cpu资源 。若是没有数据,则让进程立即返回错误。
错误码如下:
普通文件描述符 EAGAIN
套接字 EAGAIN or EWOULDBLOCK
错误码路径:
/usr/include/asm-generic/errno.h

3)IO多路复用
同时监控多个文件,用一个单进程同时对多路阻塞的IO进行控制,降低系统资源的消耗,提高效率

4)异步信号通知[了解即可]
文件就绪的时候,通过驱动发送信号给应用程序,然后让应用程序对文件进行操作。

二.非阻塞的实现IO

1>文件描述符的属性修改函数

头文件:#include <unistd.h>   
       #include <fcntl.h>
open()
int fcntl(int fd, int cmd); 
int fcntl(int fd, int cmd, long arg);
功能:对文件描述符的一些属性进行设置或者修改。第三个arg参数是否使用由cmd参数的值来决定。
参数:
    @ fd         操作的文件描述符
    @cmd        操作的指令
                 F_GETFL (get file)    arg被忽略不适用  
                            获得fd的文件标志(O_RDONLY ,O_WRONLY,O_RDWR三者选一被获得, 0,1,2,分别是三者的值 )。    
                 F_SETFL (set file)   通过arg给文件描述符设置状态标志。
                                    arg 可用标志:
                                    O_NONBLOCK(非阻塞的IO,例如read没有文件可读的时候,返回-1和errno置EAGAIN错误)
返回值:若是cmd 为F_GETFL 返回一个相应的文件标志。
       若是cmd 为F_SETFL  成功返回0,失败返回-1.

例如:

//把文件描述符设置为非阻塞模式。
int flags = fcntl(fd ,F_GETFL); //获得文件的当前标志位.
flags |= O_NONBLOCK. //在当前标志位添加负责非阻塞的标志.
fcntl(fd,F_SETFL,flags) ;//重新设置fd的状态标志。

fcntl.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//./a.out newfile
int main(int argc, const char *argv[])
{
	int fd = 0;	
	int flag = 0;
	if(argc < 2)
	{
		fprintf(stderr,"Usage : %s newfile\n",argv[0]);	
		exit(EXIT_FAILURE);
	}

//	fd  = open(argv[1],O_RDONLY);	
//  fd  = open(argv[1],O_WRONLY | O_CREAT | O_TRUNC,0666);	
    fd  = open(argv[1],O_RDWR | O_CREAT | O_TRUNC);	
	if(fd < 0)
	{
		perror("Fail to open");	
		exit(EXIT_FAILURE);
	}
	//1.获得当前文件标志位
	flag = fcntl(fd,F_GETFL);
	switch(flag)
	{
	case O_RDONLY:
		printf("File flag is O_RDONLY\n");
		break;
	case O_WRONLY:
		printf("File flag is O_WRONLY\n");
		break;
	case O_RDWR:
		printf("File flag is O_RDWR\n");
		break;
	}
	printf("flag : %d\n",flag);
	return 0;
}

三.IO多路复用的实现

1.本质:
通过一个单进程创建一个文件描述符表,把多路阻塞的IO存放如表中,进控制。降低系统资源的消耗,提高效率。

在这里插入图片描述

2.基本思想
while(1)
{
利用selcet函数同时监控多个文件。如果没有文件就绪,就阻塞休眠。只要有一个文件就绪,就返回找出就绪的文件,然后处理就绪的文件。
}

常用函数:

1.头文件 
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:实现IO多路复用,检测指定的文件描述符
参数:
@        nfds                文件描述符的个数   (最大的文件描述符maxfd + 1)
@        readfsd             监控的"读操作"文件描述符表
@        writefds            监控的"写操作"文件描述符表
@        exceptfds        其它的文件描述符集合 (一般是指异常的文件描述符集合)
@        timeout            NULL               阻塞方式调用
             struct timeval结构体中的数据为  0     超时时间设置为0s,
												非阻塞的方式调用
                          自己指定超时时间        指定的时间内等待

struct timeval{
        long  tv_sec;    //设置秒数
        long  tv_usec; //设置微秒
}


返回值:
成功 ,  若是返回值>0  返回就绪的文件的描述符的个数,并且在文件描述符表中清除其他未就绪的文件描述符。
       若是返回值=0  , 连接超时
 错误,返回-1, 并置errno

例如:
//设置5s中的超时时间
struct timeval tm = {5,0};
//设置8000微秒的超时时间
struct timeval tm = {0,800}

select (…,&tm)

思考:为什么我们的select第一个参数是最大的文件描述符 + 1呢?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

typedef struct{
	long int fds_bits[1024 / 32];
}fd_set;
FD_ISSET

注意:fd_set 结构体 实质为一个long类似的数组每一个数组元素都能与一打开的socket文件描述符建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件发生了可读或可写事件。

 //将fd 从 set 的文件描述符数组集合中清除 [file descriptor clear]
void FD_CLR(int fd, fd_set *set); 

//判断 fd在set的文件描述符数组集合中是否已被设置(就绪)  [file descriptor set]
int  FD_ISSET(int fd, fd_set *set);                                

 //将fd添加到 set对应的文件描述符数组集合中 
void FD_SET(int fd, fd_set *set); 

//将set文件描述符数组集合清零,
void FD_ZERO(fd_set *set);   

例如:
假如我们的服务端想要从键盘输入数据,并且还能接受用户的连接。我们就可以进行如下操作:
利用监测listen_fd STDIN_FILENO 它们是否进行了读操作
<1>建立文件描述符表

fd_set  readfds;   // 定义一个读文件描述符集合
fd_set  readfds_bak; //对文件描述符集合的一个备份

FD_ZERO(&readfds_bak); //将表清空
FD_SET(STDIN_FILENO,&readfds_bak); //添加要监控的文件描述符集合
FD_SET(listenfd,&readfds_bak);

//找到最大的文件描述符
maxfd = listen_fd > STDIN_FILENO ? listenfd :STDIN_FILENO;

while(1)
{
    //重新添加关注的文件描述符集合。
    //由于select每次找到对应的文件描述符集合就会清除文件描述符集合。
    //因此,我们这里创建了一个reasfds_bak,select真正检测的是readfds中
    //的数据,对我们的readfds_bak没有影响。每次清楚之后,重新再把需要检测的文件
    //监控的文件描述符集合赋值回去。
    readfs = readfds_bak;
    n = select(maxfd + 1, &readfds,NULL,NULL,NULL);

   //从0到最大的文件描述符开始,对所有的描述符监控
    for(i = 0;i < =maxfd,;i++)
    {
            if(FD_ISSET(i,readfs))   //判断readfs中对应的文件描述符是否就绪
           {
              //说明标准数据就绪
              if(i === STDIN_FILENO)
              {
						//读的时候一定有数据
                      fgets(buf,sizeof(buf),stdin);
                      fputs(buf,stdout);
              }else if(i == listenfd)
              {
                      //说明有用户请求连接。
                      connect_fd = accept();
              }
            }
    }
    
}

练习:select函数应用于tcp服务器
1.监测标准输入,listen_fd,若标准输入就绪,读数据并打印,若有新连接请求,提取并打印请求方ip和端口如果有用户输入数据,则接收用户数据并打印出来。
server.c

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>	       /* See NOTES */
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define N 1024
//./a.out ip port
int main(int argc, const char *argv[])
{
	char buf[1024] = {0};
	int listen_fd = 0;
	int connect_fd = 0;
	struct sockaddr_in my_addr;
	struct sockaddr_in client_addr;
	int len = sizeof(my_addr);
	int n = 0 ;

	fd_set rfds; //添加文件描述符读表
	fd_set rfds_bak; //添加文件描述符读表的备份
	int maxfd = 0;
	int i = 0;

	if(argc < 3)
	{
		fprintf(stderr,"Usage : %s ip port!\n",argv[0]);	
		exit(EXIT_FAILURE);
	}

	//1.通过socket创建监听套接字
	listen_fd = socket(AF_INET,SOCK_STREAM,0);
	if(listen_fd < 0)
	{
		perror("Fail to socket");	
		exit(EXIT_FAILURE);
	}

	//2.填充服务器自己的ip地址和端口
	memset(&my_addr,0,sizeof(my_addr));	
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(atoi(argv[2]));
	my_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//3.把ip和port进行绑定
	if(bind(listen_fd,(struct sockaddr *)&my_addr,len) < 0)
	{
		perror("Fail to bind");	
		exit(EXIT_FAILURE);
	}

	//4.监听客户端的连接
	listen(listen_fd,5);
	printf("Listen....\n");


	//思考:若是我们的服务端需要从键盘输入数据存放到缓冲去中,并且我们的accept
	//     需要接受多个用户的连接请求,除了并发服务器外,还有什么方法?
	// 使用IO多路复用
		
	//1.将关注的表清除,创建一个空表
	FD_ZERO(&rfds_bak);
	FD_ZERO(&rfds);

	//2.STDIN_FILENO,listen_fd,将我们需要的数据添加到表中
	FD_SET(STDIN_FILENO,&rfds_bak);
	FD_SET(listen_fd,&rfds_bak);
	

	//3.寻找最大的文件描述符
	maxfd = listen_fd > STDIN_FILENO ? listen_fd : STDIN_FILENO; 


	while(1)
	{
		//由于每次循环找到,匹配的文件描述符表后,里面的数据
		//会被清除掉,因此,我们需要一个备份
		rfds = rfds_bak;			
		n = select(maxfd + 1,&rfds,NULL,NULL,NULL);
		if(n < 0)
		{
			perror("Fail to select");	
			exit(EXIT_FAILURE);
		}
	
		//0,1,3,4,5,6,7,8,9
		//遍历从0到maxfd中,所有的文件描述符
		for(i = 0;i <= maxfd;i++)
		{
			if(FD_ISSET(i,&rfds))	
			{
				//若是标准输入,表示用户从键盘输入了数据
				//存放到缓冲区,我们可以
				if(i == STDIN_FILENO)	//一直是就绪				
				{
					putchar('>');	

					fgets(buf,sizeof(buf),stdin);
					printf("buf : %s\n",buf);
				}else if(i == listen_fd){ 
					//5.准备接收客户端的连接请求
					connect_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&len);	
					if(connect_fd < 0)
					{
						perror("Fail to accept");	
						exit(EXIT_FAILURE);
					}
					
					printf("=============================================");
					printf("connect_fd : %d\n",connect_fd);
					printf("client IP : %s\n",inet_ntoa(client_addr.sin_addr));
					printf("client port : %d\n", ntohs(client_addr.sin_port));
					printf("=============================================");

					FD_SET(connect_fd,&rfds_bak);
					//重新找到最大的文件描述符集合
					maxfd = connect_fd > maxfd ? connect_fd : maxfd;
				}
			
			}
		}
	}

	close(listen_fd);
	close(connect_fd);
	exit(EXIT_SUCCESS);
}


client.c

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>	       /* See NOTES */
#include <stdlib.h>
#include <string.h>

#define N 1024
//./a.out ip port
int main(int argc, const char *argv[])
{
	char buf[1024] = {0};
	int sockfd = 0;
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	int len = sizeof(server_addr);
	int n = 0 ;
	if(argc < 3)
	{
		fprintf(stderr,"Usage : %s ip port!\n",argv[0]);	
		exit(EXIT_FAILURE);
	}

	//1.通过socket创建监听套接字
	sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		perror("Fail to socket");	
		exit(EXIT_FAILURE);
	}

	//2.填充服务器自己的ip地址和端口
	memset(&server_addr,0,sizeof(server_addr));	
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(atoi(argv[2]));
	server_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//3.准备向服务端的请求连接
	if(connect(sockfd,(struct sockaddr *)&server_addr,len) < 0)
	{
		perror("Fail to accept");	
		exit(EXIT_FAILURE);
	}

	while(1)
	{
		memset(buf,0,sizeof(buf));
		putchar('>');
		fgets(buf,sizeof(buf),stdin);
		buf[strlen(buf) - 1] = '\0';

		n = send(sockfd,buf,strlen(buf),0);	

		if(n < 0)
		{
			perror("Fail to recv!\n");	
			exit(EXIT_FAILURE);
		}else if(n == 0){
			printf("clinet is not connect\n");	
			exit(EXIT_FAILURE);
		}

		if(strncmp(buf,"quit",4) == 0)
			break;

	}

	close(sockfd);
	exit(EXIT_SUCCESS);
}


四.设置,获得socket属性

#include <sys/types.h>       
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
功能:获得或者设置socket属性
参数:
    @sockfd          指定socket
    @level           指定操作socket层次
                     可选SOL_SOCKET    //通用的socket选项
                          IPPROTO_IP    //IP层
                          IPPROTO_TCP   //TCP层            
   @optname     由level来选择控制的方式
                    SOL_SOCKET  :SO_BROADCAST 允许发送广播包
                                :SO_RCVTIMEO  接收超时
                                :SO_KEEPALIVE 保持连接

    @optval  设置相应控制方式的值,定义变量传地址
    @optlen  optval之的大小
返回值:成功返回0;失败返回-1,置errno
例如:
设置5s的超时
struct timeval tm = {5,0};
if (setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tm,sizeof(tm)) < 0)
{
    perror("setsockopt fail");
    exit(EXIT_FAILURE);
}

网络超时检测的方法
1>设置socket超时

struct timeval
  {
    __time_t tv_sec;	 /* Seconds.  */
    __suseconds_t tv_usec;	/* Microseconds.  */
  };
  
socket 用sockfd

socklen_t len = sizeof(struct timeval); 
struct timeval tv;

tv.tv_sec = 5;    //超时5s
tv.tv_usec = 0;
//检测5s时间的超时检测
if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,len) < 0)
handle_error();

Service.c

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>	       /* See NOTES */
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>	       /* See NOTES */

//./a.out ip port
int main(int argc, const char *argv[])
{
	int sockfd = 0;	
	struct sockaddr_in my_addr;
	struct sockaddr_in client_addr;
	socklen_t len =  sizeof(my_addr);
	int n = 0;
	char buf[1024] = {0};

	struct timeval tv = {5,0};//设置5s的超时机制
	if(argc < 3)
	{
		fprintf(stderr,"Usage : %s ip port!\n",argv[0]);	
		exit(EXIT_FAILURE);
	}

	//1.通过socket创建文件描述符
	sockfd = socket(AF_INET,SOCK_DGRAM ,0);
	if(sockfd < 0)
	{
		perror("Fail to socket!");	
		exit(EXIT_FAILURE);
	}

	//2.填充服务器自己的ip地址和port,方便绑定
	memset(&my_addr,0,sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(atoi(argv[2]));
	my_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//3.把ip地址和port与sockfd绑定
	if(bind(sockfd,(struct sockaddr *)&my_addr,len) < 0)
	{
		perror("Fail to bind");	
		exit(EXIT_FAILURE);
	}

	//把sockfd设置为5s的接收超时
	if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) < 0)
	{
		perror("Fail to setsocketopt");	
		exit(EXIT_FAILURE);
	}

	//4.接收数据 ,循环接收,阻塞接收,若是没有数据
	//发送过来,则阻塞
	while(1)
	{
		memset(buf,0,sizeof(buf));
		n = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&client_addr,&len);
		if(n < 0)
		{ 
			perror("recvfrom timeout deal with....\n");	
			continue;
		}else{

		printf("===============================\n");
		printf("Recv from IP : %s\n",inet_ntoa(client_addr.sin_addr));
		printf("Recv fromt port : %d\n", ntohs(client_addr.sin_port));
		printf("Recv %d bytes : %s\n",n,buf);
		}
	}
	return 0;
}

client.c

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>	       /* See NOTES */
#include <stdlib.h>
#include <string.h>

//./a.out ip port
int main(int argc, const char *argv[])
{
	int sockfd = 0;	
	struct sockaddr_in server_addr;
	socklen_t len =  sizeof(server_addr);
	int n = 0;
	char buf[1024] = {0};
	if(argc < 3)
	{
		fprintf(stderr,"Usage : %s ip port!\n",argv[0]);	
		exit(EXIT_FAILURE);
	}

	//1.通过socket创建文件描述符
	sockfd = socket(AF_INET,SOCK_DGRAM ,0);
	if(sockfd < 0)
	{
		perror("Fail to socket!");	
		exit(EXIT_FAILURE);
	}

	//2.填充服务器自己的ip地址和port,方便绑定
	memset(&server_addr,0,sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(atoi(argv[2]));
	server_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//发送数据
	while(1)
	{
		memset(buf,0,sizeof(buf));
		fgets(buf,sizeof(buf),stdin);
		buf[strlen(buf) - 1] = '\0'; //'\n'-->'\0'

		n = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&server_addr,len);
		if(n < 0)
		{
			perror("Fail to recvfrom");	
			exit(EXIT_FAILURE);
		}

	}
	return 0;
}

每5s中如果没有输入的话就会出现出错提醒。

2>select函数设置超时检测

int select(int nfds, fd_set *readfds, fd_set *writefds,
        fd_set *exceptfds, struct timeval *timeout);

socket由sockfd标识
例:
fd_set rfds,fds_bak;

FD_ZERO(&fds_bak);
FD_SET(&sockfd,&fds_bak);
struct timeval tv = {5,0};

Service.c

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>	       /* See NOTES */
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define N 1024
//./a.out ip port
int main(int argc, const char *argv[])
{
	char buf[1024] = {0};
	int listen_fd = 0;
	int connect_fd = 0;
	struct sockaddr_in my_addr;
	struct sockaddr_in client_addr;
	int len = sizeof(my_addr);
	int n = 0 ;

	struct timeval tv = {10, 0};
	struct timeval *p = NULL;
	p = &tv;

	fd_set rfds; //添加文件描述符读表
	fd_set rfds_bak; //添加文件描述符读表的备份
	int maxfd = 0;
	int i = 0;

	if(argc < 3)
	{
		fprintf(stderr,"Usage : %s ip port!\n",argv[0]);	
		exit(EXIT_FAILURE);
	}

	//1.通过socket创建监听套接字
	listen_fd = socket(AF_INET,SOCK_STREAM,0);
	if(listen_fd < 0)
	{
		perror("Fail to socket");	
		exit(EXIT_FAILURE);
	}

	//2.填充服务器自己的ip地址和端口
	memset(&my_addr,0,sizeof(my_addr));	
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(atoi(argv[2]));
	my_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//3.把ip和port进行绑定
	if(bind(listen_fd,(struct sockaddr *)&my_addr,len) < 0)
	{
		perror("Fail to bind");	
		exit(EXIT_FAILURE);
	}

	//4.监听客户端的连接
	listen(listen_fd,5);
	printf("Listen....\n");


	//思考:若是我们的服务端需要从键盘输入数据存放到缓冲去中,并且我们的accept
	//     需要接受多个用户的连接请求,除了并发服务器外,还有什么方法?
	// 使用IO多路复用
		
	//1.将关注的表清除,创建一个空表
	FD_ZERO(&rfds_bak);
	FD_ZERO(&rfds);

	//2.STDIN_FILENO,listen_fd,将我们需要的数据添加到表中
	FD_SET(STDIN_FILENO,&rfds_bak);
	FD_SET(listen_fd,&rfds_bak);
	

	//3.寻找最大的文件描述符
	maxfd = listen_fd > STDIN_FILENO ? listen_fd : STDIN_FILENO; 


	while(1)
	{
		//由于每次循环找到,匹配的文件描述符表后,里面的数据
		//会被清除掉,因此,我们需要一个备份
		rfds = rfds_bak;	
		//设置10s的超时,10s后超时非阻塞调用		
		n = select(maxfd + 1,&rfds,NULL,NULL,p);
		if(n < 0)
		{
			perror("Fail to select");	
			exit(EXIT_FAILURE);
		} else if (n == 0) {
			printf("select will timeout!\n");
			sleep(1);
			p->tv_sec = 10;
			p->tv_usec = 0;
		}
	
		//0,1,3,4,5,6,7,8,9
		//遍历从0到maxfd中,所有的文件描述符
		for(i = 0;i <= maxfd;i++)
		{
			if(FD_ISSET(i,&rfds))	
			{
				//若是标准输入,表示用户从键盘输入了数据
				//存放到缓冲区,我们可以
				if(i == STDIN_FILENO)	//一直是就绪				
				{
					putchar('>');	

					fgets(buf,sizeof(buf),stdin);
					printf("buf : %s\n",buf);
				}else if(i == listen_fd){ 
					//5.准备接收客户端的连接请求
					connect_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&len);	
					if(connect_fd < 0)
					{
						perror("Fail to accept");	
						exit(EXIT_FAILURE);
					}
					
					printf("=============================================");
					printf("connect_fd : %d\n",connect_fd);
					printf("client IP : %s\n",inet_ntoa(client_addr.sin_addr));
					printf("client port : %d\n", ntohs(client_addr.sin_port));
					printf("=============================================");

					FD_SET(connect_fd,&rfds_bak);
					//重新找到最大的文件描述符集合
					maxfd = connect_fd > maxfd ? connect_fd : maxfd;
				} else {
					memset(buf, 0, sizeof(buf));
					n = recv(i, buf, sizeof(buf), 0);
					if (n < 0) {
						perror("Fail to recv");
						exit(EXIT_FAILURE);
					} else if (n == 0) {
						printf("client no connect\n");
						exit(EXIT_FAILURE);
					}
					//打印出接受的数据字节数
					printf("Recv %d bytes: %s\n", n, buf);

					//若是发现接受的字符串是quit,说明客户端断开连接i
					//此时的i应该从文件描述符集合中清除,FD_CLR, close(i)
					if (strncmp(buf, "quit", 4) == 0) {
						FD_CLR(i, &rfds_bak);
						close(i);
					}
				}
			
			}
		}
	}

	close(listen_fd);
	close(connect_fd);
	exit(EXIT_SUCCESS);
}


rfds = fds_bak;
select(maxfd + 1,&rfds,NULL,NULL,&tv); //设置5s的超时,非阻塞

3> 超时机制的常用实例。针对不同的网络异常进行处理

  1. 客户端出了问题(死机、重启了、网络断了…)
  2. 服务端出了问题(死机、重启了、网络断了、内存耗尽、…)
  3. 网络不正常

解决方法:设置心跳包。

Keepalive,是TCP中一个可以检测死TCP连接的机制。原理很简单,TCP协议会在空闲了一定的时间之后发送数据给对方。如果对方在一定的时间内没有回应,则表示超时,撤销连接。

typedef struct
{
     int type;        //发送包的类型,数据包,还是心跳包
     char buf[1024]; //发送的数据
}

#define  DATA_PACKET   10   //数据包
#define HEADT_PACKET  20    //心跳包

客户端: 创建子线程每过1s发送一个心跳包,主线程正常从键盘输入数据发送。
心跳包类型 MSG msg = {HEART_PACKET,“I am alive!”};
服务器端:
客气子进程进行接收客户端消息。通过alarm设置定时,收到正常包正常打印,收到心跳包重新打印数据,若是一定时间内没有收到内没有收到心跳包,就结束当前进程。

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>	       /* See NOTES */
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define N 1024

typedef struct {
	int type;
	char buf[1024];
} msg_t;

#define DATA_PACKET 10
#define HEADT_PACKET 20

void handle_alarm() {
	printf("the GAME OVER!\n");
	exit(EXIT_SUCCESS);
	return;
}

void do_client(int sockfd) {
	msg_t msg;
	int n = 0;

	if (signal(SIGALRM, handler_alarm) == SIG_ERR) {
		perror("Fail to signal");
		exit(EXIT_FAILURE);
	}
	//10s后发送SIGALARM信号,10s是超时时间,10s后若是没有,收到心跳包,则结束该进程。
	//这里的定时不能删掉,否则第一次建立连接套接字后,服务器会一直阻塞在下面的recv等待
	//用户的发送数据,有了这条语句后10s没有数据发过来的话直接发送SIGALARM,处理handler_alarm函数,结束进程
	
	alarm(10);

	while(1) {
		memset(&msg, 0, sizeof(msg));
		if ((n = recv(sockfd, &msg, sizeof(msg), 0)) > 0)
		{
			if (msg.type == DATA_PACKET) {
				printf("Recv %d bytes : %s\n", strlen(msg.buf), msg.buf);
				continue;
			} else if (msg.type == HEADT_PACKET) {
				printf("head recv : %s\n", msg.buf);
				alarm(10);
			}
			if (strncmp(msg.buf, "quit", 4) == 0)
				break;
		}
		exit(EXIT_SUCCESS);
	}
}

void handler_process(int signum) {
	waitpid(-1, NULL, WNOHANG);
	printf("child exit is success!\n");
	return;
}

//./a.out ip port
int main(int argc, const char *argv[])
{
	char buf[1024] = {0};
	int listen_fd = 0;
	int connect_fd = 0;
	struct sockaddr_in my_addr;
	struct sockaddr_in client_addr;
	int len = sizeof(my_addr);
	int n = 0 ;

	pid_t pid;
	int ret =0;
	
	struct timeval tv = {10, 0};
	struct timeval *p = NULL;
	p = &tv;

	fd_set rfds; //添加文件描述符读表
	fd_set rfds_bak; //添加文件描述符读表的备份
	int maxfd = 0;
	int i = 0;

	if(argc < 3)
	{
		fprintf(stderr,"Usage : %s ip port!\n",argv[0]);	
		exit(EXIT_FAILURE);
	}
	
	//对僵尸态子进程进行回收
	if (signal(SIGCHLD, handler_process) ==  SIG_ERR) {
		perror("Fail to signal");
		exit(EXIT_FAILURE);
	}

	//1.通过socket创建监听套接字
	listen_fd = socket(AF_INET,SOCK_STREAM,0);
	if(listen_fd < 0)
	{
		perror("Fail to socket");	
		exit(EXIT_FAILURE);
	}

	//2.填充服务器自己的ip地址和端口
	memset(&my_addr,0,sizeof(my_addr));	
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(atoi(argv[2]));
	my_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//3.把ip和port进行绑定
	if(bind(listen_fd,(struct sockaddr *)&my_addr,len) < 0)
	{
		perror("Fail to bind");	
		exit(EXIT_FAILURE);
	}

	//4.监听客户端的连接
	listen(listen_fd,5);
	printf("Listen....\n");


	//思考:若是我们的服务端需要从键盘输入数据存放到缓冲去中,并且我们的accept
	//     需要接受多个用户的连接请求,除了并发服务器外,还有什么方法?
	// 使用IO多路复用
		
	//1.将关注的表清除,创建一个空表
	FD_ZERO(&rfds_bak);
	FD_ZERO(&rfds);

	//2.STDIN_FILENO,listen_fd,将我们需要的数据添加到表中
	FD_SET(STDIN_FILENO,&rfds_bak);
	FD_SET(listen_fd,&rfds_bak);
	

	//3.寻找最大的文件描述符
	maxfd = listen_fd > STDIN_FILENO ? listen_fd : STDIN_FILENO; 


	while(1)
	{
		//由于每次循环找到,匹配的文件描述符表后,里面的数据
		//会被清除掉,因此,我们需要一个备份
		rfds = rfds_bak;	
		//设置10s的超时,10s后超时非阻塞调用		
		n = select(maxfd + 1,&rfds,NULL,NULL,p);
		if(n < 0)
		{
			perror("Fail to select");	
			exit(EXIT_FAILURE);
		} else if (n == 0) {
			printf("select will timeout!\n");
			sleep(1);
			p->tv_sec = 10;
			p->tv_usec = 0;
		}
	
		//0,1,3,4,5,6,7,8,9
		//遍历从0到maxfd中,所有的文件描述符
		for(i = 0;i <= maxfd;i++)
		{
			if(FD_ISSET(i,&rfds))	
			{
				//若是标准输入,表示用户从键盘输入了数据
				//存放到缓冲区,我们可以
				if(i == STDIN_FILENO)	//一直是就绪				
				{
					putchar('>');	

					fgets(buf,sizeof(buf),stdin);
					printf("buf : %s\n",buf);
				}else if(i == listen_fd){ 
					//5.准备接收客户端的连接请求
					connect_fd = accept(listen_fd,(struct sockaddr *)&client_addr,&len);	
					if(connect_fd < 0)
					{
						perror("Fail to accept");	
						exit(EXIT_FAILURE);
					}
					
					printf("=============================================");
					printf("connect_fd : %d\n",connect_fd);
					printf("client IP : %s\n",inet_ntoa(client_addr.sin_addr));
					printf("client port : %d\n", ntohs(client_addr.sin_port));
					printf("=============================================");

					FD_SET(connect_fd,&rfds_bak);
					//重新找到最大的文件描述符集合
					maxfd = connect_fd > maxfd ? connect_fd : maxfd;
				} else {
					memset(buf, 0, sizeof(buf));
					n = recv(i, buf, sizeof(buf), 0);
					if (n < 0) {
						perror("Fail to recv");
						exit(EXIT_FAILURE);
					} else if (n == 0) {
						printf("client no connect\n");
						exit(EXIT_FAILURE);
					}
					//打印出接受的数据字节数
					printf("Recv %d bytes: %s\n", n, buf);

					//若是发现接受的字符串是quit,说明客户端断开连接i
					//此时的i应该从文件描述符集合中清除,FD_CLR, close(i)
					if (strncmp(buf, "quit", 4) == 0) {
						FD_CLR(i, &rfds_bak);
						close(i);
					}
				}
			
			}
		}
	}

	close(listen_fd);
	close(connect_fd);
	exit(EXIT_SUCCESS);
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python的IO网络编程主要涉及socket模块。在socket模块中,有一些常用的函数,比如accept()和recv()。这些函数都是阻塞的,也就是说,如果没有数据可接收,它们会一直等待,直到有数据可用。这种阻塞型接口是指系统调用(一般是IO接口)如果不返回结果就一直等待。在网络IO中,当一个read操作发生时,它会经历两个阶段:等待数据准备和将数据从内核拷贝到进程中。\[1\]\[2\] 在Python的IO编程中,常见的操作包括网络操作、文件操作和终端操作。网络操作包括建立socket对象、进行连接、发送和接收数据等。文件操作包括建立file对象,进行文件的读写操作。终端操作包括进行交互式输入输出等操作。\[2\] 举个例子,如果想要进行网络编程中的广播接收,可以使用socket模块中的相关函数。首先,需要创建一个UDP套接字,并设置套接字为可以接收广播。然后,选择接收端口,并在一个循环中接收广播消息。广播发送的代码类似,需要创建一个UDP套接字,并设置套接字为可以发送广播。然后,使用sendto()函数发送广播消息。\[3\] 总结来说,Python的IO网络编程主要涉及socket模块,通过使用socket模块中的函数来进行网络操作、文件操作和终端操作。 #### 引用[.reference_title] - *1* *2* [python网络编程——网络IO模型](https://blog.csdn.net/JackLiu16/article/details/87838941)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [python IO网络编程](https://blog.csdn.net/weixin_30802171/article/details/96956828)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值