计算套接字上传速度

6
我想知道有没有人知道如何计算C++中Berkeley socket的上传速度。我的send调用不会阻塞,并且需要0.001秒来发送5兆字节的数据,但是需要一段时间来recv响应(所以我知道它正在上传)。
这是一个TCP套接字到HTTP服务器,我需要异步检查已上传/剩余多少字节的数据。然而,在Winsock中我找不到任何API函数,所以我感到困惑。
非常感谢任何帮助。
编辑:我已经找到解决方案,并将尽快发布答案!
编辑2:适当的解决方案已添加为答案,将在4小时后添加为解决方案。
4个回答

8

感谢 bdolan 的建议,我解决了我的问题,他建议减小 SO_SNDBUF。但是,请注意,要使用此代码,您的代码必须使用 Winsock 2(用于重叠套接字和 WSASend)。此外,您的 SOCKET 句柄必须类似于以下方式创建:

SOCKET sock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);

请注意最后一个参数要加上WSA_FLAG_OVERLAPPED标志。
在本答案中,我将介绍将数据上传到TCP服务器并跟踪每个上传块及其完成状态的各个阶段。这个概念需要将上传缓冲区分成块(仅需要进行最少的现有代码修改),然后逐块上传,并跟踪每个块。

我的代码流程

全局变量

您的代码文档必须具有以下全局变量:
#define UPLOAD_CHUNK_SIZE 4096

int g_nUploadChunks = 0;
int g_nChunksCompleted = 0;
WSAOVERLAPPED *g_pSendOverlapped = NULL;
int g_nBytesSent = 0;
float g_flLastUploadTimeReset = 0.0f;

注意:在我的测试中,将 UPLOAD_CHUNK_SIZE 减小会增加上传速度的准确性,但会降低总体上传速度;将 UPLOAD_CHUNK_SIZE 增加会降低上传速度的准确性,但会提高总体上传速度。对于大小约为500KB的文件来说,4千字节(4096字节)是一个很好的折衷方案。

回调函数

这个函数会增加已发送的字节数和已完成的块数(当一个块完全上传到服务器后被调用)。

void CALLBACK SendCompletionCallback(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
    g_nChunksCompleted++;
    g_nBytesSent += cbTransferred;
}

准备套接字

首先,必须通过将SO_SNDBUF减少到0来准备套接字。

注意:在我的测试中,任何大于0的值都会导致不良行为。

int nSndBuf = 0;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&nSndBuf, sizeof(nSndBuf));

创建 WSAOVERLAPPED 数组

必须创建一个WSAOVERLAPPED结构的数组,以保存所有上传块的重叠状态。我只需执行以下操作:

// Calculate the amount of upload chunks we will have to create.
// nDataBytes is the size of data you wish to upload
g_nUploadChunks = ceil(nDataBytes / float(UPLOAD_CHUNK_SIZE));

// Overlapped array, should be delete'd after all uploads have completed
g_pSendOverlapped = new WSAOVERLAPPED[g_nUploadChunks];
memset(g_pSendOverlapped, 0, sizeof(WSAOVERLAPPED) * g_nUploadChunks);

上传数据

所有需要发送的数据,例如,都保存在名为pszData的变量中。然后,使用WSASend,将数据分块发送,每个块的大小由常量UPLOAD_CHUNK_SIZE定义。

WSABUF dataBuf;
DWORD dwBytesSent = 0;
int err;
int i, j;

for(i = 0, j = 0; i < nDataBytes; i += UPLOAD_CHUNK_SIZE, j++)
{
    int nTransferBytes = min(nDataBytes - i, UPLOAD_CHUNK_SIZE);

    dataBuf.buf = &pszData[i];
    dataBuf.len = nTransferBytes;

    // Now upload the data
    int rc = WSASend(sock, &dataBuf, 1, &dwBytesSent, 0, &g_pSendOverlapped[j], SendCompletionCallback);

    if ((rc == SOCKET_ERROR) && (WSA_IO_PENDING != (err = WSAGetLastError())))
    {
        fprintf(stderr, "WSASend failed: %d\n", err);
        exit(EXIT_FAILURE);
    }
}

等待游戏

现在我们可以在所有块上传时做任何我们想做的事情。

注意:调用WSASend的线程必须定期处于可警报状态,这样我们的“传输完成”回调(SendCompletionCallback)才能从APC(异步过程调用)列表中出队。

在我的代码中,我不断循环直到g_nUploadChunks == g_nChunksCompleted。这是为了显示最终用户的上传进度和速度(可以修改为显示估计完成时间、经过时间等)。

注意2:此代码使用Plat_FloatTime作为秒计数器,请将其替换为您的代码所使用的秒计时器(或进行相应调整)。

g_flLastUploadTimeReset = Plat_FloatTime();

// Clear the line on the screen with some default data
printf("(0 chunks of %d) Upload speed: ???? KiB/sec", g_nUploadChunks);

// Keep looping until ALL upload chunks have completed
while(g_nChunksCompleted < g_nUploadChunks)
{
    // Wait for 10ms so then we aren't repeatedly updating the screen
    SleepEx(10, TRUE);

    // Updata chunk count
    printf("\r(%d chunks of %d) ", g_nChunksCompleted, g_nUploadChunks);

    // Not enough time passed?
    if(g_flLastUploadTimeReset + 1 > Plat_FloatTime())
        continue;

    // Reset timer
    g_flLastUploadTimeReset = Plat_FloatTime();

    // Calculate how many kibibytes have been transmitted in the last second
    float flByteRate = g_nBytesSent/1024.0f;
    printf("Upload speed: %.2f KiB/sec", flByteRate);

    // Reset byte count
    g_nBytesSent = 0;
}

// Delete overlapped data (not used anymore)
delete [] g_pSendOverlapped;

// Note that the transfer has completed
Msg("\nTransfer completed successfully!\n");

结论

我真心希望这篇文章能够帮助到未来想要在TCP套接字上计算上传速度而不需要进行任何服务器端修改的人。虽然我不知道SO_SNDBUF = 0对性能有多大影响,但我相信一个套接字专家会指出这一点。


2
您可以通过从套接字写入的字节数中减去SO_SNDBUF套接字选项的值来获得接收和确认的数据量的下限。可以使用setsockopt调整此缓冲区大小,但在某些情况下,操作系统可能会选择比您指定的长度更小或更大的长度,因此您必须在设置后重新检查。
然而,要更精确,您必须让远程方通知您进度,因为winsock不公开API以检索当前待发送缓冲区中的数据量。
或者,您可以在UDP上实现自己的传输协议,但是为这样的协议实现速率控制可能会非常复杂。

我的唯一远程协议选项是HTTP,所以我只需要一种方法来检查Winsock实际发送到服务器的字节数。 - Saul
1
那么,我想你将不得不减小SNDBUF的大小,以便进行测量。请注意,这可能会对性能产生负面影响。 - bdonlan

1

由于您无法控制远程方,并且希望在代码中执行此操作,因此我建议采用非常简单的近似方法。我假设长时间运行的程序/连接。一次性上传将受到ARP、DNS查找、套接字缓冲区、TCP慢启动等的影响。

有两个计数器 - 等待上传队列中的字节数(OB),以及发送的字节数(SB):

  • 每次将块排队上传时,都将OB增加要发送的字节数,
  • send(2)返回的数字(模除-1情况)会减少OB并增加SB,
  • 定时器对OB和SB进行采样 - 存储、记录或计算运行平均值,
  • 计算每秒/每分钟/任何时刻的未完成字节数和已发送字节数。

网络堆栈进行缓冲,TCP进行重传和流量控制,但这并不重要。这两个计数器将告诉您应用程序生成数据的速率以及它将其推送到网络的速率。这不是查找真实链接速度的方法,而是保持有关应用程序表现良好的有用指标的方法。

如果数据生产速率低于网络输出速率,那么一切都很好。如果情况相反,网络无法跟上应用程序的速度,那么就有问题了——您需要更快的网络、更慢的应用程序或不同的设计。
对于一次性实验,只需定期拍摄netstat -sp tcp输出(或在Windows上是什么)并手动计算发送速率即可。
希望这可以帮助到您。

0
如果您的应用程序使用像
0001234DT
这样的数据包头,其中000123是单个数据包的长度,您可以考虑使用MSG_PEEK + recv()在实际读取recv()之前获取数据包的长度。
问题在于send()并不是您想象的那样 - 它被内核缓冲。
getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &flag, &sz));
fprintf(STDOUT, "%s: listener socket send buffer = %d\n", now(), flag);
sz=sizeof(int);
ERR_CHK(getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &flag, &sz));
fprintf(STDOUT, "%s: listener socket recv buffer = %d\n", now(), flag);

看看这些对你有什么帮助。

当您在非阻塞套接字上接收到数据时,通常不会有MB的数据停留在缓冲区中准备接收。我经历过的大部分情况是,每次接收套接字都有约1500字节的数据。由于您可能正在阻塞套接字上读取,因此需要一段时间才能完成recv()。

套接字缓冲区大小可能是套接字吞吐量的最佳预测器。setsockopt()允许您更改套接字缓冲区大小,但有一个限制。请注意:在像Solaris这样的许多操作系统中,这些缓冲区是共享的。通过过度调整这些设置,您可能会降低性能。

此外,我认为您没有测量您认为的内容。send()的真正效率是recv()端的吞吐量。而不是send()端。在我看来。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接