输入和输出(read,recv,recvmsg...和write,writev,writemsg)

  每一个TCP套接口有一个发送缓冲区,可以用SO_SNDBUF套接口选项来改变这个缓冲区的大小。

应用进程调用 write时,内核从应用进程的缓冲区中拷贝所有数据到套接口的发送缓冲区。如果套接口的发送缓冲区容不下应用程序的所有数据(或是应用进程的缓冲区大于 套接口发送缓冲区,或是套接口发送缓冲区还有其他数据),应用进程将被挂起(睡眠)。这里假设套接口是阻塞的,这是通常的缺省设置。内核将不从write 系统调用返回,直到应用进程缓冲区中的所有数据都拷贝到套接口发送缓冲区。因此从写一个TCP套接口的write调用成功返回仅仅表示我们可以重新使用应用进程的缓冲区。它并不告诉我们对端的 TCP或应用进程已经接收了数据。

    TCP取套接口发送缓冲区的数据并把它发送给对端TCP,其过程基于TCP数据传输的所有规则。对端TCP必须确认收到的数据,只有收到对端的ACK,本端TCP才能删除套接口发送缓冲区中已经确认的数据。TCP必须保留数据拷贝直到对端确认为止。

输入操作: read、readv、recv、recvfrom、recvmsg

  如果某个进程对一个阻塞的TCP套接口调用这些输入函数之一,而且该套接口的接收缓冲区中没有数据可读,该进程将被投入睡眠,直到到达一些数据。既然 TCP是字节流协议,该进程的唤醒就是只要到达一些数据:这些数据既可能是单个字节,也可以是一个完整的TCP分节中的数据。如果想等到某个固定数目的数 据可读为止,可以调用readn函数,或者指定MSG_WAITALL标志。

  既然UDP是数据报协议,如果一个阻塞的UDP套接口的接收缓冲区为空,对它调用输入函数的进程将被投入睡眠,直到到达一个UDP数据报。

  对于非阻塞的套接口,如果输入操作不能被满足(对于TCP套接口即至少有一个字节的数据可读,对于UDP套接口即有一个完整的数据报可读),相应调用将立即返回一个EWOULDBLOCK错误。

read

  read函数只是一个通用的读文件设备的接口是否阻塞需要由设备的属性和设定所决定。一般来说

  1. 读字符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式
  2. 读磁盘上的文件,一般不会是阻塞方式的。但使用锁和 fcntl设置取消文件O_NOBLOCK状态,也会产生阻塞的read效果。

  当读成功 时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误.如果错误为EINTR说明读是由中断引起 的, 如果是ECONNREST表示网络连接出了问题. 

int my_read(int fd,void *buffer,int length)
{
  int bytes_left;
  int bytes_read;
  char *ptr;
  
  bytes_left=length;
  while(bytes_left>0)
  {
      bytes_read=read(fd,ptr,bytes_read);
      if(bytes_read<0)
      {
        if(errno==EINTR)
           bytes_read=0;
         else
            return(-1);
      }
      else if(bytes_read==0)
         break;
      bytes_left-=bytes_read;
      ptr+=bytes_read;
  }
  return(length-bytes_left);
}

recv

  同步Socket的recv函数的执行流程:当应用程序调用recv函数时

  1. recv先等待socket的发送缓冲中的数据被协议传送完毕,如果协议在传送socket的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
  2. 如果socket的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字socket的接收缓冲区,如果socket接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕;
  3. 当协议把数据接收完毕,recv函数就把socekt的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把socket的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数;
  4. 如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

readv

#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);
两个函数的返回值:若成功则返回已读、写的字节数,若出错则返回-1

  上面两个函数中第二个参数的说明,个人感觉是有问题的,const struct iovec (*iov)[],这样写的话iov才是一个指向iovec结构数据的指针。而上面函数中第二个参数明明是一个指向iovec结构的指针。

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

#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; }

 recvmsg

  recvmsg和sendmsg是最通用的I/O函数,只要设置好参数,read、readv、recv、recvfrom和write、writev、send、sendto等函数都可以对应换成这两个函数来调用。

#include <sys/socket.h>
ssize_t recvmsg(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.关于两个函数msghdr的msghdr的msg_flags成员,目前没有研究。

  unp中给出了下面各种辅助数据的用途:



  其中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[] */
}

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

 

  cmsghdr中实际上只有三个元素,而cmsg_data成员实际上并不存在,只是用来表明接下来都是数据,并且实际上数据和结构中还存在着填充数据。填充数据可能是为了对齐(unp中讲到msg_control指向的辅助数据必须为cmsghdr结构适当的对齐),在两个cmsghdr之间也存在着填充数据。
  看到这里的时候我是很郁闷的,那我要怎么获取到辅助数据呢?一开始以为要自己手动给cms_data分配内存,但是我连cmsg_data成员都获取不到啊!然后仔细看了unp中的内容才发现可以通过下面5个CMSG_XXX宏来获取和设置辅助数据。

#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的区别在于后者会包含两个辅助数据之间的填充字节。

char contorl[CMSG_SPACE(size_of_struct1) + CMSG_SPACE(size_of_struct2)];
struct msghdr msg;
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语句来判断本系统是否兼容该项,如果不兼容的话会直接跳过接下来的处理(见例子)。

msg_level和msg_type需要注意支持的协议。

/* unpudpsendmsg.c */
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>

#define SERV_PORT 51002
#define MAXLINE   256
#define SA struct sockaddr

void
err_quit(const char *sstr)
{
    printf("%s\n", sstr);
    exit(0);
}

int 
main(int argc, char **argv)
{
    int sockfd, n;
    struct sockaddr_in servaddr, dstaddr;
    char buff[MAXLINE], buff2[MAXLINE];
    struct msghdr msgsent, msgrecvd;
    struct cmsghdr cmsg, *cmsgtmp;
    struct iovec iov, iov2;
    const int on = 1;
    char control[CMSG_SPACE(64)];  // 使用CMSG_DATA分配cmsg_control内存,实际应该根据已知的结构分配。

    if ( argc < 2 ) {
        err_quit("usage: unpudpsendmsg <IPaddress>");
    }

    sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    // 处理sendmsg的msghdr结构
    msgsent.msg_name = NULL;
    msgsent.msg_namelen = 0;
    msgsent.msg_iovlen = 1;
    iov.iov_base = buff;         // 为iov[0]分配内存
    iov.iov_len = MAXLINE;
    msgsent.msg_iov = &iov;
    msgsent.msg_control = 0;     // 对sendmsg,msg_control要设置为0。
    msgsent.msg_controllen = 0;

    // 处理recvmsg的msghdr结构
    msgrecvd.msg_name = &dstaddr;
    msgrecvd.msg_control = control;
    msgrecvd.msg_controllen = sizeof(control);
    iov2.iov_base = (void *)buff2;
    iov2.iov_len = MAXLINE;
    msgrecvd.msg_iov = &iov2;
    msgrecvd.msg_iovlen = 1;
    msgrecvd.msg_flags = 0;

    connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
#if defined(IP_PKTINFO)
    setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
#elif defined(IP_RECVDSTADDR)
    setsockopt(sockfd, IPPROTO_IP, IP_RECVORIGDSTADDR, &on, sizeof(on));
#endif

    while ( 1 ) {
        fgets(buff, MAXLINE, stdin);
        n = sendmsg(sockfd, &msgsent, 0);
        if ( n <= 0 ) { 
            continue;
        }
        n = recvmsg(sockfd, &msgrecvd, 0);
        printf("recvmsg: %s", (char *)msgrecvd.msg_iov[0].iov_base);
            // 通过缓冲数据组获取服务器端返回的数据。
        printf("msg_controllen: %d\n", (int)msgrecvd.msg_controllen);
        for ( cmsgtmp = CMSG_FIRSTHDR(&msgrecvd); cmsgtmp != NULL; cmsgtmp = CMSG_NXTHDR(&msgrecvd, cmsgtmp) ) {
#if defined(IP_RECVDSTADDR)
            if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_RECVDSTADDR ) {
            // 判断msg_level和msg_type再进行相应的处理。
                struct sockaddr_in *addrtmp;
                char ip[14];
                addrtmp = (struct sockaddr_in *)CMSG_DATA(cmsgtmp);
                inet_ntop(AF_INET, addrtmp, ip, sizeof(ip));
                printf("recv ip: %s, port: %d\n", ip,  ntohs(addrtmp->sin_port));
            }
#elif defined(IP_PKTINFO)
            if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_PKTINFO ) {
                struct in_pktinfo *pktinfo;
                pktinfo = (struct in_pktinfo*)CMSG_DATA(cmsgtmp);
                printf("recv ip: %s, ifindex: %d\n", inet_ntoa(pktinfo->ipi_addr), pktinfo->ipi_ifindex);
            }
#endif
        }
    }
}

输出操作:write、writev、send、sendto、sendmsg

  对于一个TCP套接口,内核将从应用进程的缓冲区到该套接口的发送缓冲区拷贝数据。对于阻塞的套接口,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。对于一个非阻塞的TCP套接口,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错误。如果其发送缓冲区中有一些空间,返回值将是内核能够拷贝到该缓冲区中的字节数。这个字节数也称为不足计数(short count)

    UDP套接口不能在真正的发送缓冲区。内核只是拷贝应用进程数据并把它沿协议栈向下传送,渐次冠以UDP头部和IP头部。因此对一个阻塞的UDP套接口,输出函数调用将不会因为与TCP套接口一样的原因而阻塞,不过有可能会因其他的原因而阻塞

write

  write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有两可能.

  1. 返回值大于0,表示写了部分或者是全部的数据. 这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
  2. 返回的值小于0,此时出现了错误.我们要根据错误类型来处理.
  3. 如果错误为EINTR表示在写的时候出现了中断错误.
  4. 如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).
int my_write(int fd,void *buffer,int length)
{
int bytes_left;
int written_bytes;
char *ptr;

ptr=buffer;
bytes_left=length;
while(bytes_left>0)
{
        
         written_bytes=write(fd,ptr,bytes_left);
         if(written_bytes<=0)
         {       
                 if(errno==EINTR)
                         written_bytes=0;
                 else             
                         return(-1);
         }
         bytes_left-=written_bytes;
         ptr+=written_bytes;     
}
return(0);
}

send

  论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合

  1. MSG_DONTROUTE:不查找表,是send函数使用的标志.这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.一般用网络诊断和路由程序里面.
  2. MSG_OOB:接受或者发送带外数据 ,表示可以接收和发送带外的数据.关于带外数据我们以后会解释的.
  3. MSG_PEEK:查看数据,并不从系统缓冲区移走数据,是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.
  4. MSG_WAITALL:等待所有数据,是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv一直阻塞,直到指定的条件满足,或者是发生了错误. 1>当读到了指定的字节时,函数正常返回.返回值等于len 2>当读到了文件的结尾时,函数正常返回.返回值小于len 3>当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)
  5. MSG_NOSIGNAL: is a flag used by send,This flag requests that the implementation does not to send a SIGPIPE signal on errors on stream oriented sockets when the other end breaks the connection. The EPIPE error is still returned as normal.Though it is in some Berkely sockets APIs (notably Linux) it does not exist in what some refer to as the reference implementation, FreeBSD, which instead uses a socket option SO_NOSIGPIPE, 对于服务器端,我们可以使用这个标志。目的是不让其发送SIG_PIPE信号,导致程序退出。如果flags为0,则和read,write一样的操作.还有其它的几个选项,不过我们实际上用的很少,可以查看 Linux Programmer's Manual得到详细解释

注意

  同步Socket的send函数的执行流程,当调用该函数时,send先比较待发送数据的长度len和套接字socket的发送缓冲的长度(因为待发送数据是要copy到套接字s的发送缓冲区的,注意并不是send把socket的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到socket的发送缓冲区的剩余空间里):

  1. 如果len大于socket的发送缓冲区的长度,该函数返回SOCKET_ERROR
  2. 如果len小于或者等于socket的发送缓冲区的长度,那么send先检查协议是否正在发送socket的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送socket的发送缓冲中的数据或者socket的发送缓冲中没有数据,那么 send就比较socket的发送缓冲区的剩余空间和len:

           1>如果len大于剩余空间大小send就一直等待协议把socket的发送缓冲中的数据发送完;

         2>如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里。

   3.send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

  注意:send函数把buf中的数据成功copy到socket的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)

writev

  writev是个原子操作,一次调用操作只产生单个udp数据报

  一个四字节的write操作跟一个396个字节的write可能触发Nagle算法,首选办法之一是对这两个缓冲区调用writev

转载于:https://www.cnblogs.com/tianzeng/p/9925275.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值