为什么下面的代码很慢? 而慢,我的意思是100x-1000x慢。 它只是直接在TCP套接字上直接读写。 好奇的是,只有当我使用两个函数调用来读写时,它才会很慢,如下所示。 如果我更改服务器或客户端代码使用单个函数调用(如注释中),它变得非常快。
代码片段:
int main(...) { int sock = ...; // open TCP socket int i; char buf[100000]; for(i=0;i<2000;++i) { if(amServer) { write(sock,buf,10); // read(sock,buf,20); read(sock,buf,10); read(sock,buf,10); }else { read(sock,buf,10); // write(sock,buf,20); write(sock,buf,10); write(sock,buf,10); } } close(sock); }
我们在一个更大的程序中偶然发现了这个,实际上是使用stdio缓冲。 当有效载荷大小略微超过缓冲区大小的时候,它神秘地变得缓慢。 然后我用strace
挖了一下,最后把问题解决了。 我可以通过缓冲策略来解决这个问题,但我真的很想知道这里究竟发生了什么。 在我的机器上,当我将两个读取呼叫更改为单个呼叫时,从我的机器上0.030秒到超过一分钟(在本地和远程机器上testing)。
这些testing是在各种Linux发行版和各种内核版本上完成的。 同样的结果。
完全可运行的代码与networking样板:
#include <netdb.h> #include <stdbool.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/ip.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> static int getsockaddr(const char* name,const char* port, struct sockaddr* res) { struct addrinfo* list; if(getaddrinfo(name,port,NULL,&list) < 0) return -1; for(;list!=NULL && list->ai_family!=AF_INET;list=list->ai_next); if(!list) return -1; memcpy(res,list->ai_addr,list->ai_addrlen); freeaddrinfo(list); return 0; } // used as sock=tcpConnect(...); ...; close(sock); static int tcpConnect(struct sockaddr_in* sa) { int outsock; if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1; if(connect(outsock,(struct sockaddr*)sa,sizeof(*sa))<0) return -1; return outsock; } int tcpConnectTo(const char* server, const char* port) { struct sockaddr_in sa; if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1; int sock=tcpConnect(&sa); if(sock<0) return -1; return sock; } int tcpListenAny(const char* portn) { in_port_t port; int outsock; if(sscanf(portn,"%hu",&port)<1) return -1; if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1; int reuse = 1; if(setsockopt(outsock,SOL_SOCKET,SO_REUSEADDR, (const char*)&reuse,sizeof(reuse))<0) return fprintf(stderr,"setsockopt() failed\n"),-1; struct sockaddr_in sa = { .sin_family=AF_INET, .sin_port=htons(port) , .sin_addr={INADDR_ANY} }; if(bind(outsock,(struct sockaddr*)&sa,sizeof(sa))<0) return fprintf(stderr,"Bind failed\n"),-1; if(listen(outsock,SOMAXCONN)<0) return fprintf(stderr,"Listen failed\n"),-1; return outsock; } int tcpAccept(const char* port) { int listenSock, sock; listenSock = tcpListenAny(port); if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed\n"),-1; close(listenSock); return sock; } void writeLoop(int fd,const char* buf,size_t n) { // Don't even bother incrementing buffer pointer while(n) n-=write(fd,buf,n); } void readLoop(int fd,char* buf,size_t n) { while(n) n-=read(fd,buf,n); } int main(int argc,char* argv[]) { if(argc<3) { fprintf(stderr,"Usage: round {server_addr|--} port\n"); return -1; } bool amServer = (strcmp("--",argv[1])==0); int sock; if(amServer) sock=tcpAccept(argv[2]); else sock=tcpConnectTo(argv[1],argv[2]); if(sock<0) { fprintf(stderr,"Connection failed\n"); return -1; } int i; char buf[100000] = { 0 }; for(i=0;i<4000;++i) { if(amServer) { writeLoop(sock,buf,10); readLoop(sock,buf,20); //readLoop(sock,buf,10); //readLoop(sock,buf,10); }else { readLoop(sock,buf,10); writeLoop(sock,buf,20); //writeLoop(sock,buf,10); //writeLoop(sock,buf,10); } } close(sock); return 0; }
编辑:这个版本与其他片段略有不同,它读取/写入循环。 所以在这个版本中,即使readLoop
只被调用一次,两个独立的写操作也会自动产生两个独立的read()
调用。 但否则,问题依然存在。
有趣。 您与TCP延迟确认一起成为Nagle算法的受害者。
Nagle的算法是一种用于TCP的机制,用于推迟小型段的传输,直到已经积累了足够的数据,使其值得建立并通过网络发送段。 从维基百科的文章:
Nagle的算法通过组合一些小的传出消息,并一次发送它们。 具体而言,只要发送方没有收到确认信息,发送方就应该对输出进行缓冲,直到它具有完整的分组输出为止,以便输出可以一次全部发送。
然而,TCP通常使用一种称为TCP延迟确认的技术,这是一种由累积一批ACK应答(因为TCP使用累积ACKS)组成的技术,以减少网络流量。
该维基百科文章进一步提到这一点:
在启用两种算法的情况下,对TCP连接进行两次连续写入的应用程序,然后在第二次写入的数据到达目的地之前将不会被执行的读取经历长达500毫秒的恒定延迟,而“ ACK延迟“ 。
(强调我的)
在您的具体情况中,由于服务器在读取答复之前不发送更多数据,客户端正在造成延迟:如果客户端写入两次 , 则第二次写入将被延迟 。
如果Nagle的算法被发送方使用,数据将被发送方排队,直到收到ACK。 如果发送方没有发送足够的数据来填充最大分段大小(例如,如果它执行了两个小的写入,然后是阻塞读取),那么传输将暂停到ACK延迟超时。
所以,当客户端进行2次写入调用时,会发生以下情况:
用1写,这是发生了什么事情:
ACK
是TCP头部的一部分,所以如果你正在写,你可能不需要额外的费用就可以承认以前的部分。 这样做。 如果您想在客户端继续写两次,则需要禁用Nagle的算法。 这是算法作者自己提出的解决方案:
用户级解决方案是为了避免套接字上的写入 – 读取序列。 写读写读取是好的。 写写入是好的。 但是写写读是一个杀手。 所以,如果可以的话,把你的小写入缓冲到TCP并一次性发送。 在每次读取之前使用标准的UNIX I / O包和刷新写入通常是可行的。
( 请参阅维基百科上的引文 )
正如大卫·施瓦茨在评论中提到的那样 ,这可能不是出于各种原因的最好的想法,但它说明了这一点,并表明这确实造成了延误。
要禁用它,您需要使用setsockopt(2)
在套接字上设置TCP_NODELAY
选项。
这可以在客户端的tcpConnectTo()
完成:
int tcpConnectTo(const char* server, const char* port) { struct sockaddr_in sa; if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1; int sock=tcpConnect(&sa); if(sock<0) return -1; int val = 1; if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0) perror("setsockopt(2) error"); return sock; }
并在tcpAccept()
为服务器:
int tcpAccept(const char* port) { int listenSock, sock; listenSock = tcplistnAny(port); if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed\n"),-1; close(listenSock); int val = 1; if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0) perror("setsockopt(2) error"); return sock; }
看到这个巨大的差异是很有趣的。
如果您不想弄乱套接字选项,那么确保客户端在下次读取之前只写一次,而且只写一次就足够了。 您仍然可以让服务器读取两次:
for(i=0;i<4000;++i) { if(amserver) { writeLoop(sock,buf,10); //readLoop(sock,buf,20); readLoop(sock,buf,10); readLoop(sock,buf,10); }else { readLoop(sock,buf,10); writeLoop(sock,buf,20); //writeLoop(sock,buf,10); //writeLoop(sock,buf,10); } }