网络编程基础篇学习4--收发数据

发送数据

三个常用函数,write、send和sendmsg

ssize_t write (int socketfd, const void* buffer, size_t size)
ssize_t send (int socketfd, const void* buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr* msg, int flags)
复制代码

以上三个函数的使用场景不同:

  1. write:常见的文件写函数,把socketfd换成文件描述符,就是普通的文件写入。
  2. send:可以指定选项,发送带外数据(带外数据:基于TCP协议的紧急数据,用于客户端-服务端在特定场景下的紧急处理)。
  3. sendmsg:指定多重缓冲区用来传输数据,以结构体msghdr的方式发送数据。
句柄

句柄(handle)是C++程序设计中经常提及的一个术语。它并不是一种具体的、固定不变的数据类型或实体,而是代表了程序设计中的一个广义的概念。句柄一般是指获取另一个对象的方法——一个广义的指针,它的具体形式可能是一个整数、一个对象或就是一个真实的指针,而它的目的就是建立起与被访问对象之间的唯一的联系

使用socket描述符的write和使用普通文件描述符的write的区别

对于普通文件描述符而言,一个文件描述符代表了打开的一个文件句柄,通过调用 write 函数,操作系统内核帮我们不断地往文件系统中写入字节流。注意,写入的字节流大小通常和输入参数 size 的值是相同的,否则表示出错
对于socket描述符而言,它代表了一个双向连接,在socket描述符上调用 write 写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的。
这是因为操作系统内核为读取和发送数据做了很多我们表面上看不到的工作。

发送缓冲区

当TCP连接建立成功后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。 发送缓冲区的大小可以通过套接字选项来改变,当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去
有两种情况:

  1. 操作系统内核的发送缓冲区足够大,可以直接容纳这份数据,我们的程序从 write 调用中退出,返回写入的字节数就是应用程序的数据大小。
  2. 操作系统内核的发送缓冲区是够大了,不过还有数据没有发送完,或者数据发送完了,但是操作系统内核的发送缓冲区不足以容纳应用程序数据

在第二种情况下,操作系统内核并不会返回,也不会报错。而是应用程序被阻塞,也就是应用程序在write调用处停留,不直接返回。也就是操作系统内核中所说的,将应用程序”挂起“。

MSS和MTU

MSS:TCP协议定义的一个选项,MSS选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。即MSS是TCP数据包每次能够传输的最大数据分段
MTU:最大传输单元(Maximum Transmission Unit,MTU)用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。MTU是数据链路层的概念。MTU限制的是数据链路层的payload,也就是上层协议的大小,例如IP,ICMP等。即数据链路层对接收来自上层数据的大小进行了限制,这个限制就是MTU。 如果在网络层下发到链路层的数据超过了MTU,就需要在网络层进行分片,切成 <=MTU 的IP数据包。网络层如果发现链路层的MTU小于IP包的大小(网络层可以调用函数获取链路层信息),也并不会立刻开始分片,还需要看IP包的是否允许分片位DF(Don't Fragment),如果允许分片,就会分成多个ID一样的IP包

什么时候可以从系统调用中返回?

每个操作系统的内核处理是不同的。大部分UNIX系统是一直等到可以把应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回

发送缓冲区.png 当 TCP 连接建立之后,它就开始运作起来。你可以把发送缓冲区想象成一条包裹流水线,有个聪明且忙碌的工人不断地从流水线上取出包裹(数据),这个工人会按照 TCP/IP 的语义,将取出的包裹(数据)封装成 TCP 的 MSS 包,以及 IP 的 MTU 包,最后走数据链路层将数据发送出去。这样我们的发送缓冲区就又空了一部分,于是又可以继续从应用程序搬一部分数据到发送缓冲区里,这样一直进行下去,到某一个时刻,应用程序的数据可以完全放置到发送缓冲区里。在这个时候,write 阻塞调用返回。注意返回的时刻,应用程序数据并没有全部被发送出去发送缓冲区里还有部分数据,这部分数据会在稍后由操作系统内核通过网络发送出去

读取数据

由于UNIX中万物皆文件,而socket描述符本身和文件描述符并无区别,所以可以将socket描述符传递给那些原本为处理文件而设计的函数。

read函数

read函数原型如下:

ssize_t read (int socketfd, void* buffer, size_t size)
复制代码

read 函数要求操作系统内核从socket描述字 socketfd 读取最多多少个字节(size),并将结果存储到 buffer返回值告诉我们实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),这在网络中表示对端发送了 FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。当然,如果是非阻塞 I/O,情况会略有不同(后面说明)

read中的size表示的是最多读取size个字节。如果我们想让程序每次都能读到size个字节的话,就需要不断地循环读取(read):

/* 从socketfd描述字中读取"size"个字节. */
size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = buffer;
    int length = size;

    while (length > 0) {
        int result = read(fd, buffer_pointer, length);

        if (result < 0) {
            if (errno == EINTR)
                continue;     /* 考虑非阻塞的情况,这里需要再次调用read */
            else
                return (-1);
        } else if (result == 0)
            break;                /* EOF(End of File)表示套接字关闭 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length);        /* 返回的是实际读取的字节数*/
}
复制代码

对于while循环,表示在没有读满read字节之前,需要一直read
如果是非阻塞I/O的情况下,没有数据可以读时,需要继续调用read
如果对方发出了FIN包,返回值为0,表示EOF,此时需要关闭套接字,所以需要break

length -= result;
buffer_pointer += result;
复制代码

表示将需要读取的字符数减少,并且把缓存指针向下移动
最后需要返回实际读取的字符数(正常情况下为size,如果碰到了EOF,可能会比size小)

总结

发送成功仅仅表示的是数据被拷贝到了发送缓冲区中,并不意味着连接对端已经收到所有的数据。至于什么时候发送到对端的接收缓冲区,或者更进一步说,什么时候被对方应用程序缓冲所接收,对我们而言完全都是透明的。

  1. 对于send来说,返回成功仅仅表示数据写到发送缓冲区成功,并不表示对端已经成功收到。
  2. 对于read来说,需要循环读取数据,并且需要考虑 EOF 等异常条件。

为什么缓冲区不能无限大? 由于内核协议栈不确定用户一次要发送多少数据,如果用户请求一次就发一次,那么当每次请求的数据很少时,会造成网络I/O很频繁,并且真正发送的数据也不多。 n 同时网络传输的大小MTU也会限制每次发送的大小,数据过长会就会导致分片发送。
并且数据传输也有时延要求,不会一直呆在缓冲区中,所以总会有空出来的缓冲区去存放新数据。
综上,缓冲区是不能无限大的。

数据流从发送端到接收端需要经过几次拷贝? 首先从用户缓冲区拷贝到内核缓冲区,然后进行报文封装(此时也要进行拷贝)并发送到网卡。同时在接收端需要进行相反的处理。

猜你喜欢

转载自juejin.im/post/7036716104105852942
4--