连接建立的根本目的是为了数据的收发。
1、发送数据
发送数据时常用的有三个函数,分别是 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 函数,操作系统内核帮我们不断地往文件系统中写入字节流。注意,写入的字节流大小通常和输入参数 size 的值是相同的,否则表示出错。
对于套接字描述符而言,它代表了一个双向连接,在套接字描述符上调用 write 写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的。
2、发送缓冲区
当 TCP 三次握手成功,TCP 连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。
发送缓冲区的大小可以通过套接字选项来改变,当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
这里有几种情况:
第一种情况很简单,操作系统内核的发送缓冲区足够大,可以直接容纳这份数据,那么皆大欢喜,我们的程序从 write 调用中退出,返回写入的字节数就是应用程序的数据大小。
第二种情况是,操作系统内核的发送缓冲区是够大了,不过还有数据没有发送完,或者数据发送完了,但是操作系统内核的发送缓冲区不足以容纳应用程序数据,在这种情况下,你预料的结果是什么呢?报错?还是直接返回?
操作系统内核并不会返回,也不会报错,而是应用程序被阻塞,也就是说应用程序在 write 函数调用处停留,不直接返回。术语“挂起”也表达了相同的意思,不过“挂起”是从操作系统内核角度来说的。
实际上,每个操作系统内核的处理是不同的。大部分 UNIX 系统的做法是一直等到可以把应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。怎么理解呢?
当 TCP 连接建立之后,它就开始运作起来。你可以把发送缓冲区想象成一条包裹流水线,有个聪明且忙碌的工人不断地从流水线上取出包裹(数据),这个工人会按照 TCP/IP 的语义,将取出的包裹(数据)封装成 TCP 的 MSS 包,以及 IP 的 MTU 包,最后走数据链路层将数据发送出去。这样我们的发送缓冲区就又空了一部分,于是又可以继续从应用程序搬一部分数据到发送缓冲区里,这样一直进行下去,到某一个时刻,应用程序的数据可以完全放置到发送缓冲区里。在这个时候,write 阻塞调用返回。注意返回的时刻,应用程序数据并没有全部被发送出去,发送缓冲区里还有部分数据,这部分数据会在稍后由操作系统内核通过网络发送出去。
3、读取数据
套接字描述本身和本地文件描述符并无区别,在 UNIX 的世界里万物都是文件,这就意味着可以将套接字描述符传递给那些原先为处理本地文件而设计的函数。这些函数包括 read 和 write 交换数据的函数。
最简单的 read 函数的原型如下:
ssize_t read (int socketfd, void *buffer, size_t size)
read 函数要求操作系统内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中。返回值告诉我们实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),这在网络中表示对端发送了 FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。
4、服务器端读取数据程序
下面是服务器端读取数据的程序:
#include "lib/common.h" void read_data(int sockfd) { ssize_t n; char buf[1024]; int time = 0; for (;;) { fprintf(stdout, "block in read\n"); if ((n = readn(sockfd, buf, 1024)) == 0) return; time++; fprintf(stdout, "1K read for %d \n", time); usleep(1000); } } int main(int argc, char **argv) { int listenfd, connfd; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(12345); /* bind到本地地址,端口为12345 */ bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); /* listen的backlog为1024 */ listen(listenfd, 1024); /* 循环处理用户请求 */ for (;;) { clilen = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen); read_data(connfd); /* 读取数据 */ close(connfd); /* 关闭连接套接字,注意不是监听套接字*/ } }
对服务器端程序解释如下:
- 21-35 行先后创建了 socket 套接字,bind 到对应地址和端口,并开始调用 listen 接口监听;
- 38-42 行循环等待连接,通过 accept 获取实际的连接,并开始读取数据;
- 8-15 行实际每次读取 1K 数据,之后休眠 1 秒,用来模拟服务器端处理时延。
5、客户端发送数据程序
下面是客户端发送数据的程序:
#include "lib/common.h" #define MESSAGE_SIZE 102400 void send_data(int sockfd) { char *query; query = malloc(MESSAGE_SIZE + 1); for (int i = 0; i < MESSAGE_SIZE; i++) { query[i] = 'a'; } query[MESSAGE_SIZE] = '\0'; const char *cp; cp = query; size_t remaining = strlen(query); while (remaining) { int n_written = send(sockfd, cp, remaining, 0); fprintf(stdout, "send into buffer %ld \n", n_written); if (n_written <= 0) { error(1, errno, "send failed"); return; } remaining -= n_written; cp += n_written; } return; } int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) error(1, 0, "usage: tcpclient <IPaddress>"); sockfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(12345); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); int connect_rt = connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); if (connect_rt < 0) { error(1, errno, "connect failed "); } send_data(sockfd); exit(0); }
对客户端程序解释如下:
- 31-37 行先后创建了 socket 套接字,调用 connect 向对应服务器端发起连接请求
- 43 行在连接建立成功后,调用 send_data 发送数据
- 6-11 行初始化了一个长度为 MESSAGE_SIZE 的字符串流
- 16-25 行调用 send 函数将 MESSAGE_SIZE 长度的字符串流发送出去
发送成功仅仅表示的是数据被拷贝到了发送缓冲区中,并不意味着连接对端已经收到所有的数据。至于什么时候发送到对端的接收缓冲区,或者更进一步说,什么时候被对方应用程序缓冲所接收,对我们而言完全都是透明的。