关于TCP Delay ACK的概念我就不多说了,到处复制粘贴标准文献以及别人的文章只能让本文篇幅加长而降低被阅读完的欲望,再者这也不是什么论文,附录参考文献几乎很少有人去看,所以我把这些都略过了。
和风吹的干皮鞋,吹的断愁绪吗?
#undef UNICODE #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <winsock2.h> #include <ws2tcpip.h> #include <stdlib.h> #include <stdio.h> // Need to link with Ws2_32.lib #pragma comment (lib, "Ws2_32.lib") #define DEFAULT_PORT "8080" int __cdecl main(void) { WSADATA wsaData; int iResult; SOCKET ListenSocket = INVALID_SOCKET; SOCKET ClientSocket = INVALID_SOCKET; struct addrinfo *result = NULL; struct addrinfo hints; int iSendResult; // Initialize Winsock iResult = WSAStartup(MAKEWORD(2,2), &wsaData); if (iResult != 0) { printf("WSAStartup failed with error: %d\n", iResult); return 1; } ZeroMemory(&hints, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE; // Resolve the server address and port iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result); if ( iResult != 0 ) { printf("getaddrinfo failed with error: %d\n", iResult); WSACleanup(); return 1; } // Create a SOCKET for connecting to server ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); if (ListenSocket == INVALID_SOCKET) { printf("socket failed with error: %ld\n", WSAGetLastError()); freeaddrinfo(result); WSACleanup(); return 1; } // Setup the TCP listening socket iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { printf("bind failed with error: %d\n", WSAGetLastError()); freeaddrinfo(result); closesocket(ListenSocket); WSACleanup(); return 1; } freeaddrinfo(result); iResult = listen(ListenSocket, SOMAXCONN); if (iResult == SOCKET_ERROR) { printf("listen failed with error: %d\n", WSAGetLastError()); closesocket(ListenSocket); WSACleanup(); return 1; } // Accept a client socket ClientSocket = accept(ListenSocket, NULL, NULL); if (ClientSocket == INVALID_SOCKET) { printf("accept failed with error: %d\n", WSAGetLastError()); closesocket(ListenSocket); WSACleanup(); return 1; } int v = 1; //setsockopt(ClientSocket, IPPROTO_TCP, TCP_QUICKACK, &v, 4); // No longer need server socket closesocket(ListenSocket); // Receive until the peer shuts down the connection do { char buff1[1] = {0}; char buff2[999] = {0}; char buff3[1000] = {0}; char buff4[2000] = {0}; iSendResult = send( ClientSocket, buff1, sizeof(buff1), 0 ); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 1000, 0); iSendResult = send( ClientSocket, buff2, sizeof(buff2), 0 ); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iSendResult = send( ClientSocket, buff3, sizeof(buff3), 0 ); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); } while (0); sleep(10); // shutdown the connection since we're done iResult = shutdown(ClientSocket, SD_SEND); if (iResult == SOCKET_ERROR) { printf("shutdown failed with error: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; } // cleanup closesocket(ClientSocket); WSACleanup(); return 0; }
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include<arpa/inet.h> #include<string.h> #include <netinet/in.h> #include <netinet/tcp.h> #define DEFAULT_PORT "8080" int main(void) { int iResult; int ListenSocket; int ClientSocket; struct sockaddr_in server; int iSendResult; char recvbuf[2000]; int recvbuflen = 2000; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(8080); // Create a SOCKET for connecting to server ListenSocket = socket(AF_INET , SOCK_STREAM , 0); if (ListenSocket == -1) { return 1; } // Setup the TCP listening socket iResult = bind( ListenSocket, (struct sockaddr *)&server , sizeof(server)); if (iResult < 0) { perror("bind"); close(ListenSocket); return 1; } iResult = listen(ListenSocket, 3); if (iResult == -1) { close(ListenSocket); return 1; } // Accept a client socket ClientSocket = accept(ListenSocket, NULL, NULL); if (ClientSocket == -1) { close(ListenSocket); return 1; } int v = 0; setsockopt(ClientSocket, IPPROTO_TCP, TCP_QUICKACK, &v, 4); // No longer need server socket close(ListenSocket); // Receive until the peer shuts down the connection do { char buff1[1] = {0}; char buff2[999] = {0}; char buff3[1000] = {0}; char buff4[2000] = {0}; iSendResult = send( ClientSocket, buff1, sizeof(buff1), 0 ); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 1000, 0); iSendResult = send( ClientSocket, buff2, sizeof(buff2), 0 ); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iSendResult = send( ClientSocket, buff3, sizeof(buff3), 0 ); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); iResult = recv(ClientSocket, buff4, 700, 0); iResult = recv(ClientSocket, buff4, 300, 0); } while (0); sleep(10); // cleanup close(ClientSocket); return 0; }
编译为Client。代码非常简单,就是连接上面的两个几乎一模一样的服务器。#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define DEFAULT_PORT 8080 int main(int argc, char** argv) { int cPort = DEFAULT_PORT; int cClient = 0; int cLen = 0; struct sockaddr_in cli; char cbuf[4096] = {0}; char buff1[700] = {0}; char buff2[300] = {0}; char buff3[1000] = {0}; if(argc < 2) { printf("Uasge: client[server IP address]\n"); return -1; } memset(cbuf, 0, sizeof(cbuf)); cli.sin_family = AF_INET; cli.sin_port = htons(cPort); cli.sin_addr.s_addr = inet_addr(argv[1]); cClient = socket(AF_INET, SOCK_STREAM, 0); if(cClient < 0) { printf("socket() failure!\n"); return -1; } if(connect(cClient, (struct sockaddr*)&cli, sizeof(cli)) < 0) { printf("connect() failure!\n"); return -1; } cLen = recv(cClient, cbuf, 1,0); cLen = send(cClient, buff1, sizeof(buff1), 0); sleep(1); cLen = send(cClient, buff2, sizeof(buff2), 0); cLen = send(cClient, buff3, sizeof(buff3), 0); cLen = recv(cClient, cbuf, 999, 0); cLen = send(cClient, buff1, sizeof(buff1), 0); cLen = send(cClient, buff2, sizeof(buff2), 0); sleep(1); cLen = send(cClient, buff1, sizeof(buff1), 0); cLen = send(cClient, buff2, sizeof(buff2), 0); cLen = recv(cClient, cbuf, 1000, 0); cLen = send(cClient, buff1, sizeof(buff1), 0); cLen = send(cClient, buff2, sizeof(buff2), 0); sleep(1); cLen = send(cClient, buff1, sizeof(buff1), 0); cLen = send(cClient, buff2, sizeof(buff2), 0); sleep(1); cLen = send(cClient, buff1, sizeof(buff1), 0); cLen = send(cClient, buff2, sizeof(buff2), 0); cLen = send(cClient, buff1, sizeof(buff1), 0); cLen = send(cClient, buff2, sizeof(buff2), 0); close(cClient); return 0; }
我给出该场景的连接拓扑:
想要解释表象的背后原理首先要摸清表象本身。很多人比如温州皮鞋厂老板太关注实现细节,上去就去看代码,这是不对的!
我来通过tcpdump来看个究竟,通过tcpdump输出的时间戳可以看出详细的传输时间分布。我们首先看连接Windows服务的tcpdump输出,因为这个可能更符合大家的预期,也就是本文序言中展示的那3种想法中的其中之一:然而相同的代码在Linux中表现却是另一番景象。它好像并不受某一个参数或者某个socket选项的影响(正如TCP_QUICKACK的manual里所述,其实如果你在服务器端的accept调用后添加设置TCP_QUICKACK选项,情况也是一样的。)。也就是说,Linux中的Delay ACK不是配置参数和socket选项决定的,而是自适应的,在这个自适应机制的范围内,系统依然提供了一个临时的开启/关闭Delay ACK的开关,即TCP_QUICKACK选项!
0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 0.000 bind(3, ..., ...) = 0 0.000 listen(3, 1) = 0 0.000...0.280 accept(3, ..., ...) = 4 0.100 < S 0:0(0) win 32792 <mss 1460, sackOK, nop, nop, nop, wscale 7> 0.100 > S. 0:0(0) ack 1 <...> 0.280 < . 1:1(0) ack 1 win 100 //0.300 setsockopt(4, IPPROTO_TCP, TCP_QUICKACK, [0], 4) = 0 0.300 write(4, ..., 1) = 1 0.300 < P. 1:701(700) ack 2 win 16425 1.300 < P. 701:1001(300) ack 2 win 16425 1.300 < P. 1001:2001(1000) ack 2 win 16425 1.300 write(4, ..., 999) = 999 1.300 %{ print tcpi_rcv_mss }% 1.300 < P. 2001:2701(700) ack 1001 win 16421 1.300 < P. 2701:3001(300) ack 1001 win 16421 2.300 < P. 3001:3701(700) ack 1001 win 16421 2.300 < P. 3701:4001(300) ack 1001 win 16421 2.300 write(4, ..., 1000) = 1000 2.300 < P. 4001:4701(700) ack 2001 win 16425 2.300 < P. 4701:5001(300) ack 2001 win 16425 3.300 < P. 5001:5701(700) ack 2001 win 16425 3.300 < P. 5701:6001(300) ack 2001 win 16425 4.300 < P. 6001:6701(700) ack 2001 win 16425 4.300 < P. 6701:7001(300) ack 2001 win 16425 4.300 < P. 7001:7701(700) ack 2001 win 16425 4.300 < P. 7701:8001(300) ack 2001 win 16425 10.300 write(4, ..., 13) = 13
这个意义如下图所示:
我们可以看出,在TCP双向数据传输是一来一回的场景下,Delay ACK可以省去所有的纯ACK段的发送。典型的比如远程终端登录(需要回显的如telnet,ssh之类)。
Linux为这种双向数据传输取了个名字,叫做pingpong。上图描述的就是一个完美的pingpong模式,对于一条TCP连接的任意一端来讲,pingpong模式指的就是“R-W-R-W-R-W-R-W...”(其中R代表Read,W代表Write,简称RW模式)模式,但是事实上,在现实中,这种完美适应Delay ACK的RW模式几乎不存在,如果出现RRW模式恰逢对端启用了Nagle的话,就会出现问题,因此,不管是开启还是关闭Delay ACK,都无法完美平衡ACK开销与传输延迟之间的矛盾,自适应机制势在必行。buff = alloc_skb(MAX_TCP_HEADER, GFP_ATOMIC);
if (buff == NULL) {
inet_csk_schedule_ack(sk);
inet_csk(sk)->icsk_ack.ato = TCP_ATO_MIN;
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
TCP_DELACK_MAX, TCP_RTO_MAX);
return;
}
if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)
icsk->icsk_ack.pingpong = 1;
联系客服