文章未完成...
最近写了一个TCP文件传输的C/S程序。
Client是windows C++程序。
Server是Linux C++(ubuntu)程序,虚拟机。
作用是,可以从客户端向服务端传输文件(单线程)。
测试用例是一个wireshark抓包文件,文件大小约235m。
初始的测试结果发现,传输完文件需要
Client debug版:5’42’‘,
Client release版: 2’30’'。
同样的文件,vscode从ubuntu下载到windows,耗时约19‘’半(测试了3次都是19’'多一点),
使用FileZilla Client,只要2‘’半(怎么做到这么快的?)。
时间差距这么大,感觉很离谱,因此我想借这个机会研究一下,怎么增大C++程序的TCP传输速度。
完整代码在这里
优化TCP传输速度的方法
- 把一个文件分成几块,每块用一个线程来传输,这样应该更快。(我觉得这个方法可以,但是还没试,这个文章中也不准备尝试这个方法。)
- 因为是文件传输,所以优化文件读写,应该可以节省一些时间。(按字节读写文件,按照指定buff大小循环读写文件,零拷贝sendfile)
- 设置TCP缓冲区的大小(LINUX)
- TCP_NODELAY选项,禁用 Nagle 算法
- TCP_CORK选项
- 虚拟机由桥接改成NAT
暂时搜集到这些方法,No1后面再尝试。
二、优化发送和接受的代码
目前用的方法就是对文件read和write指定大小的数组。尝试以下几种办法。
linux缓冲区设置为:
查看当前的TCP缓冲区大小设置
zlc@192:~/share$ sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 2048 2048 4096
代码中不使用setsockopt修改SO_RCVBUFF。
按照字节读写文件
这个先不考虑了。
按照指定buff大小循环读写文件
这个就是当前使用的方法,文件传送时间:56‘’
零拷贝sendfile()
因为我这个工程是windwos+linux的,winddows没有提供零拷贝的系统函数,好像没法进行零拷贝。
所以零拷贝会单独写一个文章来学习Linux中的使用。
三、修改TCP缓冲区的大小
TCP的缓冲区会影响TCP的窗口大小,所以是会影响传输速度的。
关于缓冲区和窗口的关系,可以看这篇文章:
linux高性能网络编程之tcp连接的内存使用
可以通过代码setsockopt来修改单个FD的SO_ECVBUFF的大小,
也可以通过sysctl.conf来修改所有TCP连接的接受缓冲区大小。
查看允许的TCP接受缓冲区最大值
zlc@192:~/share$ sysctl net.core.rmem_max
net.core.rmem_max = 212992
查看当前的TCP缓冲区大小设置
zlc@192:~/share$ sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 1024 1024 2048
使用默认系统设置
此时的代码逻辑:
Client:每个循环从文件中读取64kb字节,然后send 64kb字节。
Server:每个循环为recv准备64kb的buff,把读取到的字节,全部写入文件。
Client部分:
int buffsize = 64 * 1024; //64KB
char* buff = new char[buffsize];
ifs.seekg(0);
while (!ifs.eof()&& remain_size > 0 && ifs.read(buff, remain_size > buffsize ? buffsize : remain_size))
{
int count = send(conn->conn_socket, buff, remain_size > buffsize ? buffsize : remain_size, 0);
if (count < 0)
{
if (WSAGetLastError() == EWOULDBLOCK || WSAGetLastError() == EAGAIN)
{
perror("send file try again");
return 0;
}
perror("send file");
char msg_ipv4[128] = { 0x00 };
inet_ntop(AF_INET, &conn->ipv4_addr.sin_addr, msg_ipv4, 128);
LOG(3, "disconnect", msg_ipv4, I2A(conn->ipv4_addr.sin_port));
FD_CLR(conn->conn_socket, fds);
closesocket(conn->conn_socket);
return -1;
}
else if (count == 0)
{
if (WSAGetLastError() != EINTR)
{
perror("trans_init send");
char msg_ipv4[128] = { 0x00 };
inet_ntop(AF_INET, &conn->ipv4_addr.sin_addr, msg_ipv4, 128);
LOG(3, "disconnect", msg_ipv4, I2A(conn->ipv4_addr.sin_port));
FD_CLR(conn->conn_socket, fds);
closesocket(conn->conn_socket);
return 0;
}
}
remain_size -= count;
sendsize += count;
//std::cerr << "remain_size:" << remain_size << std::endl;
}
Server部分:
int buffsize = 64*1024;
char* buff = new char[buffsize];
std::cout << std::endl;
int max_recv_size_every_loop = 0;
conns[event_fd]->ofs.seekg(0);
while (max_count < conns[event_fd]->filesize)
{
int count = recv(event_fd,buff, buffsize, 0);
if(count == 0 && errno != EINTR)
{
LOG(1, "recv disconnect.");
RestoreFDstat(event_fd);
epoll_clt_del(event_fd);
close(event_fd);
return -1;
}
else if(count == -1)
{
if(errno == EINTR)
continue;
else if(errno == EAGAIN || errno == EWOULDBLOCK)
{
LOG(1, "recv end.");
RestoreFDstat(event_fd);
return max_count;
}
perror("\nrecv error:");
RestoreFDstat(event_fd, msg_fail);
// epoll_clt_del(event_fd);
// close(event_fd);
return -1;
}
max_count += count;
conns[event_fd]->ofs.write(buff,count);
if(max_recv_size_every_loop < count)
{
max_recv_size_every_loop = count;
std::cout << "max_recv_size_in_loop is " << max_recv_size_every_loop << std::endl;
}
}
试一下,这样的情况,传输文件需要的时间。
Server处用getsockopt取得了SO_RCVBUFF的值,此时accept到的连接FD的recv缓冲区是2048字节。
传输完成,传输用时1’43’'。循环中,recv返回的最大字节数是512字节。
修改sysctl.conf
把net.ipv4.tcp_rmem大幅度调大
上面recv函数的返回值最大就到了512就不再增长了。
我认为是这个512太小了。
发现net.ipv4.tcp_rmem的默认值是1024,是不是跟他有关。
zlc@192:~/share$ sudo sysctl -a
net.ipv4.tcp_rmem = 1024 1024 2048
编辑/etc/sysctl.conf
然后执行sudo sysctl -p
把net.ipv4.tcp_rmem调整成:
zlc@192:~/share$ sudo sysctl -a
net.ipv4.tcp_rmem = 65535 65535 131070
执行结果:
WTF?竟然半小时多???
我又放了一个晚上,第二天早上再传一次(虚拟机时间不准确,显示18:00,其实已经第二天早上10:00了)。
TMD!!!又13‘’了。。。。。
因为我调整了TCP缓冲区的大小,扩大了不少,我认为13‘’这个才是预期的数值。传输时间是半小时,太离谱了。
我又重复了几遍,就不截图了,只记录执行传输的时间:
(1)15‘’
(2)14‘’
(3)12‘’
(4)重启windows+重启ubuntu虚拟机,2’39’’
(5)再次传输,2‘33‘’
目前很苦恼,这是为什么,出现这么大的起伏。
想起网上的各种攻略中,也说到缓冲区调的太大,不一定会对网络传输启动积极作用,反而会增加拥塞风险。
破案了
因为我用的是wifi,平时家里爸爸妈妈会连wifi看抖音…之前测试的十几秒的情况,都是早上或者晚上,爸妈休息的时候测出来的。爸妈一开始看抖音,我这边传输就会拥塞。
今天又趁着爸妈出门溜达的时间,测了几次,传输时间又回归20’'了。
把net.ipv4.tcp_rmem小幅度调大
net.ipv4.tcp_rmem的默认值是1024,给他翻倍。
编辑/etc/sysctl.conf
然后执行sudo sysctl -p
把net.ipv4.tcp_rmem调整成:
zlc@192:~/share$ sudo sysctl -a
net.ipv4.tcp_rmem = 2048 2048 4096
执行结果:56’‘。
重复几次:
(1)1’05’’
(2)1’03’’
(3)重启ubuntu,再次测试:1’05’’
跟上一个比,这个速度很稳定。
通过sersockopt设置FD的recv缓冲区大小
把net.ipv4.tcp_rmem
还原成1024 1024 2048
查看net.core.rmem_max
net.core.rmem_max = 212992
把SO_RCVBUFF设置成:4 * 1024。Accept部分添加以下代码:
//Check sock opt
//set recv buff
int recvbufflength = 4 * 1024;
len = sizeof(recvbufflength);
setsockopt(conn_fd, SOL_SOCKET, SO_RCVBUF, &recvbufflength,len ); // ←新增
recvbufflength = 0;
getsockopt(conn_fd, SOL_SOCKET, SO_RCVBUF, &recvbufflength,&len );
LOG(2, "SOCKET recv buff length:", I2A(recvbufflength));
执行传输。
用时2分半。(‘ 。’ 我家的网络无法承受这么大的缓冲区吗 )
再试一下Setsockopt是否真的生效了
调整tcp_rmem:
net.ipv4.tcp_rmem = 2048 2048 4096
调整代码:
//Check sock opt
//set recv buff
int recvbufflength = 512;
len = sizeof(recvbufflength);
setsockopt(conn_fd, SOL_SOCKET, SO_RCVBUF, &recvbufflength,len ); // ←新增
recvbufflength = 0;
getsockopt(conn_fd, SOL_SOCKET, SO_RCVBUF, &recvbufflength,&len );
LOG(2, "SOCKET recv buff length:", I2A(recvbufflength));
执行传输。
这里有点问题,如果不进行setsockopt,获取的SO_RCVBUFF是2048。设置了以后,获取的SO_RCVBUFF不是512 * 2,而是2304。
linux高性能网络编程之tcp连接的内存使用
这篇文章有以下解释:
当设置了SO_SNDBUF时,就相当于划定了所操作的TCP连接上的写缓存能够使用的最大内存。然而,这个值也不是可以由着进程随意设置的,它会受制于系统级的上下限,当它大于上面的系统配置wmem_max(net.core.wmem_max)时,将会被wmem_max替代(同样翻一倍);而当它特别小时,例如在2.6.18内核中设计的写缓存最小值为2K字节,此时也会被直接替代为2K。
我看见网上有人写,setsockopt要放到listen()之前,我试下来如果操作accept到的fd,放到accept之后也可以生效。
四、 TCP_NODELAY选项
此时ubuntu系统设置:
net.ipv4.tcp_rmem = 2048 2048 4096
不激活TCP_NODELAY的传输时间:45‘’
激活TCP_NODELAY,加在listen之前:
//set TCP_NODELAY
int flag = 1;
setsockopt(listen_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
56‘’,没有明显变化。
在accept之后也加一个,54‘’
在发送端,connect之前也加一个,55’'。
TCP_NODELAY会禁用Nagle算法(合并小包),减少延迟,但是看来在我这个需求中,没有明显作用。
五、 TCP_CORK选项
TCP_CORK选项用来关闭CORK算法。
Nagle和CORK算法的区别:
TCP_CORK配置选项和TCP_NODELAY Nagle算法的异同
按照“四”中的方法, 添加TCP_CORK选项(windows好像无法设置)。传输用时56’'。跟“四”比没有明显提升。
没有提升也可以理解,Nagle等算法的目的是减少网络中的小包,但是这里传输的是文件,不停的把文件buff拷贝到内核缓冲区,应该不会存在很多小包,所以本身也没有算法导致的延迟。
六、虚拟机改成NAT模式
由于不经过路由转发,传输时间几乎稳定在2’'。
VMWARE NAT设置:
虚拟机网络模式(NAT模式主机访问虚拟机)