本章讨论了以下问题:
1.设置超时的三种方法
2.read/write
函数的变体:recv/send
readv/writev
recvmsg/sendmsg
上述提到有三种方式为套接字I/O操作设置超时:
1.调用alarm,超时将产生
SIGALRM
信号
2.select函数最后一个参数为timeout设置
3.SO_RCVTIMEO
和SO_SNDTIMEO
套接字选项
书中编写函数int connect_timeo
用来超时等待固定时间,前三个参数是connect
的参数,最后一个参数是超时等待的时间(秒)。以下是注释版本:
int
connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
Sigfunc *sigfunc;//信号处理函数
int n;
//注册信号处理函数
//connect_alarm,收到SIGALRM信号return
sigfunc = Signal(SIGALRM, connect_alarm);//signal返回connect_alarm函数指针
//如果alarm返回不为0说明之前有设置过报警时钟
if (alarm(nsec) != 0)
err_msg("connect_timeo: alarm was already set");
if ( (n = connect(sockfd, saptr, salen)) < 0) //connect函数
{
close(sockfd);
//这里if是防止如果调用被中断(EINTR错误),修改ETIMEOUT
if (errno == EINTR)
errno = ETIMEDOUT;
}
//关闭alarm
alarm(0);
//恢复原来的信号处理函数
Signal(SIGALRM, sigfunc);
return(n);
}
static void
connect_alarm(int signo)
{
return;
}
注:一直不理解书中恢复原来的信号处理函数是什么意思,后来
man signal
一蛤,发现:signal函数return的是previous handler
,也就是说上述sigfunc保存了previous handler
,最后Signal(SIGALRM,sigfunc)
,实际上是恢复previous handler
可以直接试一试这个函数,在intro/daytimetcpcli.c
中直接用connect_timeo替换connect,并且设置超时为3秒:
if (connect_timeo(sockfd, (SA *) &servaddr, sizeof(servaddr),3) < 0)
err_sys("connect error,timeout!");
编译,并在终端输入./daytimetcpcli <一个不可达的IP(例如:192.168.1.1)>
,程序阻塞3秒后返回。
这个方法与上述类似。过程就是注册信号处理函数(超时直接return),并在recvfrom之前调用alarm。
select自带超时参数,根据select的返回值来判断(0表示timeout),直接添加即可,仿照书中修改的dg_cli函数,修改之前的udp客户端文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define SERV_PORT 1024
#define MAXLEN 1024
void dg_cli(FILE*,int ,const struct sockaddr*,socklen_t);
int readable_timeo(int fd,int sec);
int main(int argc, char ** argv)
{
int sockfd;
struct sockaddr_in servaddr;
if(argc!=2)
{
printf("usage: udpcli <IPaddress>\r\n");
return -1;
}
memset(&servaddr,0x00,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<0)
{
printf("inet_pton error\r\n");
return -1;
}
sockfd = socket(AF_INET,SOCK_DGRAM,0);
dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
return 0;
}
//该函数直接将描述符和时间传进来,用select来等待可读
int readable_timeo(int fd,int sec)
{
fd_set rset;
struct timeval tv;
FD_ZERO(&rset);
FD_SET(fd,&rset);
tv.tv_sec =sec;
tv.tv_usec =0;
return (select(fd+1,&rset,NULL,NULL,&tv));
}
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+1];
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
sendto(sockfd,sendbuff,strlen(sendbuff),0,pservaddr,servlen);
if(readable_timeo(sockfd,5)==0)//设置超时等待5秒
{
fprintf(stderr,"socket timeout\n");
}
else
{
if((n=recvfrom(sockfd,recvbuff,MAXLEN,0,NULL,NULL))<=0)
{
printf("recvfrom error\r\n");
return ;
}
recvbuff[n]='\0';
fputs(recvbuff,stdout);
}
}
}
直接编译,这里我们不运行udp服务器,直接运行客户端,fgets
一段文本后,等待5秒钟,终端打印出“socket timeout”消息,select超时。
这就要调用setsockopt
函数来设置套接字参数了。
再次修改dg_cli
函数如下:
void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen)
{
int n;
char sendbuff[MAXLEN];
char recvbuff[MAXLEN+1];
struct timeval tv;
tv.tv_sec=5;
tv.tv_usec=0;
if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv))<0)//return < 0 error
{
printf("setsocketopt error\r\n");
}
while(fgets(sendbuff,MAXLEN,fp)!=NULL)
{
sendto(sockfd,sendbuff,strlen(sendbuff),0,pservaddr,servlen);
n=recvfrom(sockfd,recvbuff,MAXLEN,0,NULL,NULL);
if(n<0)
{
if(errno == EWOULDBLOCK)
{
fprintf(stderr,"socket timeout\r\n");
continue;
}
else
{
fprintf(stderr,"recvfrom error\r\n");
}
}
fputs(recvbuff,stdout);
}
}
直接编译,这里我们不运行udp服务器,直接运行客户端,fgets
一段文本后,等待5秒钟,终端打印出“socket timeout”消息。
跟read/write
类似,不过需要额外的参数:
#include <sys/socket.h>
ssize_t recv(int sockfd ,void *buff, size_t nbytes , int flags);
ssize_t send(int sockfd ,const void *buff, size_t nbytes, int flags);
//返回:成功返回读入或写出的字节数,出错为-1
关于flag参数,可以设置为0,也可以设置下列参数:
flags | 说明 | recv | send |
---|---|---|---|
MSG_DONTROUTE | 绕过路由表查找 | X | |
MSG_DONTWAIT | 仅仅本操作非阻塞 | X | X |
MSG_OOB | 发送或接收带外数据 | X | X |
MSG_PEEK | 窥看外来数据 | X | |
MSG_WAITALL | 等待所有数据 | X |
打叉的表示支持,也可以通过man recv
和man send
来了解这些flags的含义。
跟read/write
类似,不过readv/writev
允许单个系统调用读入到或写出自一个或多个缓冲区。这些操作被称作分散读和集中写。来自读操作的输入数据被分散到多个应用缓冲区,来自多个应用缓冲区的输出数据被集中提供给单个写操作。
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov ,int iovcnt);
ssize_t writev(int filedes, const struct iovec * iov , int iovcnt);
第二个参数指向某个iovec结构数组的一个指针,可以设置缓冲区的起始地址和大小。
另外,这两个操作可以应用与任何描述符,而不是仅限于套接字。另外,writev是一个原子操作,以为着对于一个基于记录的协议(UDP协议)而言,一次调用只产生单个UDP数据报。
这两个IO函数是最通用的IO函数,我们可以把所有的read、readv、recv和recvfrom替换成recvmsg调用,各种输出函数调用也可以替换成sendmsg调用。
#include <sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr *msg,int flags);
//返回:成功则为读入或写出的字节数,出错则为-1。
说它们通用,主要是因为他们封装了大部分参数到struct msghdr
里面。
struct msghdr{
void *msg_name; //指向套接字结构体sockaddr_in,用于UDP协议
socklen_t msg_namelen;//长度16个字节
//指定输入或输出缓冲区数组(起始地址,长度等)
struct iovec * msg_iov;
int msg_iovlen;//3因为分配了3个iovec结构构成的数组。
void * msg_control;
socketlen_t msg_controllen;
int msg_flags;
};
函数 | 任何描述符 | 仅套接字描述符 | 单个读/写缓冲区 | 分散/集中读写 | 可选标志 | 可选对端地址 | 可选控制信息 |
---|---|---|---|---|---|---|---|
read/write | X | X | |||||
readv/writev | X | X | |||||
recv/send | X | X | X | ||||
recvfrom/sendto | X | X | X | X | |||
recvmsg/sendmsg | X | X | X | X | X |
通过sendmsg/recvmsg两个函数的msg_control和msg_controllen两个成员发送和接收。辅助数据其实是控制信息。
如果我们想要在不真正读取数据的前提下知道一个套接字上已用多少数据排队等着读取。可用三个技术实现:
1.可以使用非阻塞I/O。
2.如果既想查看数据,又想数据仍然保留在接受队列中以供本进程其他部分稍后读取,那么可以使用MSG_PEEK标志。(需要注意的是:如果使用这个标志来读取套接字上可读数据的大小,在两次调用之间缓冲区可能会增加数据,如果第一次指定使用MSG_PEEK标志,而第二次调用没有指定使用MSG_PEEK标志,那么这两次调用的返回值是一样的,即使在这两次调用之间缓冲区已经增加了数据。)
3.一些实现支持ioctl的FIONREAD命令。该命令的第三个ioctl参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接字接受队列的当前字节数。
用fdopen打开标准输入和输出,修改服务器回射函数str_echo
void echo_str(int sockfd)
{
char line[MAXLEN];
FILE *fpin=fdopen(sockfd,"r");
FILE *fpout=fdopen(sockfd,"w");
char *x;
while((x=fgets(line,MAXLEN,fpin))!=NULL)
{
if(fputs(line,fpout)==EOF)
{
printf("fputs error\r\n");
}
}
}
fdopen创建两个IO流,一个用于输入,一个用于输出,当运行客户,直到输入EOF,才回射所有文本。
PS:这里我没有跑出书上的效果,不知道为什么终端读取不了ctrl+D
。
实际发生的步骤如下:
客户端发送文本到服务器端;
服务器fgets到这段文本,并用fputs回射;
文本被回射到标准IO缓冲区,但不把缓冲区内容写到描述符,因为缓冲区未满;
直到输入EOF字符,客户端发送一个FIN,fgets返回,子进程终止,返回main函数;
exit调用标准的I/O清理函数,缓冲区中的内容被输出。
服务器子进程终止,已连接套接字关闭,TCP四分组终止。
这里就有三个概念了:
1.完全缓冲:缓冲区满、fflush、exit,才发生IO
2.行缓冲:换行符、fflush、exit,才发生IO
3.不缓冲:每次标准IO输出函数都发生IO
本章介绍几种不同的IO方式,有些可能实际情况可能用不到,大概了解一蛤即可。
本章用到了signal和alarm函数,有必要稍微了解以下他们的机制,比如返回值等等。
1.UNIX网络编程——高级I/O函数(十四)
2.《UNP》卷一
热门源码