Unix/Linux编程:数据读写之recv、send、sendto、recvfrom、recvmsg、sendmsg

read和write也可以用于读写socket。但是socket编程接口也提供了几个专门用于socket数据读写的系统调用。他们增加了对数据读写的控制

TCP数据读写

理论

#include <sys/types.h>  
#include <sys/socket.h>  
/*
* 作用: 接收经指定的socket 传来的数据
* 参数: socket -- 已建立好连接的socket
*        buff    指明存放数据的缓冲区
*        len     指明缓冲区的长度
*        flags    一般设置为0
* 返回值: 
*     (1)返回实际读取到的数据长度,他可能小于期望的len。因此我们可能需要多次调用recv才能读到完整的数据
*     (2)返回0,表示对端已经关闭连接了
*      (3) 返回-1,表示出错,将会设置error
*/
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
/*
* 作用: 将数据由指定的socket 传给对方主机
* 参数:  socket -- 已建立好连接的socket
*        buff    指明存放数据的缓冲区
*        len     指明缓冲区的长度
*        flags    一般设置为0
* 返回值:
* 		(1)成功返回实际写入的长度
* 		(2)失败返回-1并设置errno
*/  
ssize_t send(int sockfd, const void *buff, size_t len, int flags);  

关于flag
在这里插入图片描述

每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的流量控制就是依赖于这两个独立的buffer以及buffer的填充状态。

  • 接收缓冲区把数据缓存入内核,应用进程一直没有调用recv()进入读取的话,此数据就会一直缓存在相应socket的的接收缓冲区内。也就是说,不管进程是否调用recv()读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,然后返回

    • 接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止
    • 如果数据一直没有被应用程序读取,当接收缓冲区满了之后,发生的动作是:接收方通知发送发,接收窗口关闭(win = 0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它
  • 进程调用send()发送数据的时候,最简单的情况(也是一般情况)就是将应用层buffer的数据拷贝到socket的内核发送缓冲区中,并不负责将数据发送给对端,发送数据是TCP的事。

同步send的工作原理

  • send函数只负责将数据提交给协议层
  • 当调用该函数时,send先比较待发送数据的长度len_data和套接字s的发送缓冲区的长度len_buff
    • len_data > len_buff:返回SOCKET_ERROR
    • len_data <= len_buff:先检查协议是否正在发送socket的发数缓冲区中的数据
      • 如果是就等待协议把数据发送完
      • 如果不是,再比较发送缓冲区的剩余空间len_buff_remain和len_data
        * len_data > len_buff_remain:等待TCP协议把发送缓冲区的数据发送完
        * len_data <= len_buff_remain:将待发送的数据copy到剩余空间中
        * 如果copy成功,返回实际copy的字节数
        * 如果copy的时候出现错误,返回SOCKET_ERROR
    • 如果send在等待协议时网络断开,返回SOCKET_ERROR
  • 也就是说send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端 。 如 果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR
  • 另外:在Unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个会收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

同步recv的工作原理

  • recv先检查是否TCP协议正在处理接收缓冲区的数据:
    • 如果协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕
    • 如果接收缓存区空闲,recv函数就把应用层的数据copy到接收缓冲区中(有可能待copy的数据比接收缓冲区的长度小,这时需要调用多次recv才能将数据copy到接收缓冲区中),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0

在进行TCP协议传输的时候,要注意数据流传输的特点,recv和send不一定是一一对应的(一般情况下是一一对应),也就是说并不是send一次,就一定recv一次就接收完,有可能send一次,recv多次才接收完,也可能send多次,一次recv就接收完了。TCP协议会保证数据的有序完整的传输,但是如何去正确完整的处理每一条信息,是程序员的事情。
例如:服务器在循环recv,recv的缓冲区大小为100byte,客户端在循环send,每次send 6byte数据,则recv每次收到的数据可能为6byte,12byte,18byte,这是随机的,编程的时候注意正确的处理。

实践

查看socket发送缓冲区大小

$ cat /proc/sys/net/ipv4/tcp_wmem 
4096    16384   4194304

第一个值是一个限制值,socket发送缓存区的最少字节数;
第二个值是默认值----16384(16K);
第三个值是一个限制值,socket发送缓存区的最大字节数;

proc文件系统下的值和sysctl中的值都是全局值,应用程序可根据需要在程序中使用setsockopt()对某个socket的发送缓冲区尺寸进行单独修改

UDP数据读写

// 
ssize_t sendto(int sockfd, const void *buf, size_t len, unsigned int flags, 
        const struct sockaddr *dest_addr, int addrlen);

/*
* 因为UDP没有连接的概念,所以我们每次读取数据时都需要获取发送端的socket地址,也就是src_dest的内容
* addrlen指定该地址的长度
*/
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *src_dest,socklen_t *addrlen);

recvfrom/sendto可以用用于TCP编程,只要把后面两个参数置NULL即可

通用数据读写

理论

recvmsg、sendmsg

即可用于读取TCP数据流,也可以用于读取UDP数据报。

#include <sys/socket.h>

struct msghdr {
    void          *msg_name;            /* socket地址 */
    socklen_t     msg_namelen;          /* socket地址长度 */
    struct iovec  *msg_iov;             /* 分散的内存块 */
    int           msg_iovlen;           /* 分散内存块的数量v */
    void          *msg_control;         /* 指向辅助数组的起始位置 */
    socklen_t     msg_conntrollen;      /* 辅助数据的大小 */
    int           msg_flags;            /* 复制函数中的flags参数,并在调用过程中更新 */
}

#include <sys/uio.h>
struct iovec {
    void    *iov_base;      /* 内存起始位置 */
    size_t  iov_len;        /* size of buffer */
}

/*
* 功能:接收远程主机经指定的socket 传来的数据. 
* 参数:
* 	sockfd -- 已建立好连线的socket, 如果利用UDP协议则不需经过连线操作. 
*   msg    -- 待接收的数据
*   flags  -- 一般默认为0
* 返回值:成功则返回接收到的字符数, 失败则返回-1, 错误原因存于errno 中.
*/
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
/*
* 功能:将数据由指定的socket传给对方主机
* 参数:
* 	sockfd -- 已建立好连线的socket, 如果利用UDP协议则不需经过连线操作. 
*   msg    -- 待发送的数据
*   flags  -- 一般默认为0
* 返回值:成功则返回接收到的字符数, 失败则返回-1, 错误原因存于errno 中.
*/
ssizt_t sendmsg(int sockfd, struct msghdr *msg, int flags);

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

关于msghdr :

  • msg_namemsg_namelen
    • 对于UDP通信,msg_name必须指向一个socket地址结构变量。它指定通信对方的socket地址。msg_namelen为这个地址的长度
    • 对于TCP通信,msg_name必须设置为NULL,msg_namelen为0。因为对数据流socket而言,对方的地址已经知道。
  • msg_iovmsg_iovlen两个成员用于指定数据缓冲区数组,即iovec结构数组。而iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的iovec有多少个
    • 对于recvmsg:数据读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这称为分散读
    • 对于sendmsgmsg_iovlen块分散的内存中的数据将被一起发送,这称为集中写
  • msg_controlmsg_controllen是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息(control infomation)。这两个成员可以用来返回关于数据报文的其他指定信息,不过需要通过setsockopt函数指定要返回的辅助信息。对于sendmsg,这两项需要都设置成0,否则会导致发送数据失败
  • msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg会在调用结束之前,将某些更新后的标志设置到msg_flag中。

readv、writev

  • readv/writevrecvmsg/sendmsg的简化版,主要针对与文件IO(对read/write的优化)。具体可以参见这里

实践

sendmsg传递数据

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>



int main(int argc, char* argv[]){
    int ret;     /* 返回值 */
    int sock[2];    /* 套接字对 */
    struct msghdr msg;
    struct iovec iov[1];
    char send_buf[100] = "it is a test";
    struct msghdr msgr;
    struct iovec iovr[1];
    char recv_buf[100];

    /* 创建套接字对 */
    ret = socketpair(AF_LOCAL,SOCK_STREAM,0,sock);
    if(ret == -1){
        printf("socketpair err\n");
        return 1;
    }

    /* sock[1]发送数据到本地主机  */
    bzero(&msg, sizeof(msg));
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    iov[0].iov_base = send_buf;
    iov[0].iov_len = sizeof(send_buf);
    msg.msg_iov = iov;//要发送或接受数据设为iov
    msg.msg_iovlen = 1;//1个元素

    printf("开始发送数据:\n");
    printf("发送的数据为: %s\n", send_buf);
    ret = sendmsg(sock[1], &msg, 0 );
    if(ret == -1 ){
        printf("sendmsg err\n");
        return -1;
    }
    printf("发送成功!\n");

    /* 通过sock[0]接收发送过来的数据 */
    bzero(&msg, sizeof(msg));
    msgr.msg_name = NULL;
    msgr.msg_namelen = 0;
    iovr[0].iov_base = &recv_buf;
    iovr[0].iov_len = sizeof(recv_buf);
    msgr.msg_iov = iovr;
    msgr.msg_iovlen = 1;
    ret = recvmsg(sock[0], &msgr, 0);
    if(ret == -1 ){
        printf("recvmsg err\n");
        return -1;
    }
    printf("接收成功!\n");
    printf("收到数据为: %s\n", recv_buf);

    /* 关闭sockets */
    close(sock[0]);
    close(sock[1]);

    return EXIT_SUCCESS;
}

socketpair的用法和理解
TCP之深入浅出send和recv
socket中send和recv函数
高性能服务器编程
recvmsg和sendmsg函数

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值