socket用户缓冲区、socket内核缓冲区与tcp协议buffer(滑动窗口)的关系
1 操作系统socket内核缓冲区是tcp协议buffer(滑动窗口)的具体实现
2 用户缓冲区即是局部的byte[]
https://www.zhihu.com/question/48454744
TCP流量控制中的滑动窗口大小、TCP字段中16位窗口大小、MTU、MSS、缓存区大小有什么关系?
TCP流量控制中的滑动窗口大小、TCP字段中16位窗口大小、MTU、MSS、缓存区大小有什么关系?
链接:https://www.zhihu.com/question/48454744/answer/580308144
来源:知乎
著作权归作者所有。商业请联系作者获得授权,非商业请注明出处。
"还有内核socket接受和发送缓存区的大小"
就从这句话为切入点展开回答下, 内核发送接收缓冲区和应用层发送接收缓冲区
操作系统一般分为用户态和内核态, 用户态一般无法直接操作内核, 和内核打交道都是通过操作系统提供的sdk来完成
比如你要申请一块堆内存, 上层写个malloc()或者new就可以, 传递到内核后人家帮你干了一堆事情, 成功的话就给你返回可用的指针.
比如想创建个socket搞网络io, 上层调用一个socket()就完事了, 到内核里面人家又干了一堆事情
如分配资源,寻找还可用的fd. 干完一堆事情之后成功的话, 才给你返回一个可用的fd
那么问题来了, 在socket收发(send() recv())的时候, 内核干了啥
先说socket发送, 函数原型如下所示
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
.参数很简单, 无非就是之前创建的fd, 以及要发送缓冲区的首地址以及发送数据的数量.
这里的参数2[const void *buf]算是用户态的发送缓冲区
我可以写出如下的代码
segment_1
char send_buf[0xff] = __TIMESTAMP__;
for (int i = 0; i != 0x10; i++)
{
if (send(cli_sock, send_buf, strlen(send_buf), 0) == -1)
{
fprintf(stderr, "call send() failed, errno:[%d]\n", errno);
return -1;
}
}
在这段代码里, send_buf是应用层(用户态)的缓冲区, 在循环连续发送16次(x010)后, 是不是最后只会发送一次, 因为无论多少次循环缓冲区指针都是send_buf.
其实当然不是, 因为内核有自己的发送缓冲区, 和应用层彼此独立.
换句话说, 作为上层调用者, 只要关心send()的返回值就可以了, 每次成功调用后, 都会把应用层缓冲区里的数据复制一份到内核的发送缓冲区队列里, 至于每次复制多少次数据, 得看send()的返回值.
如果数据一次性都成功发送出去了(即一次send()的返回值和参数3相等)
上层就不用关心应用层缓冲区的有效性了, 上述代码调用的16次send()会把字符串复制16次到内核缓冲区里, 至于为啥内核要留一份数据的备份, 后面说.
看第二段代码
segment_2
char send_buf[0xff] = __TIMESTAMP__;
size_t total_sb = 0;
while (total_sb < strlen(send_buf))
{
size_t sb = 0;
if ((sb = send(cli_sock, send_buf + total_sb, strlen(send_buf) - total_sb, 0)) == -1)
{
fprintf(stderr, "call send() failed, errno:[%d]\n", errno);
return -1;
}
total_sb += sb;
}
这里有个循环, 目的是期望一次性把数据都发出去, 但也同时兼容了如果对于一个缓冲区一次性发不出去(如发送缓冲区满), 那么就分多次发送, 每一次累计该次发送成功的字节数, 那下一次再发就会从缓冲区未发送的位置开始
接下来聊聊send()函数的返回值问题, 因其和内核态缓冲区有极大关系
(ps:每一个面试官最爱问的就是, send()调用成功后, 返回值为你发送数据的字节数, 是不是就代表了对端成功接收了呀)
在调用send()成功返回后(正整数, 不一定要等于你发送数据包的字节数)
a> 是否可以代表对端应用层(接收缓冲区)已经接受了你发的数据呢?
必须代表不了, 人家对端代码里咋写你完全控制不了, 甚至可以不写recv()函数(从内核态到用户态的拷贝), 建立连接后啥也不管(无send()和recv())程序就在那儿如美男子一样静静的挂着
b> 是否可以代表对面的内核(接收缓冲区)已经接到了你发的数据呢?
其实也代表不了, 举个栗子
A(客户端) B(服务端)两端三次握手(SYN ACKSYN ACK)建立起连接后
B端直接拔网线, 或者禁用网络(这种情况不同于B进程直接死了, 如果B进程crash后会给每一个与其ESTABLISED状态(建立的连接)发一个RST, 强制终止所有与之建立起来的链接)
而在A端看来, B端没有其他动作, 仍然和其处于ESTABLISED. (先不考虑协议层KEEPLIVE若干个心跳包检测的问题)
那么此时在客户端A调用send(), 仍然可以成功返回, 数据会在A端的内核发送区不停累积
如果是阻塞socket, 当内核发送缓冲区积累的数据达到上限(可以用setsockopt()调整SND_SENDBUFF 来设置具体大小), 线程就会挂起, 等待内核发送缓冲区重新可用(获取对端ack释放部分数据腾出新的空间)
在此期间内核缓冲区不停的尝试重发(TCP超时重传特质), 直到若干次后发现对端没反应发RST断开连接.
c> 那么send()返回成功能代表啥
可能只代表一个是你发送端应用层缓冲区的数据成功复制到了你发送端内核层的缓冲区(复制多少看返回值看返回值)
另外一个就是可能代表目前的发送缓冲区仍然可用(可能还没满)
那么内核态的发送缓冲区什么时候可用空间增大(释放掉部分数据)?
等对端内核态接收缓冲区的ACK(对端不必一定调用recv()从内核态读到用户态)后, 发送端的内核态发送缓冲区就可以放心的释放掉ACK确认了的那些数据, 不用再维护了(TCP可靠性机制之一)
这也就是题主你问的内核缓冲区和发送缓冲区以及socket 发送的问题
至于接收和发送差不多.
假设一个阻塞的socket
在没数据的时候调用recv()会导致线程挂起
内核态接收缓冲区"有可以返回给上层数据"的话线程就会唤醒, 调用recv()就可以把数据从内核态的接收缓冲区拷贝到应用层的缓冲区.
这里说的有"有可以返回给上层数据"要注意下, 因为TCP数据包在往下一层(网络层)封装的时候要加上ip头作为ip数据报在链路层之间传递. 而ip数据报的发送是不保证对端接收的顺序一定按照发送的顺序, 所以完全可能是后发的ip数据报(实际为TCP)先到达, 如发送端用TCP发送了ABC三个字节, 那么对端内核态的接收缓冲区完全可以先收到BC, 最后再收到A. 当只有BC的时候, 虽然socket的接收缓冲区内有数据, 但是并不会往上层返回也不会给发送端发送ACK.只有收到前面连续的数据包时才会返回给上层, 同时会发一个ACK给发送端同时确认收到了ABC三个字符, 所以当上层接收的时候都是排序好的数据
简单总结下彼此的关系
内核发送缓冲区数据的增加: 应用层调用send()成功, 增加的数据量依赖于send()的返回值.
内核发送缓冲区数据的减少: 收到对端内核接收缓冲区对收到的数据发送ACK回执
内核接收缓冲区数据的增加: 接收到对端内核态发送的数据(并会给这些数据发送ACK)
内核接收缓冲区数据的减少: 应用层调用recv()成功, 内核删除的数据依赖于应用层读取了多少数据
3 内核缓冲区(滑动窗口)大小占16位,最大65536长度字节数,三次握手时通知对方
4 滑动窗口不完全等于内核缓冲区,应描述为内核缓冲区剩余可用字节数
https://www.cnblogs.com/lisuyun/articles/5803352.html
原文地址:http://blog.csdn.net/yusiguyuan/article/details/21439633#1536434-tsina-1-74921-66a1f5d8f89e9ad52626f6f40fdeadaa
TCP/IP详解--举例明白发送/接收缓冲区、滑动窗口协议之间的关系.
一个例子明白发送缓冲区、接受缓冲区、滑动窗口协议之间的关系。
在上面的几篇文章中简单介绍了上述几个概念在TCP网络编程中的关系,也对应了几个基本socket系统调用的几个行为,这里再列举一个例子,由于对于每一个TCP的SOCKET来说,都有一个发送缓冲区和接受缓冲区与之对应,所以这里只做单方向交流,不做互动,在recv端不send,在send端不recv。细细揣摩其中的含义。
一、recv端
在监听套接字上准备accept,在accept结束以后不做什么操作,直接sleep很久,也就是在recv端并不做接受数据的操作,在sleep结束之后再recv数据。
二、send端
通过查看本系统内核默认的支持的最大发送缓冲区大小,cat/proc/sys/net/ipv4/tcp_wmem,最后一个参数为发送缓冲区的最大大小。接受缓冲区最大的配置文件在tcp_rmen中。
将套接字设置为阻塞,一次发送的buffer大于最大发送缓冲区所能容纳的数据量,一次send结束,在发送返回后接着答应发送的数据长度
测试结果:
阶段一:
接受端表现:在刚开始发送数据时,接收端处于慢启动状态,滑动窗口大小越来愈大,但是由于接收端不处理接受缓冲区内的数据,其滑动窗口越来越小(因为接受端回应发送端中的win大小表示接受端还能够接受多少数据,发送端下次发送的数据大小不能超过回应中win的大小),最后发送端回应给接受端的ACK中显示的win大小为0,表示接收端不能够再接受数据。
发送端表现:发送端一直不能返回,如果接受端一直回应win为0的情况下,发送端的send就会一直不能返回,这种僵局一直持续到接收端的sleep结束。
原因分析:首先需要明白几个事实,阻塞式I/O会一直等待,直达这个操作完成;发送端接受到接收端的回应后才能将发送缓冲区中的数据进行清空。
在接收端不recv,那么接收端的接受缓冲区内会一直有数据,接受缓冲区满,导致滑动窗口为0,导致发送端不能发送数据。但是send操作为何不能反悔呢?send操作只是将应用缓冲区的数据拷贝到发送缓冲区,但是发送缓冲区的数据并没有完全得到接收端的ACK回应,所以暂时不能将发送缓冲区中的数据丢弃,导致发送缓冲区的被填满,这样应用层中的数据也就不能拷贝到内核发送缓冲区内,也就会一直阻塞在这里,直到可以继续讲应用层的数据拷贝到发送缓冲区中,何时触发这个操作呢?等到发送端回应win大于0时才有这样的操作。
阶段二;
接受端:在sleep结束以后,开始调用recv系统调用。这个时候接受端的滑动窗口又开始大于零。那么这样就唤醒了发送端继续发送数据。
发送端:发送端接受到接收端win大于0的回应,这个时候发送端又可以将应用层buffer中的数据拷贝到内核的发送缓冲区中。
原因分析:由于接受端调用recv将内核接受缓冲区的数据拷贝到应用层中,这样滑动窗口又大于0了所以激发了发送端继续发送数据,由于发送端可以发送数据了,内核协议栈便将发送缓冲区中的数据发送给接受端,这样发送缓冲区又有空间了,那么send操作就可以将应用层的数据拷贝到发送缓冲区了!这样的操作一直保持到send操作返回,这样代表着将应用层的数据全部拷贝到发送缓冲区内,但不代表将数据发送给对端。发送给对端成功的标志是接受到对端的ACK回应,这个时候发送端才可以将发送缓冲区的数据丢弃。不丢弃的原因是时刻准备重发丢失/出错的数据!
Ps: TCP通信为了保证可靠性,每次发送的数据都需要得到对方的ACK才确认对方收到了(仅保证对方TCP接收缓冲收到 数据了,但不保证对方应用程序取到数据了),这时如果每次发送一次就要停下来等着对方的ACK消息,显然是一种极大的资源浪费和低下的效率,这时就有了滑动窗口的出现。
发送方的滑动窗口维持着当前发送的帧序号,已发出去帧的计时器,接收方当前的窗口大小(由接收方ACK通知,大体等于接收缓冲大小-未处理的消息包),接收方滑动窗口保存的有已接收的帧信息、期待的下一帧的帧号等,至于滑动窗口的具体工作原理这里就不说了。
一 个socket有两个滑动窗口(一个sendbuf、一个recvbuf),两个窗口的大小是通过setsockopt函数设置的,现在问题就出在这里, 通过抓包显示,设置的窗口大小没有生效,最后排查发现setsockopt函数是后来加上的,写到了listen函数的后面,这样每次accept出的 socket并没有继承得到主socket设置的窗口大小,无语啊……
解决办法:setsockopt函数提前到listen函数之前,这样在服务器程序启动监听前recvbuf就已经有了,accept后的链接得到的就是recvbuf了,启动程序运行,抓包显示窗口已经是指定的大小了。
一、TCP的滑动窗口大小实际上就是socket的接收缓冲区大小的字节数
二、 对于server端的socket一定要在listen之前设置缓冲区大小,因为,accept时新产生的socket会继承监听socket的缓冲区大 小。对于client端的socket一定要在connet之前设置缓冲区大小,因为connet时需要进行三次握手过程,会通知对方自己的窗口大小。在 connet之后再设置缓冲区,已经没有什么意义。
三、由于缓冲区大小在TCP头部只有16位来表示,所以它的最大值是65536,但是对于一些情况来说需要使用更大的滑动窗口,这时候就要使用扩展的滑动窗口,如光纤高速通信网络,或者是卫星长连接网络,需要窗口尽可能的大。这时会使用扩展的32位的滑动窗口大小。
3+ https://www.icode9.com/content-4-267226.html
factor 3次握手时协商,之后不可改变
如9tcp缓冲区大小设置中就有130k的window size
3++ https://www.cnblogs.com/daimadebanyungong/p/5300790.html
|
1
2
3
4
5
6
7
|
一、TCP的滑动窗口大小实际上就是socket的接收缓冲区大小的字节数 二、 对于server端的socket一定要在listen之前设置缓冲区大小,因为,accept时新产生的socket会继承监听socket的缓冲区大 小。对于client端的socket一定要在connet之前设置缓冲区大小,因为connet时需要进行三次握手过程,会通知对方自己的窗口大小。在 connet之后再设置缓冲区,已经没有什么意义。 三、由于缓冲区大小在TCP头部只有16位来表示,所以它的最大值是65536,但是对于一些情况来说需要使用更大的滑动窗口,这时候就要使用扩展的滑动窗口,如光纤高速通信网络,或者是卫星长连接网络,需要窗口尽可能的大。这时会使用扩展的32位的滑动窗口大小。 |
5 每个tcp连接都有各自大小、相互独立的内核缓冲区
https://blog.csdn.net/stpeace/article/details/43777287
6 socket的flush,只是将数据写入操作系统缓存中,并不保证数据会立即发送,在tcp层面,由操作系统控制发送
关于tcp delayedack实践(一)tcp 14读缓冲区(滑动窗口)耗尽与write阻塞、拆包、延迟(三)
我们现在来回看2018年9月12日的思考:

“发送心跳消息,并在发送失败时关闭该连接”
一般情况下,数据会成功拷贝到操作系统内核发送缓冲区,后返回,无视你断网发不出去,直到发送缓冲区积压满


7 每个tcp连接的内核发送缓冲区SNDBUF与内核接收缓冲区RCVBUF互不干扰
8 过程

为了简便起见, 我们仅考虑单向的数据流, 即A(客户端)向B(服务端)发送数据。
在应用程序Program A中, 我们定义一个数组char szBuf[100] = "tcp"; 那么这个szBuf就是应用程序缓冲区(对应上图的Program A中的某块内存), send函数对应上面蓝色的Socket API, 内核缓冲区对应上面的黄色部分。 我们看到, send函数的作用是把应用程序缓冲区中的数据拷贝到内核缓冲区, 仅此而已。 内核缓冲区中的数据经过(DMA拷贝到)网卡, 经历网络传到B端的网卡(TCP协议), 然后(DMA拷贝)进入B的内核缓冲区, 然后由recv函数剪切/复制到Program B的应用程序缓冲区。
https://blog.csdn.net/stpeace/article/details/43719449
对于java,还有一个jvm内存拷贝到直接内存的过程 ,更具体的,参考----- 0拷贝nio直接内存
谈到网络socket编程, 我们不得不提两个基本也很重要的函数:send和recv. 对socket编程理解不深的童鞋容易产生这样一个错误的认识: send函数是用来发送数据, 而recv函数是用来接收数据的, 其实, 这种观点是稍微有点偏颇的, 掩盖了本质。
下面, 我们看一幅图, 了解一下send和recv函数所处的位置(这幅图是我在网上找的, 不太清晰, 请凑合看一下):

为了简便起见, 我们仅考虑单向的数据流, 即A(客户端)向B(服务端)发送数据。 在应用程序Program A中, 我们定义一个数组char szBuf[100] = "tcp"; 那么这个szBuf就是应用程序缓冲区(对应上图的Program A中的某块内存), send函数对应上面蓝色的Socket API, 内核缓冲区对应上面的黄色部分。 我们看到, send函数的作用是把应用程序缓冲区中的数据拷贝到内核缓冲区, 仅此而已。 内核缓冲区中的数据经过网卡, 经历网络传到B端的网卡(TCP协议), 然后进入B的内核缓冲区, 然后由recv函数剪切/复制到Program B的应用程序缓冲区。前面我们用过wireshark抓包, wireshark抓的正是流经网卡的数据。
强调一下:
1. 对于客户端A, 其发送的内核缓冲区和接收的内核缓冲区是不一样的, 互不干扰. 服务端B也同理。
2. recv函数是剪切还是复制, 由最后一个参数决定, 我们在之前的博文已经讲述过了。
下面, 我们不考虑recv函数, 仅仅玩转一下send(双向send), 并用wireshark抓包实验一下, 加深理解。 注意, Wireshark抓不了环回包, 所以, 需要在两台电脑上测试。
服务端B的程序为:
#include <stdio.h>
#include <winsock2.h> // winsock接口
#pragma comment(lib, "ws2_32.lib") // winsock实现
int main()
{
WORD wVersionRequested; // 双字节,winsock库的版本
WSADATA wsaData; // winsock库版本的相关信息
wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
// 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
WSAStartup( wVersionRequested, &wsaData );
// AF_INET 表示采用TCP/IP协议族
// SOCK_STREAM 表示采用TCP协议
// 0是通常的默认情况
unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET; // TCP/IP协议族
addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket对应的IP地址
addrSrv.sin_port = htons(8888); // socket对应的端口
// 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
// 将socket设置为监听模式,5表示等待连接队列的最大长度
listen(sockSrv, 5);
// sockSrv为监听状态下的socket
// &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
// len是包含地址信息的长度
// 如果客户端没有启动,那么程序一直停留在该函数处
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);
while(1)
{
getchar(); // 阻塞一下
send(sockConn, "tcp", strlen("tcp") + 1, 0); // send来啦
}
closesocket(sockConn);
closesocket(sockSrv);
WSACleanup();
return 0;
}
先启动服务端B.
下面我们来看客户端A:
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
WSAStartup( wVersionRequested, &wsaData );
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.101");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8888);
connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
while(1)
{
getchar();
send(sockClient, "cpp", strlen("cpp") + 1, 0);
}
closesocket(sockClient);
WSACleanup();
return 0;
}
好, 再启动客户端A.
我们在客户端A上安装wireshark并启动抓包, 实验发现:
1. 当A向B发送数据时, A上的wireshark可以抓到对应的包, 因为数据经过了A的网卡。(不管B是否有去recv)
2. 当B向A发送数据时, A上的wireshark也可以抓到对应的包, 因为数据到达了A的内核缓冲区, 也经历了A的网卡。(不管A是否有去recv)
相信通过本文的讨论, 又加深了对send和recv的认识。 未来, 路漫漫, 我们慢慢来。
TCP 协议中的 Window Size与吞吐量










理论上 ----》 
浙公网安备 33010602011771号