Bootstrap

socket 笔记(阻塞处理--非阻塞I/O和超时处理)

问题描述:

服务器响应数据无限地丢失,客户端没有接收到数据,recv()无限期的阻塞程序。

以下是我在看完TCP/IP Socket编程总结的。

1、非阻塞I/O:
非阻塞套接字:

更改套接字的行为,使得所有调用都是非阻塞的。对于这样的套接字,如果请求可以立即完成,调用的返回值就会指示成功,否则就会指示失败。不管成功还是失败,调用都不会无限期的阻塞。如果失败是由于调用被阻塞而发生的,系统就会把error设置为EWOULDBLOCK。而connect失败返回的是EINPROGRESS

linux中可以调用fcntl() (file control)来更改默认的阻塞行为。

//函数体
int fcnt1(int socket,int command,...)
//设置套接字IO为非阻塞模式
fcnt(servSock,F_SETFL,FASYNC)

函数中,socket必须是一个有效的套接字(或文件)描述符。command是要执行的操作,它是系统定义的常量,与errno和信号差不多,都是一个整型数对应一种操作。更改阻塞行为只是一种操作,其余的还有很多。

异步I/O:

由于设置了非阻塞模式,所以套接字的行为就变成了,发送一次recv()请求,如果缓冲区有东西就读取,没有东西就返回-1,不做停留,但是如果我们使用类似while(1)无线循环请求判断是否有数据,那和阻塞了有什么区别。所以需要一种方法,当缓冲区中有数据过来时,就通知socket过来取,没有数据时就各做各的。这样的话,由于不会阻塞了,所以可以放心的执行主函数中的其他代码了,当有数据过来时,再去取数据。

打个比方,recv()sendto()本身就是从接收缓冲区中读取和向发送缓冲区中写入,缓冲区就像一个大池子,每次舀一点出来(recv()),同时水管(sendto())不定期的向池子里注水。假如水舀干了,即recv()返回-1,就不舀水去干别的事情了,等再有水注入时,就过来通知人再来舀水。

异步I/O所做的事情是:当套接字上上发生某个与I/O相关的事件时,把SIGIO信号传递给进程。这是书上原话,第一次看到时感觉很模糊,我不知道IO信号到底是缓冲区中有数据过来触发的,还是说我socketrecv返回-1触发的。

//绑定异步IO信号和action
sigaction(SIGIO,&handler,0)

为此,我不得不花了好一阵来查阅资料,无果。后来自己写了段代码,做了一下验证。

服务器端代码中,创建IO信号任务,客户端则负责向服务器端发送数据。下面实验代码我没给全,不过可以拿下面程序实现那一块的代码跟着下面的代码修改,只要修改IO信号绑定的action任务即可。

//服务器端部分代码
servSock = socket(servAddr->ai_family, servAddr->ai_socktype,servAddr->ai_protocol);   //套接字
bind(servSock, servAddr->ai_addr, servAddr->ai_addrlen);
struct sigaction handler;          //信号量
handler.sa_handler = SIGIOHandler;  //设置信号量的action
sigfillset(&handler.sa_mask);//sigfillset表示将加入的信号集初始化,一般将加入的标志位设置为1
handler.sa_flags = 0;    //标志置0,无标志    
sigaction(SIGIO, &handler, 0);//绑定IO信号与action
fcntl(servSock, F_SETOWN, getpid());   //F_SETOWN在这里是set owner的意思,getpid()获取进程的pid,设置套接字归属于这个进程,只被这个进程使用
fcntl(servSock, F_SETFL, O_NONBLOCK | FASYNC); //设置套接字为非阻塞模式和中断模式
for (;;)          
	UseIdleTime();   //模拟应答任务


//信号绑定的action任务
void SIGIOHandler(int signalType) {
    puts("异步I/O触发了");
}


//模拟应答任务
void UseIdleTime() {
	puts(".");
	sleep(3);
}
//客户端部分代码
//仅作发送,拿我之前博客里敲的代码就能复用了
int sock = socket(servAddr->ai_family,servAddr->ai_socktype,servAddr->ai_protocol);  //创建套接字
ssize_t numBytes = sendto(sock,echoString,echoStringLen,0,servAddr->ai_addr,servAddr->ai_addrlen);   //数据写入缓冲区  //返回发送数据长

运行结果:
在这里插入图片描述
结果很明显了,每次我运行客户端程序向服务端发送数据的时候,服务端就输出文字,即触发了异步IO信号。

步骤:
  • 绑定IO信号和action
  • 指派套接字的拥有者(进程),确保套接字只被这个进程使用(同一套接字可以被多个进程访问)。
  • 设置套接字io为非阻塞和中断模式,原本的阻塞模式下,套接字未接收到数据就会一直阻塞着,设置非阻塞模式后,套接字在一次请求后未收到数据的情况下,就会立即停止从缓冲区收取数据。
  • 凡是从缓冲区读取到了数据,就继续尝试下一次读取,一旦未接收到数据则立即结束,然后等待下一次的IO信号触发再进行读取和判断。

回到最之前的问题,发送的数据丢失,导致无限期的阻塞的情况在这里就不存在了,没有收到就不会进行通知,我也就不会作出响应,更不会对程序造成阻塞了。

程序实现:

由于书上只给了服务端代码,而且代码我在运行时有一些问题,所以我自己写了客户端代码,以及在服务端代码的基础上做了很多修改,直接把代码放上来了,上面我做了很多注释:

//服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include "Practical.h"   //这个脚本可以在我之前关于tcp通信的笔记中找到代码
#define MAXSTRINGLENGTH 8192

void UseIdleTime();   //没有数据过来时执行的函数
void SIGIOHandler(int signalType);     //IO信号响应的action

int servSock;     //套接字

int main(int argc, char *argv[]) {
	if (argc != 2)    //参数不足或多于2个时提示错误
		DieWithUserMessage("Parameter(s)", "<Server Port/Service>");
	char *service = argv[1];

	struct addrinfo addrCriteria;   //地址标准
	memset(&addrCriteria, 0, sizeof(addrCriteria));  //全部置0

	addrCriteria.ai_family = AF_UNSPEC;   //任意地址簇,IPV4或IPV6均可
	addrCriteria.ai_flags = AI_PASSIVE;    //accept所有address/port
	addrCriteria.ai_socktype = SOCK_DGRAM;    //数据报套接字
	addrCriteria.ai_protocol = IPPROTO_UDP;   //使用UDP协议

	struct addrinfo *servAddr;   //地址信息List
	int rtnVal = getaddrinfo(NULL, service, &addrCriteria, &servAddr);  //获取服务(参数输入)的地址信息(动态链表)
	if (rtnVal != 0)
		DieWithUserMessage("getaddrinfo() failed", gai_strerror(rtnVal));       //获取失败

	servSock = socket(servAddr->ai_family, servAddr->ai_socktype,servAddr->ai_protocol);   //套接字
	if (servSock < 0)
		DieWithSystemMessage("socket() failed");    //创建套接字失败

	if (bind(servSock, servAddr->ai_addr, servAddr->ai_addrlen) < 0)       //绑定端口
		DieWithSystemMessage("bind() failed");            //绑定失败

	freeaddrinfo(servAddr);       //释放地址信息的动态链表

	struct sigaction handler;          //信号量
	handler.sa_handler = SIGIOHandler;  //设置信号量的action

	if (sigfillset(&handler.sa_mask) < 0)    //sigfillset表示将加入的信号集初始化,一般将加入的标志位设置为1
		DieWithSystemMessage("sigfillset() failed");      //标志位设置失败

	handler.sa_flags = 0;        //标志置0,无标志

	if (sigaction(SIGIO, &handler, 0) < 0)    //绑定IO信号与action
		DieWithSystemMessage("sigation() failed for SIGIO");     //绑定失败

	if (fcntl(servSock, F_SETOWN, getpid()) < 0)        //F_SETOWN在这里是set owner的意思,getpid()获取进程的pid,设置套接字归属于这个进程,只被这个进程使用
		DieWithSystemMessage("Unable to set process owner to us");    //设置owner失败

	if (fcntl(servSock, F_SETFL, O_NONBLOCK | FASYNC) < 0)     //设置套接字为非阻塞模式和中断模式
		DieWithSystemMessage("Unable to put client sock into non-blocking/async mode");     //设置失败

	for (;;)          
		UseIdleTime();   //没有数据过来时执行的函数
}

void UseIdleTime() {
	puts(".");
	sleep(3);
}

void SIGIOHandler(int signalType) {
	ssize_t numBytesRcvd;     //缓冲区接收的数据长度
	do {
		struct sockaddr_storage clntAddr;
		size_t clntLen = sizeof(clntAddr);
		char buffer[MAXSTRINGLENGTH];

		numBytesRcvd = recvfrom(servSock, buffer, MAXSTRINGLENGTH, 0, (struct sockaddr *)&clntAddr, &clntLen);   //缓冲区接收数据,由于设置了非阻塞,所以numBytesRcvd立马获得一个返回值
                printf("从客户端接收到的数据:%s\n",buffer);

		if (numBytesRcvd < 0) {   //未收到数据返回 -1,由于while循环条件判断的就是numBytesRcvd,所以立即跳出。操作完成触发下一个IO信号再做判断
			if (errno != EWOULDBLOCK)   //这里EWOULDBLOCK为阻塞错误(设置了非组赛模式不会是此错误)
				DieWithSystemMessage("recvfrom() failed");   //接受失败
		}
		else {
			ssize_t numBytesSent = sendto(servSock, buffer, numBytesRcvd, 0, (struct sockaddr *)&clntAddr, sizeof(clntAddr));    //将接收到的数据回显,放到发送缓冲区
                        printf("已发往客户端的数据:%s\n",buffer);
			if (numBytesSent < 0)
				DieWithSystemMessage("sendto() failed");   //写入缓冲区失败
			else if (numBytesSent != numBytesRcvd)         //发送的数据长度和接收的数据长度不一致,有数据丢失
				DieWithUserMessage("sendto()", "sent unexpected number of bytes");
		}
	} while (numBytesRcvd >= 0);  //当从缓冲区读取到了数据就继续进行下一次读取
	                             //,如果接收缓冲区中未读取到数据则结束,任务完成再次触发下一个IO信号
	                             //,再进行下一次尝试
}
//客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include "Practical.h"
#include <stdbool.h>

#define size 8192   //socket单次读取和写入长度

bool SockAddrsEqual(const struct sockaddr *addr1, const struct sockaddr *addr2) {   //比较两个地址是否相同
  if (addr1 == NULL || addr2 == NULL)
    return addr1 == addr2;
  else if (addr1->sa_family != addr2->sa_family)
    return false;
  else if (addr1->sa_family == AF_INET) {
    struct sockaddr_in *ipv4Addr1 = (struct sockaddr_in *) addr1;
    struct sockaddr_in *ipv4Addr2 = (struct sockaddr_in *) addr2;
    return ipv4Addr1->sin_addr.s_addr == ipv4Addr2->sin_addr.s_addr&& ipv4Addr1->sin_port == ipv4Addr2->sin_port;} 
  else if (addr1->sa_family == AF_INET6) {
    struct sockaddr_in6 *ipv6Addr1 = (struct sockaddr_in6 *) addr1;
    struct sockaddr_in6 *ipv6Addr2 = (struct sockaddr_in6 *) addr2;
    return memcmp(&ipv6Addr1->sin6_addr, &ipv6Addr2->sin6_addr,sizeof(struct in6_addr)) == 0 && ipv6Addr1->sin6_port== ipv6Addr2->sin6_port;
  } else
    return false;
}

int main(int argc, char *argv[])
{
    char *server = "192.168.1.5";    //服务器地址
    char *echoString = "你好!我是客户端!"; 
    size_t echoStringLen = strlen(echoString);
    if(echoStringLen>size)     //检验输入格式
        DieWithUserMessage(echoString,"字符串超长");
    char *servPort = "5";    //服务器端口
    struct addrinfo addrCriteria;    
    memset(&addrCriteria,0,sizeof(addrCriteria));   //0补齐
    addrCriteria.ai_family = AF_UNSPEC;          //地址族
    addrCriteria.ai_socktype = SOCK_DGRAM;
    addrCriteria.ai_protocol = IPPROTO_UDP; 
    struct addrinfo *servAddr;
    int rtnVal = getaddrinfo(server,servPort,&addrCriteria,&servAddr);      //输入地址和端口,分别返回地址和端口的链表 
    if (rtnVal !=0)
       DieWithUserMessage("地址端口解析失败",gai_strerror(rtnVal));
    int sock = socket(servAddr->ai_family,servAddr->ai_socktype,servAddr->ai_protocol);  //创建套接字
    if (sock < 0)
       DieWithSystemMessage("创建套接字失败");
    ssize_t numBytes = sendto(sock,echoString,echoStringLen,0,servAddr->ai_addr,servAddr->ai_addrlen);   //数据写入缓冲区  //返回发送数据长
    printf("已发送数据:%s\n",echoString);
    if(numBytes<0)
       DieWithSystemMessage("数据发送失败");
    else if (numBytes != echoStringLen)
       DieWithUserMessage("sendto() error","发送数据有字节丢失");
       freeaddrinfo(servAddr);
       exit(0);
}

运行结果:
在这里插入图片描述
由于绑定了进程,需要用管理员运行,否则会提示bind()没有权限。服务端提供一个端口参数,客户端里我用的是端口号5,所以服务端也用5

2、超时:
实现:

有时UDP发生丢失,对于客户端这种丢失掉的数据将永远接收不到,但是用户不能判断是否真正发生了丢失,所以可以和服务端约定等待一定的响应时间,在响应时间内服务端还没有响应客户端的请求,那么服务器就永远都不会对这个请求响应。客户端没有收到响应,要么就放弃,要么就重新发送一次请求。

实现超时机制的方法是在调用阻塞函数之前设置alarm,其声明形式为:

unsigned int alarm(unsigned int secs)    //此处secs是一个秒数

alarm()会在阻塞函数如recv()之前启动一个定时器,这个定时器在经过secs秒后计时结束,计时结束时将会发送一个SIGALRM信号给进程,并执行信号绑定的action处理函数。alarm()会为预订过的报警返回会剩余秒数,没有预订报警,其返回值会是0

程序实现中,新客户为SIGALRM安装了一个处理程序,其位置在调用recvfrom()之前,两秒钟设置一个报警。在间隔末尾递送SIGALRM信号,并调用处理程序。当处理程序返回是,阻塞的recvfrom()将返回-1,并把errno设置为EINTR。客户然后把应答请求重新发送给服务器。应答请求的这种超时和重传动作最多会发生5次,之后客户就会放弃并报告失败。

代码实现:

代码我还是放在下面,同样客户端程序是我自己写的,且服务端程序添加了很多注释。书上代码有两处有问题,程序不能正常运行,我改过来了并且在上面做了注释,正确运行了,其他地方也有一些修改,不过改动不大。

由于主要观察超时现象,而服务端接收到数据也只是显示出来,所以干脆就客户端代码不发送,只做接收好了。

以下这段服务端代码仅针对一次的收发,收不到信息向客户端发送5次消息后服务端还没收到,服务器就结束recvfrom,一但收到消息就将消息输出并结束执行。

服务器端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include "Practical.h"

#define MAXSTRINGLENGTH   8192

static const unsigned int TIMEOUT_SECS = 2;   //定时器计时
static const unsigned int MAXTRIES = 5;     //最多尝试次数

unsigned int tries = 0;            //已尝试次数
  
void CatchAlarm(int ignored);       //每触发一次Alarm信号,尝试次数就加 1 
  
int main(int argc,char *argv[]){
 
    char *server = "192.168.1.5";        //IP或服务,getaddrinfo会自动解析
    char *echoString = "我没收到";      //响应的消息
    size_t echoStringLen = strlen(echoString);    //消息长度
    if(echoStringLen > MAXSTRINGLENGTH)
        DieWithUserMessage(echoString,"too long");

    char *service = "8000";  //包括地址有 4 个参数,是则取 argv[3] ,否则 "echo"
    struct addrinfo addrCriteria;
    memset(&addrCriteria,0,sizeof(addrCriteria));    //置零
    addrCriteria.ai_family = AF_UNSPEC;         
    addrCriteria.ai_socktype = SOCK_DGRAM;
    addrCriteria.ai_protocol = IPPROTO_UDP;

    struct addrinfo *servAddr;
    //解析地址
    int rtnVal =getaddrinfo(server,service,&addrCriteria,&servAddr);   
    if (rtnVal != 0)
        DieWithUserMessage("getaddrinfo() failed",gai_strerror(rtnVal));
    //创建套接字
    int sock = socket(servAddr->ai_family,servAddr->ai_socktype,servAddr->ai_protocol);
    if (sock<0)
        DieWithSystemMessage("socket() failed");
    //信号关联的action执行函数
    struct sigaction handler;
    handler.sa_handler = CatchAlarm;
    //初始化标志
    if(sigfillset(&handler.sa_mask)<0)
        DieWithSystemMessage("sigfillset() failed");
    handler.sa_flags = 0;
    //绑定 alarm 信号的action
    if (sigaction(SIGALRM,&handler,0)<0)
        DieWithSystemMessage("sigaction() failed for SIGALRM");
    //将 "我没收到" 发送给对方
    ssize_t numBytes = sendto(sock,echoString,echoStringLen,0,servAddr->ai_addr,servAddr->ai_addrlen);

    if(numBytes < 0)
        DieWithSystemMessage("sendto() failed");
    else if(numBytes != echoStringLen)
        DieWithUserMessage("sendto() error","sent unexpected number of bytes");

    struct sockaddr_storage fromAddr;  //网络编程通用结构体,功能与 sockaddr 相似
    socklen_t fromAddrLen = sizeof(fromAddr);

    alarm(TIMEOUT_SECS);  //设定alarm的定时时延

    char buffer[MAXSTRINGLENGTH + 1];

    //当没有接收到数据,陷入阻塞时发送至多五次消息给对面
    while ((numBytes = recvfrom(sock,buffer,MAXSTRINGLENGTH,0,(struct sockaddr *) &fromAddr,&fromAddrLen))<0){ 

        if (errno==EINTR)  {   //定时结束,触发 alarm 信号
            if(tries<MAXTRIES){  //少于5次
                //最多 5 次每隔两秒将 "我没收到" 发送给对方
                numBytes = sendto(sock,echoString,echoStringLen,0,(struct sockaddr*) servAddr->ai_addr,servAddr->ai_addrlen);
            if (numBytes<0)
                DieWithSystemMessage("sendto() failed");
            else if (numBytes != echoStringLen)
                DieWithUserMessage("sendto() error","sent unexpected number of bytes");
             }
         else
             //发送大于五次了,不再发送,告诉客户端无法收到
             DieWithUserMessage("No Response","unable to communicate with server");
         }
         else
             DieWithSystemMessage("recvfrom() failed");
      }

//收到数据了,跳出while循环
alarm(0);  //alarm定时时延置0
freeaddrinfo(servAddr);  //书上这一句没加,应该是忘了
buffer[echoStringLen] = '\0';  //清空buffer
printf("Received: %s\n",buffer);   //打印收到的信息
 
close(sock);
exit(0);
}

void CatchAlarm(int ignored){  //alarm信号触发函数,尝试次数增1
    tries += 1;
    alarm(TIMEOUT_SECS);  //书上的代码中没有加这一句,然后信号就触发一次就结束了,加了这一句以后正常执行
}           

客户端代码:

//客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include "Practical.h"
#include <stdbool.h>

#define size 8192

bool SockAddrsEqual(const struct sockaddr *addr1
                    , const struct sockaddr *addr2) {   //比较两个地址是否相同
  if (addr1 == NULL || addr2 == NULL)
    return addr1 == addr2;
  else if (addr1->sa_family != addr2->sa_family)
    return false;
  else if (addr1->sa_family == AF_INET) {
    struct sockaddr_in *ipv4Addr1 = (struct sockaddr_in *) addr1;
    struct sockaddr_in *ipv4Addr2 = (struct sockaddr_in *) addr2;
    return ipv4Addr1->sin_addr.s_addr == ipv4Addr2->sin_addr.s_addr&& ipv4Addr1->sin_port == ipv4Addr2->sin_port;} 
  else if (addr1->sa_family == AF_INET6) {
    struct sockaddr_in6 *ipv6Addr1 = (struct sockaddr_in6 *) addr1;
    struct sockaddr_in6 *ipv6Addr2 = (struct sockaddr_in6 *) addr2;
    return memcmp(&ipv6Addr1->sin6_addr, &ipv6Addr2->sin6_addr,sizeof(struct in6_addr)) == 0 && ipv6Addr1->sin6_port== ipv6Addr2->sin6_port;
  } else
    return false;
}

int main(int argc, char *argv[])
{
    char *server = "192.168.1.5";    //服务器地址
    char *servPort = "8000";    //服务器端口
	struct addrinfo addrCriteria;    
	memset(&addrCriteria,0,sizeof(addrCriteria));   //0补齐
	addrCriteria.ai_family = AF_UNSPEC;          //地址族
	addrCriteria.ai_socktype = SOCK_DGRAM;
	addrCriteria.ai_protocol = IPPROTO_UDP; 
	struct addrinfo *servAddr;
	int rtnVal = getaddrinfo(server,servPort,&addrCriteria,&servAddr);      //输入地址和端口,分别返回地址和端口的链表 
	if (rtnVal !=0)
	  DieWithUserMessage("地址端口解析失败",gai_strerror(rtnVal));
       
	int sock = socket(servAddr->ai_family,servAddr->ai_socktype,servAddr->ai_protocol);  //创建套接字
	if (sock < 0)
	  DieWithSystemMessage("创建套接字失败");
        if(bind(sock,servAddr->ai_addr,servAddr->ai_addrlen)<0)  //绑定端口
	  DieWithSystemMessage("端口绑定失败");

	struct sockaddr_storage fromAddr;
	socklen_t fromAddrLen = sizeof(fromAddr);
	char buffer[size +1]; 
        
        for(;;){ 
	    ssize_t numBytes = recvfrom(sock,buffer,size,0,(struct sockaddr *) &fromAddr, &fromAddrLen);   //从缓冲区读数据
	    if (numBytes <0)
	       DieWithSystemMessage("接收数据失败");
            
            puts(buffer);
	}
	freeaddrinfo(servAddr);
	buffer[size] = '\0';
	close(sock);
	exit(0);
}
运行结果:

在这里插入图片描述

之前说过了,由于是测试超时,所以客户端只做接收。服务器端接收不到数据,每隔两秒发送一个我没收到给客户端,发送五次后,还没收到客户端的程序,则告知服务端用互无法构建通信,然后退出。

;