高级I/O

  首先是在I/O操作上设置超时,三种方法;read和write这两个函数的三个变体:recv和send运行通过第四个参数从进程到内核传递标志:readvhe writev允许指定往其中输入数据或从其中输出数据的缓冲区向量:recvmsg和sendmsg结合了其他I/O函数的所有特性,并具备接受和发送辅助数据的新能力。

套接字设置超时

  1. 使用信号处理函数alarm,不过这样会涉及到信号处理函数的问题,同时还有可能会引起程序中其他alarm函数的处理
  2. 使用select函数,在这个函数的最后一个参数中可以设置时间超时
  3. 使用比较新颖的超时套接字选项SO_RCVTIMEO和SO_SENDTIMEO,属于套接字选项中的内容。并非所有实现都支持这两个套接字选项。

  以上这三个技术都适用于输入和输出操作(read、write及其注入recvfrom、sendto之类的变体)。但是TCP内置的connect函数超时默认为75sselect可用来在connect函数上设置超时的先决条件是相应的套接字处于非阻塞模式,而上述的两个套接字选项对connect并不适用,前两种技术适用于任何技术,第三个技术适用于套接字描述符。

  一:使用SIGALRM为connect设置超时——1.本技术总能减少connect中断超时期限,但是无法延长内核现有的超时,Berkeley内核connect通常值为75s,如果我们指定一个比75s小的值可以实现,但是如果指定比75s大的值,到75s就返回。

//自定义signal函数
typedef void Sigfunc(int);    /* for signal handlers */
Sigfunc *signal1(int signo, Sigfunc *func)
{
    struct sigaction act,oact;
 
    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0; /*标志清零*/
    if (signo == SIGALRM)/*被SIGALRM信号中断的系统调用不重新启动*/
    {
        #ifdef SA_INTERRUPT
            act.sa_flags|=SA_INTERRUPT;//SunOS 4.x
        #endif // SA_INTERRUPT
    }
    else
    {
        #ifdef SA_RESTART
            act.sa_flags |= SA_RESTART;//SVR,4.4BSD
        #endif // SA_RESTART
    }
    if (sigaction(signo, &act, &oact) < 0)
        return SIG_ERR;
    return oact.sa_handler;
}

static void connect_alarm(int signo);
int connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
    Sigfunc *sigfunc;
    int n;
 
    sigfunc = Signal1(SIGALRM, connect_alarm);
    /*如果此前进程已经设置过报警时钟,并且未还未超时,输出错误信息*/
    if (alarm(nsec) != 0)
        err_msg("connect_timeo: alarm was already set");
 
    if ((n = connect(sockfd, saptr, salen)) < 0) {
        /*如果调用被中断,就关闭套接字,防止三路握手继续进行*/
        close(sockfd);
        if (errno == EINTR)
            errno = ETIMEDOUT;
    }
    /*关闭报警时钟*/
    alarm(0);
    /*恢复之前的信号处理函数*/
    Signal1(SIGALRM, sigfunc);
    return n;
}
 
static void connect_alarm(int signo)
{
    return;//just interrupt 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);//返回sig_alarm函数指针

    while (Fgets(sendline, MAXLINE, fp) != NULL) 
    {

        Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

        alarm(5);//recvfrom之前设置5s超时
        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)//中断被阻塞的recvfrom()
{
    return; /* just interrupt the recvfrom() */
}

  三.使用select为recvfrom设置超时

//本函数不执行读操作,他只是等待给定的描述符变为可读,因此本函数适用于
//任何类型的套接字,TCP or UDP
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//readable_timeo返回正值的时候
        {
                if((n=recvfrom(sockfd,recvbuff,MAXLEN,0,NULL,NULL))<=0)
                {
                    printf("recvfrom error\r\n");
                    return ;
                }
                recvbuff[n]='\0';
                fputs(recvbuff,stdout);
        }
    }
}

  四.使用套接字选项为recvfrom设置超时

  SO_RCVTIMEO(读)SO_SENTIMEO(写)两者都不能为connect设置超时,其超时设置将应用于描述符上所有的读操作,一次性设置选项,前面两个方法要求在欲设置时间限制的每个操作发生之前做些工作

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;
    //第四个参数是指向timeval结构的一个指针,填入了期望的超时值
    tv.tv_sec=5;
    tv.tv_usec=0;
    Setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv))

    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)//如果函数超时返回EWOULDBLOCK错误
            {
                fprintf(stderr,"socket timeout\r\n");
                continue;
            }
            else
                fprintf(stderr,"recvfrom error\r\n");
        }
        recvline[n] = 0;
        fputs(recvbuff,stdout);
    }
}

recv和send

#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

  falgs的值为0或:

flags 说明 recv send
 MSG_DONTROUTE 绕过路由表查找      •
 MSG_DONTWAIT 仅本操作非阻塞    •       •
 MSG_OOB     发送或接收带外数据   •   •
 MSG_PEEK   窥看外来消息   •  
 MSG_WAITALL   等待所有数据    •

  这两个函数的前三个参数和read和write的三个参数一样,(都是套接字、缓冲区、缓冲区大小,最后一个参数有一定的讲究,可以简单的设置为0,也可以参考一定的数值设置,其实这个参数就按照默认的0即可,因为其他几种设置没有实际的作用)

  Flags参数在设计上存在一个基本问题,它是按值传递的,而不是一个值-结果参数。因此它只能用于从进程向内核传递标志,内核无法向进程传回标志,对于TCP这一点不成问题,以为TCP不需要从内核向进程传递标志。然而随着OSI协议被加到4.3BSD中,却提出了随输入操作向进程返回    MSG_EOR标志的需求。这样最后的决定为保持常用输入函数(recv和recvfrom)的参数不变,而改变recvmsg和sendmsg所用的msghdr结构。这个决定同时意味着如果一个进程需要由内核更新标志,它就必须调用recvmsg,而不是recv或者recvfrom。

readv和writev

  分散读和集中写。来自读操作的输入数据被分散到多个应用缓冲区,来自多个应用缓冲区的输出数据被集中提供给单个写操作。

  一个4字节的write操作跟一个396个字节的write可能触发Nagle算法,首选办法之一是对这两个缓冲区调用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数据报。

struct iovec {
    void      *iov_base;      /* starting address of buffer */
    size_t    iov_len;        /* size of buffer */
};

  writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

  readv则将读入的数据按上述同样顺序散布到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。

  readv和writev允许单个系统调用读或写操作读入或写出一个或多个缓冲区,成为分散读或集中写,来自读操输入的数据被分散到多个应用缓冲区,来自多个应用缓冲区输入的数据被集中提供给单个写操作,这两个可用于任何描述符

code

#include <sys/uio.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
  ssize_t size;
  char buf1[9];
  char buf2[9];
  struct iovec iov[2];

  fd1=open(argv[1],O_RDONLY);
  fd2=open(argv[2],O_RDONLY);
  fd3=open(argv[3],O_WRONLY);
 
  size=read(fd1,buf1,sizeof(buf1));
  printf(“%s size is:%d\n”,argv[1],size);
  size=read(fd2,buf2,sizeof(buf2));
  printf(“%s size is:%d\n”,argv[2],size);
 
  iov[0].iov_base=buf1;
  iov[0].iov_len=sizeof(buf1);  
  iov[1].iov_base=buf2;
  iov[1].iov_len=sizeof(buf2);

  size=writev(fd3,iov,2));
  printf(“%s size is:%d\n”,argv[3],size);

  close(fd1);
  close(fd2);
  close(fd3);
  return 0;
}
View Code

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);

msghdr

struct msghdr 
{
    void          *msg_name;            /* protocol address */
    socklen_t     msg_namelen;          /* sieze of protocol address */
    struct iovec  *msg_iov;             /* scatter/gather array */
    int           msg_iovlen;           /* # elements in msg_iov */
    void          *msg_control;         /* ancillary data ( cmsghdr struct) */
    socklen_t     msg_conntrollen;      /* length of ancillary data */
    int           msg_flags;            /* flags returned by recvmsg() */
}

  1.msg_name和msg_namelen用于套接字未连接的时候(主要是未连接的UDP套接字),用来指定接收来源或者发送目的的地址。两个成员分别是套接字地址及其大小,类似recvfrom和sendto的第二和第三个参数。对于已连接套接字,则可直接将两个参数设置为NULL和0。而对于recvmsg,msg_name是一个值-结果参数,会返回发送端的套接字地址。
  2.msg_iov和msg_iovlen两个成员用于指定数据缓冲区数组,即iovec结构数组。iovec结构如下:

#include <sys/uio.h>
struct iovec 
{
    void    *iov_base;      /* starting address of buffer */
    size_t  iov_len;        /* size of buffer */
}

  其中iov_base就是一个缓冲区元素,事实上也是一个数组,而iov_len则是指定该数据的大小。也就是说,缓冲区是一个二维数组,并且每一维长度不是固定的。猜测这样子设置应该是方便传递多个结构类型不同,并且长度也是不固定的数据吧,这样子客户端就可以直接对每个位置的数据进行转换获取就行了。如果只是当存传送一个字符串,那只需要将msg_iovlen设置成1,然后将数据赋给iov[0].iov_base就行了。无论是sendmsg和recvmsg,都需要提前设置好这两项并且分配好内存。
  3.msg_control和msg_controllen是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息(control infomation)。这两个成员可以用来返回关于数据报文的其他指定信息,不过需要通过setsockopt函数指定要返回的辅助信息。对于sendmsg,这两项需要都设置成0,否则会导致发送数据失败。还未研究过sendmsg的辅助数据能够做什么。
  4.只有recvmsg使用msg_flags,recvmsg被调用时,flags参数被复制到msg_lags成员,并且由内核使用其值驱动接受处理过程,内核还是依据recvmsg的结果跟新msg_flags成员的值

  5.sendmsg忽略msg_flags成员,因为他直接使用flags参数驱动发送处理过程,这一点意味着在某个sendmsg调用中设置MSG_DONTWAIT标志,那就把flags参数设置为该值,吧msg_flags成员设置该值认为不起作用。

辅助数据

  辅助数据就好比控制信息

  其中cmsg_level和cmsg_type应该和调用setsockopt函数时传递的level和optname参数是一样的。那么我们怎么获取辅助数据呢,在msg_control辅助数据是通过一个或多个辅助数据对象保存的,辅助数据对象cmsghdr结构如下:

#include <sys/socket.h>
struct cmsghdr 
{
    socklen_t   cmsg_len;   /* length in bytes, including this structure */
    int         cmsg_level; /* originating protocol */
    int         cmsg_type;  /* protocol-specific type */
    /* followed by unsigned char cmsg_data[] */
}

  辅助数据由一个或多个辅助数据对象(ancillary data object)构成,每个对象以一个定义在头文件<sys/socket.h>中的cmsghdr结构开头

union {
          struct cmsghdr     cm;
          char               control[CMSG_SPACE(sizeof(int))];
}control_un;

而辅助数据对象在实际的存储中是如下分布的:

 

  展示了在一个控制缓冲区中出现2个辅助数据对象的一个例子

  

  msg_control指向第一个辅助数据对象,辅助数据的总长度则由msg_controllen指定。每个对象开头都是一个描述该对象的cmsghdr结构。在cmsg_type成员和实际数据之间可以有填充字节,从数据结尾处到下一个辅助数据对象之前也可以有填充字节。

  注意,不是所有实现都支持在单个控制缓冲区中存放多个辅助数据对象。

  如下图所示,展示了通过一个UNIX域套接口传递描述字或传递凭证时所用的cmsghdr结构的格式。

  既然由recvmsg返回的辅助数据可含有任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充字节,头文件<sys/socket.h>中定义了以下5个宏,以简化对辅助数据的处理

#include <sys/socket.h>
#include <sys/param.h>
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr);
    //返回:指向第一个cmsghdr结构的指针,若无辅助数据则为NULL
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsghdr);
    //返回:指向下一个cmsghdr结构的指针,若不再有辅助数据对象则为NULL
unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr);
    //返回:指向与cmsghdr结构关联的数据的第一个字节的指针
unsigned char *CMSG_LEN(unsigned int length);
    //返回:给定数据量下存放到cmsg_len中的值
unsigned char *CMSG_SPACE(unsigned int length);
    //返回:给定数据量下一个辅助数据对象总的大小。

  通过上面五个宏我们可以很方便的为msg_control分配内存和遍历辅助对象、获取辅助数据。不过对于分配内存一般需要预先知道要获取的辅助数据结构的大小。

  CMSG_LEN和CMSG_SPACE的区别在于,前者不计辅助数据对象中数据部分之后可能的填充字节,因而返回的是用于存放在cmsg_len成员中的值,后者计上结尾处可能的填充字节,因而返回的是用于为辅助对象动态分配空间的大小值。

char contorl[CMSG_SPACE(size_of_struct1) + CMSG_SPACE(size_of_struct2)];
struct msghdr msg;
/* fill in msg structure */
/* call recvmsg() */
struct cmsghdr *cmsgptr;
for ( cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL; 
    cmsgptr = CMSG_NXTHDR(&msg, cmsgptr) ) {
    /* 判断是否是自己需要的msg_level和msg_type */
    u_char *ptr;
    ptr = CMSG_DATA(cmsgptr); /* 获取辅助数据 */
}

注意:

  1. 对于已连接的套接字,msghdr的msg_name直接设置为NULL,对于recvmsg,该成员会返回对端的套接字地址。
  2. 对于sendmsg,msghdr的msg_control和msg_controllen需要设置为0,不设置为似乎无法发送成功。
  3. 处理辅助数据可以直接用5个宏,并且需要根据msg_level和msg_type判断辅助数据的类型再进行相应的转换。unp中讲到的很多cmsg_type可能自己的系统中并没有移植,这点需要注意。比如我使用ubuntu,就没有移植IP_RECVDSTADDR和IP_RECVIF,改用IP_PKTINFO才完成了例子,也是在这里纠结和浪费了很多时间。实际上unp第7章的函数就可以用来判断这些设置项是否存在,也可以在调用setsockopt和判断msg_level、msg_type之前用#if defined语句来判断本系统是否兼容该项,如果不兼容的话会直接跳过接下来的处理(见例子)。

排队的数据量

  如果我们想要在不真正读取数据的前提下知道一个套接字上已用多少数据排队等着读取。可用三个技术实现:

  1. 可以使用非阻塞I/O。
  2. 如果既想查看数据,又想数据仍然保留在接受队列中以供本进程其他部分稍后读取,那么可以使用MSG_PEEK标志。(需要注意的是:如果使用这个标志来读取套接字上可读数据的大小,在两次调用之间缓冲区可能会增加数据,如果第一次指定使用MSG_PEEK标志,而第二次调用没有指定使用MSG_PEEK标志,那么这两次调用的返回值是一样的,即使在这两次调用之间缓冲区已经增加了数据。)
  3. 一些实现支持ioctl的FIONREAD命令。该命令的第三个ioctl参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接字接受队列的当前字节数。

套接字和标准I/O

  标准I/O: fdopen:从任意描述符创建一个标准I/O流。fileno:获取一个给定标准I/O流对应的描述符
  对于标准I/o,+r意味着读写,因为TCP和UDP是全双工的,但是我们一般不这么做,我们为给定的套接字打开两个标准的I/O流,一个读,一个写。
  用fdopen打开标准输入和输出,修改服务器回射函数str_echo

void str_echo(int sockfd)
{
    char line[MAXLEN];
    FILE *fpin=Fdopen(sockfd,"r");//
    FILE *fpout=Fdopen(sockfd,"w");//
    char *x;
    while((x=fgets(line,MAXLEN,fpin))!=NULL)
        fputs(line,fpout);
}

  fdopen创建两个标准I/O流,一个用于输入,一个用于输出,当运行客户,直到输入EOF,才回射所有文本。

实际发生的步骤如下:

  1. 键入第一行文本,客户端发送到服务器端;
  2. 服务器fgets到这段文本,并用fputs回射;
  3. 文本被回射到标准IO函数全缓冲,但不把缓冲区内容写到描述符,因为缓冲区未满;
  4. 直到输入EOF字符,str_cli调用shutdown,客户端发送一个FIN,服务器收取FIN被fgets读入,返回空指针;
  5. str_echo函数结束,返回main函数;
  6. exit调用标准的I/O清理函数,缓冲区中的内容被输出;
  7. 同时子进程终止,已连接套接字关闭,TCP四分组终止。

这里就有三个概念了:

  1. 完全缓冲:缓冲区满、fflush、exit,才发生I/O;
  2. 行缓冲:换行符、fflush、exit,才发生I/O;(标准输入和标准输出是完全缓冲,除非他们指代终端设备此时为行缓冲;所有其他I/O流都是完全缓冲,除非它们指代终端设备此时为行缓冲)
  3. 不缓冲:每次标准I/O输出函数都发生I/O。(标准错误输入不缓冲)

posted on 2018-11-07 20:02  tianzeng  阅读(828)  评论(0编辑  收藏  举报

导航