问题描述:
服务器响应数据无限地丢失,客户端没有接收到数据,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
信号到底是缓冲区中有数据过来触发的,还是说我socket
的recv
返回-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);
}
运行结果:
之前说过了,由于是测试超时,所以客户端只做接收。服务器端接收不到数据,每隔两秒发送一个我没收到
给客户端,发送五次后,还没收到客户端的程序,则告知服务端用互无法构建通信,然后退出。