TCP/IP网络编程 第十三章:多种IO函数

send&recv函数

Linux中的send&recv

虽然第1章介绍过send&recv函数,但那时是基于Windows平台介绍的。本节将介绍Linux平
台下的send&recv函数。其实二者并无差别。

#include <sys/socket.h>
ssize_t send(int sockfd, const void * buf, size_t nbytes, int flags);
//成功时返回发送的字节数,失败时返回-1。

       sockfd  //表示与数据传输对象的连接的套接字文件描述符
       buf     //保存待传输数据的缓冲地址值
       nbytes  //待传输的字节数
       flags   //传输数据时指定的可选项信息

与第1章的Windows中的send函数相比,上述函数在声明的结构体名称上有些区别。但参数的顺序、含义、使用方法完全相同,因此实际区别不大。接下来介绍recv函数,该函数与Windows
的recv函数相比也没有太大差别。

#include <sys/socket.h>
ssize_t recv(int sockfd, void * buf, size_t nbytes, int flags);
//成功时返回接收的字节数(收到EOF时返回0),失败时返回-1。
   sockfd   //表示数据接收对象的连接的套接字文件描述符
   buf      //保存接收数据的缓冲地址值    
   nbytes    //可接收的最大字节数
   flags     //接收数据时指定的可选项信息

send函数和recv函数的最后一个参数是收发数据时的可选项。该可选项可利用位或同时传递多个信息。通过表13-1整理可选项的种类及含义

MSG_DONTROUTE 指定不使用路由查找目标地址(用于本地通信)
MSG_OOB 发送或接收带外数据
MSG_PEEK 查看接收队列中的数据,但不将其从队列中删除
MSG_WAITALL 在接收操作中,如果请求的字节数不足,阻塞线程直到数据完全接收
MSG_TRUNC 如果接收缓冲区不足以容纳传入数据,截断数据而不报告错误
MSG_CTRUNC 如果控制信息不足以容纳传入数据,截断控制数据而不报告错误

另外,不同操作系统对上述选项的支持也不同。

MSG_OOB:发送紧急消息

对紧急消息的理解,可能有些人会把它类比为医院里面伤势垂危的病人需要进行紧急急救,所以他需要加快处理。这个理解如果理解不到位可能会出现以下问题。如果是紧急消息,它是不是可以和前面不紧急消息交换位置,紧急消息先提前处理?如果是紧急消息,它是不是可以在传输速度上得到加速?如果是紧急消息,它内部消息本身的含义是否是紧急的?

实际上上述猜想都是错的。这里的紧急消息,它在TCP层面(TCP首部)来说,只是一个标志位而已。就是说这个紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息。并且由于TCP顺序传输,以及物理链路层的特性。它是不可能在顺序和速度上得到特殊处理的。那么它最后的结果实际上就是以一种紧急的方式来处理罢了。

检查输入缓冲

通过设置MSG_PEEK选项和MSG_DONTWAIT选项,以验证输入缓冲中是否存在接收的数据。设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读数据存在与否的函数。下面通过示例给出了使用MSG_PEEK和MSG_DONTWAIT选项的结果。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define BUFSIZE 30
void error_handling(char *message);

int main(int argc, char *argv[]){
    int acpt_sock, recv_sock;
    struct sockaddr_in acpt_adr, recv_adr;
    int str_len, state;
    socklen_t recv_adr_sz;
    char buf[BUF_SIZE];
    if(argc!=2){
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    acpt_sock=socket(PF_INET, SOCK_STREAM,0);
    memset(&acpt_adr, 0, sizeof(acpt_adr));
    acpt_adr.sin_family=AF_INET;
    acpt_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    acpt_adr.sin_port=htons(atoi(argv[1]));

    if(bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr))==-1)
          error_handling("bind() error");
    listen(acpt_sock,5);

    recv_adr_sz=sizeof(recv_adr);
    recv_sock=accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);

    while(1){
         str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT);
         if(str_len>0)
         break;
    }

    buf[str_len]=0;
    printf("Buffering %d bytes: %s \n",str_len, buf);
    str_len=recv(recv_sock,buf, sizeof(buf)-1,0);
    buf[str_len}=0;
    printf("Read again %s \n", buf);
    close(acpt_sock);
    close(recv_sock);
    return 0;
}

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(1);
}

通过运行结果可以验证,仅发送一次的数据被读取了两次,因为第一次调用recv函数时设置了MSG_PEEK可选项。

readv&writev函数

本节介绍的readv&writev函数有助于提高数据通信效率。先介绍这些函数的使用方法,再讨
论其合理的应用场景。

使用readv&writev函数

readv&writev函数的功能可概括如下:“对数据进行整合传输及发送的函数。”
也就是说,通过writev函数可以将分散保存在多个缓冲中的数据一并发送,通过readv函数可以多个缓冲分别接收。因此,适当使用这2个函数可以减少IO函数的调用次数。下面先介绍writev函数。

#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec * iov, int iovcnt);
//成功时返回发送的字节数,失败时返回-1。
    filedes  //表元数据传输对象的套接字文件描述符。但该函数并不只限于套接字
    iov      //iovec结构体数组的地址值,结构体iovec中含待发送数据的位置和大小信息
    iovcnt   //向第二个参数传递的数组长度

上述函数的第二个参数中出现的数组iovec结构体的声明如下。

struct iovec{
     void *iov_base;//缓冲地址
     size_t iov_len;//缓冲大小
}

可以看到,结构体iovec由保存待发送数据的缓冲(char型数组)地址值和实际发送的数据长度信息构成。

相信各位已经掌握writev函数的使用方法和特性,接下来给出示例。

#include<stdio.h>
#include<sys/uio.h>
int main(int argc, char *argv[]){
    struct iovec vec[2];
    char buf1[]="ABCDEFG";
    char buf2[]="1234567";
    int str_len;
    vec[0].iov_base=buf1;
    vec[0].iov_len=3;
    vec[1].iov_base=buf2;
    vec[1].iov_len=4;

    str_len=writev(1, vec,2);
    puts("");
    printf("Write bytes: %d \n",str_len);
    return 0;
}

下面介绍readv函数,它与writev函数正好相反。

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec * iov, int iovcnt);
//成功时返回接收的字节数,失败时返回-1。
 filedes  //传递接收数据的文件(或套接字)描述符。
 iov      //包含数据保存位置和大小信息的iovec结构体数组的地址值。
 iovcnt   //第二个参数中数组的长度

我们已经学习了writev函数,因此直接通过示例给出readv函数的使用方法。

#include <stdio.h>
#include <sys/uio.h> 
#define BUF_SIZE 100
int main(int argc, char *argv[]){
    struct iovec vec[2];
    char buf1[BUF_SIZE]={0,};
    char buf2[BUF_SIZE]={0,};
    int str_len;

    vec[0].iov_base=buf1;
    vec[0].iov_len=5;
    vec[1].iov_base=buf2;
    vec[1].iov_len=BUF_SIZE;

    str_len=readv(0, vec,2);
    printf("Read bytes: %d \n", str_len);
    printf("First message: %s \n", buf1);
    printf("Second message: %s \n",buf2);
    return 0;
}

合理使用readv&writev函数

哪种情况适合使用readv和writev函数?实际上,能使用该函数的所有情况都适用。例如,需要传输的数据分别位于不同缓冲(数组)时,需要多次调用write函数。此时可以通过1次write函数调用替代操作,当然会提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以必多次调用read函数,而是利用1次readv函数就能大大提高效率。即使仅从C语言角度看,减少函数调用次数也能相应提高性能。但其更大的意义在于减少据包个数。

基于Windows的实现

我们在之前的Windows示例中通过send函数和recv函数完成了数据交换,因此只需补充设置可
选项相关示例。此处需要考虑一点:
“Windows中并不存在Linux那样的信号处理机制。”

因此在Windows中无法完成针对该可选项的事件处理,需要考虑使用其他方法。我们通过select函数解决这一问题。之前讲过的select函数的3种监视对象如下所示。
□是否存在套接字接收数据?
□无需阻塞传输数据的套接字有哪些?
□哪些套接字发生了异常?
其中,第12章也未对“发生异常的套接字”另行讲解。“异常”是不同寻常的程序执行流。因此,收到Out-of-band数据也属于异常。也就是说,利用select函数的这一特性可以在Windows平台接收Out-of-band数据,参考如下示例。

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[]){
    WSADATA wsaData;
    SOCKET hAcptSock,hRecvSock;
    SOCKADDR_IN recvAdr;
    SOCKADDR_IN sendAdr;
    int sendAdrsize, strLen;
    char buf[BUF_SIZE];
    int result;
    fd_set read, except, readcopy, exceptcopy;
    struct timeval timeout;

    if(argc!=2) {
         printf("Usage : %s <port>\n", argv[0]);
         exit(1);
    }

    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
        ErrorHandling("WSAStartup() error!");

    hAcptSock=socket(PF_INET,SOCK_STREAM,0);
    memset(&recvAdr, 0, sizeof(recvAdr));
    recvAdr.sin_family=AF_INET;
    recvAdr.sin_addr.s_addr=htonl(INADDR_ANY);
    recvAdr.sin_port=htons(atoi(argv[1]));

    if(bind(hAcptsock,(SOCKADDR*)&recvAdr,sizeof(recvAdr))==SOCKET_ERROR)
        ErrorHandling("bind() error");
    if(listen(hAcptSock,5)==SOCKET_ERROR)
        ErrorHandling("listen() error");

    sendAdrsize=sizeof(sendAdr);
    hRecvSock=accept(hAcptSock,(SOCKADDR*)&sendAdr, &sendAdrSize);
    FD_ZERO(&read);
    FD_ZERO(&except);
    FD_SET(hRecvSock,&read);
    FD_SET(hRecvSock,&except);

    while(1){
        readcopy=read;
        exceptcopy=except;
        timeout.tv_sec=5;
        timeout.tv_usec=0;

        result=select(0, &readcopy,0,&exceptCopy,&timeout);
        if(result>0){
             if(FD_ISSET(hRecvSock,&exceptCopy)){//发生异常
                  strLen=recv(hRecvSock, buf,BUF_SIZE-1, MSG_OOB);
                  buf[strLen]=0;
                  printf("Urgent message: %s \n",buf);
             }
             if(FD_ISSET(hRecvSock, &readcopy)){//正常读取事件
                  strLen=recv(hRecvSock,buf,BUF_SIZE-1, 0);
                  if(strLen==0){
                  closesocket(hRecvSock);
                  break;
                  }
             else{
                  buf[strLen]=0;
                  puts(buf);
             }
             }
        }
    }
closesocket(hAcptSock);
WSACleanup();
return 0;
}

void ErrorHandling(char *message){
     fputs(message, stderr);
     fputc('\n',stderr);
     exit(1);
}

对于Linux中的writev和readv函数的功能可以通过Windows中的"重叠IO"实现。

猜你喜欢

转载自blog.csdn.net/Reol99999/article/details/131762356