2010-12-02 39 views
4

我对C相当陌生,并且正在编写一个TCP服务器,并且想知道如何处理将发送服务器响应的命令的客户端的recv()。为了这个问题,我们只是说头是第一个字节,命令标识符是第二个字节,有效载荷长度是第三个字节,后面是有效载荷(如果有的话)。处理多个recv()调用和所有可能的场景

recv()这个数据的最好方法是什么?我正在考虑调用recv()来读取缓冲区中的前3个字节,检查以确保头标和命令标识符有效,然后检查有效长度并以有效长度作为长度再次调用recv(),并将其添加到上述缓冲区的后面。然而,阅读Beej的网络文章(特别是本节的这一部分:http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html#sonofdataencap),他建议使用“一个足够大的数组来处理两个[最大长度]数据包”来处理诸如获取下一个数据包的情况。

什么是处理这些类型的recv()的最佳方式?基本的问题,但我想有效地实施它,处理所有可能出现的情况。提前致谢。

回答

5

是Beej被影射的方法,和AlastairG提到,工作是这样的:

对于每个并发连接,你保持阅读,但是,尚未处理的数据的缓冲区。 (这是Beej建议调整为最大数据包长度两倍的缓冲区)。显然,缓冲开始了空:

unsigned char recv_buffer[BUF_SIZE]; 
size_t recv_len = 0; 

每当你的插座是可读,读入缓冲区中的剩余空间,然后立即尝试过程中,你有什么:

result = recv(sock, recv_buffer + recv_len, BUF_SIZE - recv_len, 0); 

if (result > 0) { 
    recv_len += result; 
    process_buffer(recv_buffer, &recv_len); 
} 

process_buffer()尝试并将缓冲区中的数据作为数据包处理。如果缓冲区尚未包含完整的数据包,它只会返回 - 否则,它会处理数据并将其从缓冲区中删除。因此,对于您的示例协议,它看起来是这样的:

void process_buffer(unsigned char *buffer, size_t *len) 
{ 
    while (*len >= 3) { 
     /* We have at least 3 bytes, so we have the payload length */ 

     unsigned payload_len = buffer[2]; 

     if (*len < 3 + payload_len) { 
      /* Too short - haven't recieved whole payload yet */ 
      break; 
     } 

     /* OK - execute command */ 
     do_command(buffer[0], buffer[1], payload_len, &buffer[3]); 

     /* Now shuffle the remaining data in the buffer back to the start */ 
     *len -= 3 + payload_len; 
     if (*len > 0) 
      memmove(buffer, buffer + 3 + payload_len, *len); 
    } 
} 

(该do_command()功能会检查是否有一个有效的标题和指令字节)。

这种技术最终被必要的,因为任何recv()可以返回一个短 - 与你的建议的方法,如果你的有效载荷长度为500会发生什么,但接下来的recv()只返回你400个字节?无论如何,你必须保存这400个字节,直到下一次套接字变得可读。当你处理多个并发客户端时,每个客户端只需要一个recv_bufferrecv_len,并将它们填充到每个客户端结构中(这可能也包含其他内容 - 如客户端套接字,可能是它们的源地址,当前状态等等。)。

5

不错的问题。你想走多远?对于全部演唱的所有舞蹈解决方案,请使用异步套接字,只要可以,就可以读取所有可用的数据,并且每当您获取新数据时,都会调用缓冲区上的某些数据处理功能。

这可以让你做大读。如果你获得了大量的流水线命令,你可以处理它们而无需再次等待套接字,从而提高性能和响应时间。

在写上做类似的事情。这是命令处理函数写入缓冲区。如果缓冲区中有数据,那么当检查套接字(选择或轮询)时检查可写性并尽可能多地写入,记住只删除实际从缓冲区写入的字节。

在这种情况下,循环缓冲区运行良好。

有更轻松简单的解决方案。但是,这是一个很好的。请记住,服务器可能会获得多个连接,并且可能会拆分数据包。如果你从一个套接字读入一个缓冲区而只发现你没有完整命令的数据,那么你对你已经读过的数据做了什么?你在哪里存储它?如果您将它存储在与该连接相关的缓冲区中,那么您可能需要全部访问,并且首先按照上面所述读入缓冲区。

此解决方案还避免了必须为每个连接产生一个单独的线程 - 您可以处理任何数量的连接,而不会有任何实际问题。每个连接产生一个线程是系统资源的不必要的浪费 - 除非在某些情况下,无论如何推荐使用多个线程,并且您可以简单地让工作线程执行这些阻塞任务,同时保持套接字处理单线程。

基本上我同意你说的Beej说的,但不要一次只读一点点。一次读大块。编写一个这样的套接字服务器,就像我一起学习和设计一样,它基于一小部分的套接字体验和手册页,是我从事过的最有趣的项目之一,也是非常有教育意义的项目。

+0

谢谢AlastairG。我基本上试图保持它尽可能简单,只处理最常见的情况,如读入下一个数据包,或读取部分数据包。我实际上使用select来监视传入数据的客户端套接字(在这种情况下是命令),所以我不认为这是异步套接字的问题。我的方法是读取3个字节,然后有效载荷不起作用?我不想实现Beej所说的原因是因为如果没有实际的代码/伪代码就很难理解。感谢您的建议:) – Jack 2010-12-02 16:14:26

+0

对于处理传入数据后的写入/发送,我只是简单地使用Beej的sendall方法的变体,因为我知道数据包的长度..显示在这里:http:// beej页面没有自动跳转/导向/ bgnet /输出/ HTML /多页/ advanced.html#sendall。我更关心阅读。 – Jack 2010-12-02 16:15:52

+0

我对写作的评论只是为了完整 - 以防其他人想知道如何编写一个非常好的套接字服务器。我要说的一件事是,我已经尝试编写小而简单的套接字服务器,但你很快发现你只需要按照我描述的方式来完成它。但并非总是如此。读取3个字节可以工作,但为什么要这样做? – AlastairG 2010-12-02 16:33:14

2

Alastair所描述的解决方案在性能方面是最好的。仅供参考 - 异步编程也称为事件驱动编程。换句话说,您需要等待数据进入套接字,将其读入缓冲区,处理什么/什么时候可以,然后重复。您的应用程序可以在读取数据和处理数据之间做其他事情。

一对夫妇更多的链接,我发现有帮助做一些非常相似:

第二个是一个伟大的图书馆,以帮助实现这一切。

至于使用缓冲区和尽可能多的阅读,这是另一个表现的事情。批量读取更好,更少的系统调用(读取)。您处理的,当你决定你有足够的处理,但要确保只处理您的“包”中的一个缓冲的时间,而不是(你用3个字节的报头中所描述的)破坏其他数据缓冲区中的数据。

1

基本上有两个假设,如果你正在使用多个连接,然后同时处理多个连接的最佳方式(无论是监听套接字,readfd或writefd)是选择/投票/ epoll的。您可以根据您的要求使用其中任何一种。

关于你的第二个查询如何处理多个recv()这个练习可以使用: 每当数据到达时只是查看头部(它应该是固定的长度和格式,如你所描述的)。

buff_header = (char*) malloc(HEADER_LENGTH); 
    count = recv(sock_fd, buff_header, HEADER_LENGTH, MSG_PEEK); 
    /*MSG_PEEK if you want to use the header later other wise you can set it to zero 
     and read the buffer from queue and the logic for the code written below would 
     be changed accordingly*/ 

通过这个,你得到了你的头,你可以验证参数,也提取完整的味精长度。 得到充分信息长度后只收到完整的味精

msg_length=payload_length+HEADER_LENGTH; 
    buffer =(char*) malloc(msg_length); 
    while(msg_length) 
    { 
     count = recv(sock_fd, buffer, msg_length, 0); 
     buffer+=count; 
     msg_length-=count; 
    } 

所以在这种方式,你不必采取有一些固定长度的任何数组,可以轻松实现你的逻辑。