调用数据发送接口后
数据只是从应用程序拷贝到了系统内核的发送缓冲区中。由TCP协议栈来负责发送出去。
流量控制
原因:
发送端和接收端都有自己的处理能力和存储规模,所以需要流量控制。
发送/接收窗口:
发送窗口和接收窗口时TCP连接的双方,一个作为生产者,一个作为消费者,为了达到一致协同的生产-消费速率,而产生的算法模型实现
拥塞控制
原因
TCP要考虑在多个连接共享在有限的带宽上,兼顾效率和公平性的控制
常用算法:
●慢启动通过一定的规则,慢慢地将网络发送数据的速率增加到一个阈值。超过这个阈值之后,慢启动就结束了
●拥塞避免登场。在这个阶段,TCP 会不断地探测网络状况,并随之不断调整拥塞窗口的大小。
发送窗口和拥塞窗口
发送窗口:反应了作为单TCP连接,点对点之间的流量控制模型,需要和接收端一起共同协调来调整大小。
拥塞窗口:多个TCP连接共享带宽的拥塞控制模型,发送端独立地根据网络状况来动态调整
TCP发送缓冲区数据速度的因素:
至少取决于两个因素,一般取两者的最小值作为判断依据
1.当前的发送窗口大小
2.拥塞窗口大小
几个场景和解决
第一个场景: 接收端读一点就让发送端写
情况:接收端处理得急不可待,比如刚刚读入了 100 个字节,就告诉发送端:“喂,我已经读走 100 个字节了,你继续发”,在这种情况下,你觉得发送端应该怎么做呢?
处理:糊涂窗口综合征 ,需要接收端优化。接收端应该在自己的缓冲区大刀一个合理的值后,再向发送端发送窗口更新通知。这个合理的值由对应的RFC规范定义。
第二个场景: 每次传输数据很小
情况:是所谓的“交互式”场景,比如我们使用 telnet 登录到一台服务器上,或者使用 SSH 和远程的服务器交互,这种情况下,我们在屏幕上敲打了一个命令,等待服务器返回结果,这个过程需要不断和服务器端进行数据传输。这里最大的问题是,每次传输的数据可能都非常小,比如敲打的命令“pwd”,仅仅三个字符。这意味着什么?这就好比,每次叫了一辆大货车,只送了一个小水壶。在这种情况下,你又觉得发送端该怎么做才合理呢?
处理:需要在发送端进行优化。这个优化的算法叫作Nagle算法,Nagle算法的本质就是让大批量的小数据包同时发送。为此它提出,在任何一个时刻,未被确认的小数据包(长度小于最大报文段长度MSS的TCP分组,MSS是TCP数据包每次能够传输的最大数据分段)不能超过一个。这样发送端就可以把接下来的几个小数据包存储起来,等待接收到前一个小数据包的ACK后,一次性发送出去。
第三个场景: 大量ACK报文
情况:是从接收端来说的。我们知道,接收端需要对每个接收到的 TCP 分组进行确认,也就是发送 ACK 报文,但是 ACK 报文本身是不带数据的分段,如果一直这样发送大量的 ACK 报文,就会消耗大量的带宽。之所以会这样,是因为 TCP 报文、IP 报文固有的消息头是不可或缺的,比如两端的地址、端口号、时间戳、序列号等信息, 在这种情形下,你觉得合理的做法是什么?
:处理:延时ACK,即收到数据不马上回复,而是累计需要发送的ACK报文,等到有数据要发送给对端时,将累计的ACK一并发送出去。不过不能无限延时(设定一个阈值,没有数据发送的话达到阈值发累计的ACK),否则发送端会认为发送失败触发重传。
Nagle算法和延时ACK的矛盾 :两个优化互相阻止,增大了处理时延
例子:比如,客户端分两次将一个请求发送出去,由于请求的第一部分的报文未被确认,Nagle 算法开始起作用;同时延时 ACK 在服务器端起作用,假设延时时间为 200ms,服务器等待 200ms 后,对请求的第一部分进行确认;接下来客户端收到了确认后,Nagle 算法解除请求第二部分的阻止,让第二部分得以发送出去,服务器端在收到之后,进行处理应答,同时将第二部分的确认捎带发送出去。
解决:
1.没有Nagle的话,请求的第二部分早就发出去了,可以关闭Nagle算法
//除非有十足的把握,否则不要改变默认的TCP Nagle算法
int on = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&on, sizeof(on));
2.可以将写操作合并,将请求一次性发送过去,而不是分开两部分独立发送,结果会好很多
合并写操作
作用
调用 writev 操作时,会自动把几个数组的输入合并成一个有序的字节流,然后发送给对端。
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt)
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
iovec 结构:
定义如下,这两个函数的第二个参数都是指向某个 iovec 结构数组的一个指针
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
使用:
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: tcpclient <IPaddress>");
}
int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed ");
}
char buf[128];
struct iovec iov[2];
char *send_one = "hello,";
iov[0].iov_base = send_one;
iov[0].iov_len = strlen(send_one);
iov[1].iov_base = buf;
while (fgets(buf, sizeof(buf), stdin) != NULL) {
iov[1].iov_len = strlen(buf);
int n = htonl(iov[1].iov_len);
if (writev(socket_fd, iov, 2) < 0)
error(1, errno, "writev failure");
}
exit(0);
}
运行结果:
输入:
world
network
结果:
received 12 bytes: hello,world
received 14 bytes: hello,network