从套接字缓冲数据?

4
我正在尝试制作一个简单的HTTP服务器,能够解析客户端请求并发送响应。

现在我遇到了一个问题。在请求中,我必须逐行读取和处理,但我不知道应该:

  • 每次读取一个字节,还是
  • 每次读取N个字节的块,将它们放入缓冲区中,然后逐个处理字节,再读取新的字节块。

什么是最佳选择,为什么

此外,是否有其他解决方案?比如从套接字中一次读取一行的函数等?

6个回答

5

一次只处理一个字节会影响性能。考虑使用适当大小的循环缓冲区。

读取缓冲区中任意大小可用的数据块。大多数情况下,您会得到短读取。检查读取字节中的http命令结束标记。处理完整的命令,下一个字节成为缓冲区头部。如果缓冲区已满,请将其复制到备份缓冲区,使用第二个循环缓冲区,报告错误或采取其他适当措施。


请问您能否解释一下“循环缓冲区”这个概念? - mathieug
@Math - 它基本上是一个将末尾包装到开头的数组。指针跟踪头、尾、当前位置等。请参见http://en.wikipedia.org/wiki/Circular_buffer。 - Duck

4
回答你的问题,我建议一次读取一个字节。不幸的是,在这种情况下,两种方法都有优缺点。
使用缓冲区的好处在于,从网络IO的角度来看,实现更加高效。但使用缓冲区的缺点是,代码本身会比单个字节版本更加复杂。因此,这是一个效率与复杂性之间的权衡。好消息是,您可以先实现简单的解决方案,测试结果并进行“升级”,如果测试显示值得,就采用缓冲区方法。
另外,只是作为一个“思想实验”,我编写了一些基于缓冲区读取http数据包的伪代码,如下所示。实现缓冲区读取的复杂性似乎并不太糟糕。但请注意,我没有考虑过多的错误处理,也没有测试过这是否有效。但它应该避免过多地“重复处理”数据,这很重要,因为这将减少这种方法的效率收益。
#define CHUNK_SIZE 1024

nextHttpBytesRead = 0;
nextHttp = NULL;
while (1)
{
  size_t httpBytesRead = nextHttpBytesRead;
  size_t thisHttpSize;
  char *http = nextHttp;
  char *temp;
  char *httpTerminator;

  do
  {
    temp = realloc(httpBytesRead + CHUNK_SIZE);
    if (NULL == temp)
      ...
    http = temp;

    httpBytesRead += read(httpSocket, http + httpBytesRead, CHUNK_SIZE);
    httpTerminator = strstr(http, "\r\n\r\n");
  }while (NULL == httpTerminator)

  thisHttpSize = ((int)httpTerminator - (int)http + 4; // Include terminator
  nextHttpBytesRead = httpBytesRead - thisHttpSize;

  // Adding CHUNK_SIZE here means that the first realloc won't have to do any work
  nextHttp = malloc(nextHttpBytesRead + CHUNK_SIZE);
  memcpy(nextHttp,  http + thisHttpSize, nextHttpSize);

  http[thisHttpSize] = '\0';
  processHttp(http);
}

1
TCP数据流以一个IP数据包的形式逐个到来,取决于IP层配置,每个数据包可达1,500字节左右。在Linux中,这将等待一个SKB在队列中等待应用层读取。如果您一次只读取一个字节,那么您将承受应用程序与内核之间上下文切换的开销,只为简单地从一个结构复制一个字节到另一个结构。最佳解决方案是使用非阻塞IO一次读取一个SKB的内容,从而最小化开销。
如果您寻求最佳带宽,您可以一次读取更长的字节数以进一步减少上下文切换的开销,但代价是延迟会更高,因为更多时间将花费在应用程序外部执行内存复制操作。但这仅适用于极端情况,并且只有在必要时才应实现此类代码更改。
如果您检查现有的众多HTTP技术,您会发现可以使用替代方法,例如使用多线程和阻塞套接字,将更多工作推回到内核中以减少进入应用程序并返回的开销。

我已经实现了一个HTTP服务器库,非常类似于torak在这里的伪代码,http://code.google.com/p/openpgm/source/browse/trunk/openpgm/pgm/http.c。这个实现的最大速度提升来自于使所有东西都异步化,所以没有任何阻塞。


1

Indy,例如,采用缓冲区方法。当代码要求Indy读取一行时,它首先检查当前缓冲区是否存在换行符。如果没有,网络将被分块读取并附加到缓冲区,直到出现换行符为止。一旦出现,仅将数据截取到换行符从缓冲区中删除并返回给应用程序,使得剩余数据留在缓冲区中供下一次读取操作使用。这样可以在提供高效的网络I/O(读取套接字中当前可用的所有内容,缓冲它,并确认发送方不需要等待太长时间-这种方式需要较少的网络级别读取)的同时,保持简单的应用程序级别API(ReadLine、ReadStream等)之间取得良好平衡。


0

从每次读取一个字节开始(尽管在HTTP中,需要注意行以cr / lf结尾),因为这很简单。如果这不足够,请执行更复杂的操作。


但是要准备好处理单个CR和单个LF作为行尾,因为有许多客户端和服务器并不真正符合HTTP标准... - Laurent Parenteau

0

每次读取一个字节数组缓冲区。由于用户和内核模式之间的多个上下文切换(实际上取决于libc),逐个字符读取将非常慢。

如果您读取缓冲区,需要准备好缓冲区可能未完全填充(请注意长度返回),缓冲区不包含足够的字节以行结束或缓冲区包含多个行的情况。

在网络应用程序中,将您的行或固定大小块请求映射到该变量流的缓冲区是一种常见模式(通常实现错误,例如可能出现0字节长度的答案)。高级语言将隐藏此复杂性。


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