UNP Chapter 13 - 高级I/O函数
13.1. 概述
首先是一个I/O函数设置超时,这有三种方法。然后是三个read和write函数的变体。
13.2. 套接口超时
有三种方法给套接口上的I/O操作设置超时
1. 调用alarm,在到达指定时间时产生SIGALRM信号
2. 使用select阻塞在等待I/O上,select内部有一个时间限制,以此代替在read或write调用上阻塞
3. 使用新的SO_RCVTIMEO和SO——SNDTIMEO套接口选项
前两种技术可以用于任何描述字,而第三种只能用于套接口描述字。
用SIGALRM给connect设置超时
//用SIGALRM给connect设置超时
//下面是函数connect_timeo的代码,它以调用者设定的时间上限来调用connect。前三个参数是connect所需的,第四个参数是等待的秒数
#include "unp.h"
static void connect_alarm(int);
int connect_timeo(int sockfd, const SA * saptr, socklen_t salen, int nsec)
{
Sigfunc * sigfunc;
int n;
sigfunc = Signal(SIGALRM, connect_alarm); //为SIGALRM建立一个信号处理程序,现有的信号处理程序(如果有的话)被保存,因此它可以在这个函数结束时恢复
if(alarm(nsec) != 0) //进程的报警时钟设成调用者指定的秒数,alarm的返回值是进程的报警时钟(如果进程设置过一个报警时钟)现在剩下的秒数或0(如果进程当前没有设置时钟)
err_msg("connect_timeo: alarm was already set");
if((n = connect(sockfd, (struct sockaddr *)saptr, salen)) < 0)//调用connect,如果被中断(EINTR),则将errno的值设为ETIMEOUT. 关闭该套接口以防止继续进行三路握手
{
close(sockfd);
if(errno == EINTR)
errno == ETIMEOUT;
}
alarm(0); /* turn off the alarm */
Signal(SIGALRM, sigfunc); /* restore previous signal handler */
return(n);
}
static void connect_alarm(int signo)
{
return; /* just interrupt the connect() */
}
//用SIGALRM为recvfrom设置超时
#include "unp.h"
static void sig_alrm(int);
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE+1];
Signal(SIGALRM, sig_alrm);
while(Fgets(sendline, MAXLINE, fp) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
alarm(5); //在每次recvfrom前调用alarm设置5秒的超时
if((n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL)) < 0)
{
if(errno == EINTR)
fprintf(stderr, "socket timeout \n");
else
err_sys("recvfrom error");
}
else
{
alarm(0);
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}
}
static void sig_alrm(int signo)
{
return; /* just interrupt the recvform() */
}
// 用select为recvfrom设置超时
// 名为readable_timeo的函数,以指定的秒数等待一个描述字变成可读
# include "unp.h"
int readable_timeo(int fd, int sec)
{
fd_set rset;
struct timeval tv;
FD_ZERO(&rset);
FD_SET(fd, &rset); //在读描述字集中打开要读的描述字所对应的位,timeval结构设成调用者到等待的秒数
tv.tv_sec = sec;
tv.tv_usec = 0;
return(select(fd+1, &rset, NULL, NULL, &tv));//select等待描述字变为可读或者超时,函数不执行读操作,它只是等待描述字变成可读,因此它可以用在任何类型套接口上
/* >0 if descriptor is readable */
}
#include "unp.h"
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socklen_t servlen)
{//直到readable_timeo告诉我们描述字可读后我们才可以调用recvfrom,这保证了recvfrom不会阻塞
int n;
char sendline[MAXLINE], recvline[MAXLINE+1];
while(Fgets(sendline, MAXLINE, fp) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
if(Readable_timeo(sockfd, 5) == 0)
{
fprintf(stderr, "socket timeout \n");
}
else
{
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}
}
用SO_RCVTIMEO套接口选项为recvfrom设置超时
一旦为每个描述字设置了这个选项,并指定了超时值,那么这个超时对该描述字上的所有操作都起作用。这种方法的好处是只需要设置一次选项,而之前两种方法都要每次操作前都设一遍超时。但这个套接口选项只对读操作起作用,与此类似SO_SNDTIMEO只对写操作起作用,他们都不能为connect设置超时。
#include "unp.h"
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE+1];
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
Setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));//第四个参数是一个指向timeval结构的指针,其中填入超时时间。
while(Fgets(sendline, MAXLINE, fp) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
if(n<0)
{
if(errno == EWOULDBLOCK)
{
fprintf(stderr, "socket timeout \n");
continue;
}
else
err_sys("recvfrom error");
}
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}
13.3. recv和send函数
这两个函数和标准的read和write函数都类似,只是多了一个附加参数
#include <sys/socket.h>
sszie_t recv(int sockfd, void * buff, size_t nbytes, int flags); //返回: 成功返回读入或写出的字节数,出错返回-1
ssize_t send(int sockfd, void * buff, size_t nbytes, int flags);//返回: 成功返回读入或写出的字节数,出错返回-1
flag在设计上存在一个基本问题:它是按值传递的,而不是值-结果参数,因此它只能从进程向内核传递标志,内核不能向进程传递标志。
13.4. readv和writev函数
这两个函数与read和write类似,但readv和writev可以让我们在一个函数中读或写多个缓冲区,这些操作被称为分散读(因为输入数据分散到多个应用缓冲区中)和集中写(因为多个缓冲区被集中到一次写操作中)。
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec * iov, int iovcnt); //返回: 读到或写出的字节数,出错时为-1
ssize_t writev(int filedes, const struct iovec * iov, int iovcnt); //返回: 读到或写出的字节数,出错时为-1
两个函数的第二个参数都是一个指向iovec结构的数组的指针,在<sys/uio.h>头文件中定义:
struct iovec {
void * iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
}
readv和writev函数可以用于任何描述字,不仅限于套接口描述字,而且writev是一个原子操作。
13.5. recvmsg和sendmsg函数
这两个函数是最通用的I/O函数,实际上,可以用recvmsg代替read, readv, recv, recvfrom. 同样,各种输出函数可以用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
两个函数把大部分参数都包装到一个msghdr结构中:
struct msghdr
{
void * msg_name; /* protocol address */
socklen_t msg_namelen; /* size of protocol address */
struct iovec * msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void * msg_control /* ancillary data, must be aligned for a cmsghdr structure */
socklen_t msg_controllen; /* length of ancillary data */
int msg_flags; /* flags returned by recvmsg() */
};
13.6 辅助数据
可以在sendmsg和recvmsg时使用msghdr结构中的msg_contorl和msg_controllen成员发送和接收辅助数据(ancillary data)。辅助数据的另一种叫法是控制信息(control information)
辅助数据是一个或多个辅助数据对象组成,每个对象由一个cmsghdr结构开头,该结构在<sys/socket.h>文件中定义如下:
struct cmsghdr
{
socklen_t cmsg_len; /* length in bytes, including this structure */
int cmsg_level; /* orignating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by unsigned char cmsg_data[] */
};
msg_control指向的辅助数据必须按cmsghdr结构进行对齐
因为由recvmsg返回的辅助数据可以包含任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充字节,在<sys/socket.h>中定义了以下五个宏,以简化对辅助数据的处理
#include <sys/socket.h>
#include <sys/param.h> /* for ALIGN macro on many implemetations */
struct cmsghdr * CMSG_FIRSTHDR(struct msghdr * mhdrptr); //返回: 指向第一个cmsghdr结构的指针,无辅助数据时为NULL
struct cmsghdr * CMSG_NXTHDR(struct msghdr * mhdrptr, struct cmsghdr * cmsgptr); //返回: 指向下一个cmsghdr结构的指针,不再有辅助数据对象时为NULL
unsigned char * CMSG_DATA(struct cmsghdr * cmsgptr); //返回: 指向与cmsghdr结构关联的数据的第一个字节的指针
unsigned int CMSG_LEN(unsigned int legnth); //返回: 给定数据量下存储在cmsg_len中的值
unsigned int CMSG_SPACE(unsigned int length); //返回: 给定数据量下一个辅助数据对象的大小
13.7 排队的数据量
在不读出数据的情况下,如何知道一个套接口的接收队列中有多少数据可读呢?有三种方法:
1. 如果在没有数据可读时还有其他事情要做,为了不阻塞在内核中,可以使用非阻塞I/O
2. 如果想检查一下数据而使数据仍留在接收队列中,可以使用MSG_PEEK标志。如果想这样做,但又不能肯定是否有数据可读,可以把这个标志和非阻塞套接口相结合,或与MSG_DONTWAIT标志结合使用。
3. 一些实现支持ioctl的FIONREAD命令
13.8. 套接口和标准I/O
目前为止的所有例子中使用的read及write函数和它们的一些变种(recv, send等),称为Unix I/O,这些函数使用描述字工作,通常作为Unix内核中的系统调用来实现的。
进行输入输出的另一种方法是标准I/O函数库(stand), 在ANSI C的标准中对此进行了说明,其目的是方便移植到支持ANSI C的非Unix系统上。
标准I/O库可以用于套接口,但有几点要考虑:
1. 对任何描述字调用fdopen函数都可以生成一个标准I/O流,同样,对于一个标准I/O流,用fileno可以得到对应的描述字。
2. TCP和UDP套接口是全双工的。标准I/O流也可以是全双工的: 只要以r+方式打开流即可,r+意味着读写。但是对这样的流不能在一个输出函数之后紧接一个输入函数,必须插入一个fflush, fseek, fsetpos或rewind调用。同样,不能在一个输入函数之后紧接一个输出函数,必须插入一个fseek,fsetpos或rewind调用, 除非输入函数遇到一个文件结束符。后三个函数的问题是它们都调用lseek,可lseek在套接口上会失败
3. 解决这个读写问题的最简单方法是,对一个套接口打开两个标准I/O流:一个读,一个写。
// 下面我们用标准I/O代替readline和writen重新编写TCP回射服务器,下面是使用标准I/O的str_echo函数的新版本
#include "unp.h"
void str_echo(int sockfd)
{
char line[MAXLINE];
FILE * fpin, * fpout;
fpin = Fdopen(sockfd, "r");
fpout = Fdopen(sockfd, "w");
for( ; ; )
{
if(Fgets(line, MAXLINE, fpin) == NULL)
return; /* connection closed by other end */
Fputs(line, fpout);
}
}
问题是服务器上的标准I/O库自动进行缓冲,标准I/O库执行三种缓冲:
1. 完全缓冲意味着只有在以下情况时才进行I/O:缓冲区满,进程明确地调用fflush或进程调用exit终止。标准I/O缓冲区大小通常为8192字节。
2. 行缓冲意味着在以下情况时进行I/O:遇到一个换行符,进程调用fflush或进程调用exit终止。
3. 不缓冲意味着每次调用标准I/O输出函数时都进行I/O。
大多数Unix中标准I/O库的实现遵循了以下规则:
1. 标准错误输出总是不缓冲。
2. 标准输入和标准输出是全缓冲的。除非他们是一个终端设备,那样的话他们是行缓冲。
3. 其他的流都是全缓冲的,除非他们是一个终端设备,那样的话他们是行缓冲。
既然套接口不是终端设备,上面str_echo函数的问题就在于输出流(fpout)是全缓冲的。有两种解决方法:调用setvbuf将输出流强制成行缓冲的,或者在每次fputs之后调用fflush强制输出回射行。
13.9. T/TCP:事务TCP
T/TCP是在TCP的基础上做了少量的修改,以避免在最近通信过的主机之间进行三路握手。T/TCP能将SYN,FIN和数据组合到单个分节中,前提是数据的长度小于MSS。
13.10. 小结