使用套接字进行读写:
连接建立的根本目的是为了数据的收发。拿我们常用的网购场景举例子,我们在浏览商品或 者购买货品的时候,并不会察觉到网络连接的存在,但是我们可以真切感觉到数据在客户端 和服务器端有效的传送, 比如浏览商品时商品信息的不断刷新,购买货品时显示购买成功 的消息等。
发送数据:
发送数据时常用的有三个函数,分别是 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)
每个函数都是单独使用的,使用的场景略有不同:
第一个函数:是常见的文件写函数,如果把 socketfd 换成文件描述符,就是普通的文件写入。
第二个带 flag 的函数:想指定选项,发送带外数据;(带外数据,是一种基于 TCP 协议的紧急数据,用于客户端 - 服务器在特定场景下的紧急处理。)
第三个函数: 指定多重缓冲区传输数据,以结构体 msghdr 的方式发送数据。
问题:
既然套接字描述符是一种特殊的描述符,那么在套接字描述符上调用 write 函数,应该和在普通文件描述符上调用 write 函数的行为是一致的,都是通过描述符句柄写入指定的数据。(乍一看,两者的表现形式是一样,内在的区别还是很不一样的。)
对于普通文件描述符:
一个文件描述符代表了打开的一个文件句柄,通过调用 write 函数,操作系统内核帮我们不断地往文件系统中写入字节流。注意,写入的字节流大小通常和输入参数 size 的值是相同的,否则表示出错。
对于套接字描述符:
代表了一个双向连接,在套接字描述符上调用 write 写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的。
原因: 操作系统内核为读取和发送数据做了很多我们表面上看不到的工作。
接下来拿 write 函数举例,重点阐述发送缓冲区的概念。
发送缓冲区:
当 TCP 三次握手成功,TCP 连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。
发送缓冲区的大小可以通过套接字选项来改变;
当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
有几种情况:
第一种情况,操作系统内核的发送缓冲区足够大,可以直接容纳这份数据,那么皆大欢喜,我们的程序从 write 调用中退出,返回写入的字节数就是应用程序的数据大小。
第二种情况,操作系统内核的发送缓冲区是够大了,不过还有数据没有发送完,或者数据发送完了,但是操作系统内核的发送缓冲区不足以容纳应用程序数据(这种情况结果是什么?报错?还是直接返回?)
操作系统内核并不会返回,也不会报错,而是应用程序被阻塞,也就是说应用程序在 write 函数调用处停留,不直接返回。
那么什么时候才会返回呢?
大部分 UNIX 系统的做法是一直等到可以把应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。 怎么理解呢?
操作系统内核是很聪明的,当 TCP 连接建立之后,它就开始运作起来。你可以把发送缓冲区想象成一条包裹流水线,有个聪明且忙碌的工人不断地从流水线上取出包裹(数据);
这个工人会按照 TCP/IP 的语义,将取出的包裹(数据)封装成 TCP 的 MSS 包,以及 IP 的 MTU 包;
这样我们的发送缓冲区就又空了一部分,于是又可以继续从应用程序搬一部分数据到发送缓冲区里,这样一直进行下去,到某一个时刻,应用程序的数据可以完全放置到发送缓冲区里。在这个时候,write 阻塞调用返回。