发送数据
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写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的
- 产生这个现象的原因在于操作系统内核为读取和发送数据做了很多我们表面上看不到的工作。
发送缓冲区
当 TCP 三次握手成功,TCP 连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。
当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
几种情况:
- 第一种,操作系统内核的发送缓冲区足够大,可以直接容纳这份数据,那么皆大欢喜,我们的程序从 write 调用中退出,返回写入的字节数就是应用程序的数据大小。
- 操作系统内核的发送缓冲区不够时,应用程序在 write 函数调用处停留,不直接返回(“挂起”)。
- 关于返回:大部分 UNIX 系统是一直等到可以把应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。
读取数据
UNIX 的世界一切皆文件
read 函数
ssize_t read (int socketfd, void *buffer, size_t size)
- socketfd:读取最多多少个字节,并将结果存储到buffer中。
- 返回值告诉实际读取的字节数目,返回值为0,表示EOF(end-of-file),表示对端发送了FIN包,要处理断连的情况;返回值为-1,表示出错。
缓冲区实验
例子中客户端不断地发送数据,服务器端每读取一段数据之后进行休眠,以模拟实际业务处理所需要的时间。
服务端代码
#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 秒,用来模拟服务器端处理时延。
客户端代码
#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 长度的字符串流发送出去实验一: 观察客户端数据发送行为
程序结果:
1、客户端程序发送了一个很大的字节流,服务端不断地在屏幕上打印出读取字节流,而客户端直到最后所有的字节流发送完毕才打印出下面的一句话,说明在此之前 send 函数一直都是阻塞的,也就是说阻塞式套接字最终发送返回的实际写入字节数和请求字节数是相等的。
2、服务端处理变慢
发送成功仅仅表示的是数据被拷贝到了发送缓冲区中,并不意味着连接对端已经收到所有的数据。至于什么时候发送到对端的接收缓冲区,或者更进一步说,什么时候被对方应用程序缓冲所接收,对我们而言完全都是透明的。
总结:
对于 send 来说,返回成功仅仅表示数据写到发送缓冲区成功,并不表示对端已经成功收到。
对于 read 来说,需要循环读取数据,并且需要考虑 EOF 等异常条件。
问题:一段数据流从应用程序发送端,一直到应用程序接收端,总共经过了多少次拷贝?
- 1.应用程序将数据发送缓冲区时,调用send或write方法,如果缓冲中没有空间,系统调用就会失败或阻塞。这个动作就是==”显示拷贝“==。
- 2。TCP协议栈工作,创建Packet报并,并把报文发送到传输队列中(qdisc),传输队列是一个典型的FIFO队列。
TX ring 在网卡驱动和网卡之前,也是一个传输请求的队列! - 3.网卡作为物理设备工作在物理层,主要工作是吧发送的报文保存到内部的缓冲中,并发送出去。
- 4.接来下再看接受端,报文首先到达网卡,由网卡保存在自己的接受缓冲中,接下来报文被发送至网络驱动和网卡之前的RX ring,网络驱动从RX ring获取报文,然后把报文发送到上层。
- 5.网络驱动的上层之前没有缓冲,因为网络驱动使用Napi进行数据传输,因此,可以认为上层直接从RX ring中获取报文。
- 6.最后,报文的数据保存在套接字接受缓冲中,应用程序从套接字接受缓存中读取数据。