【问题标题】:bidirectional tcp socket problems in CC中的双向tcp套接字问题
【发布时间】:2012-07-24 13:00:40
【问题描述】:

我正在尝试在 C 中实现一个 TCP 客户端,它需要按如下方式工作:

  • 能够打开与给定服务器的连接
  • 能够通过已建立的连接向服务器发送任意数据,并且
  • 能够从服务器接收任意数据(将其视为对我的客户端发送到服务器的“问题”的响应)。

例如,客户端应该能够打开与任意 HTTP 服务器的连接,发送“HEAD”消息并打印来自 HTTP 服务器的响应。

(我的目标是为我日常工作使用的特定软件环境创建一个通用的“TCP 客户端插件”,该软件环境缺乏网络功能。我非常了解我的环境的 SDK,但我不知道对socket编程真的有很深的经验。)

目前,我有 2 个单独的线程用于发送和接收数据。接收线程的工作流程(只要用户设置了服务器地址和端口就自动启动)如下(这里我只提socket调用的主序列):

globalSocket = socket(); // Create socket and store it globally
bind(); // Bind the local port
connect(); // Connect to remote host & port
listen(); // Listen to the socket
while (isAlive) {
  select(... &readfds ...); // Check for ready reader descriptors
  accept(); // Accept the incoming connection
  recv(); // Receive data from server
}
close(); // End the connection

发送者线程很简单,它使用接收者线程创建的globalSocket来执行send()命令。

现在,我的问题是:我可以毫无问题地打开与远程服务器的连接。我也可以毫无问题地发送任意数据(我确认我发送的数据实际到达服务器端没有问题)。但是,我无法从服务器取回任何数据。经过一些测试,select 似乎永远不会返回正值。

我在我的代码中尝试了很多修改(比如将参数更改为 select,省略 listen 等),我在这一天至少阅读了 10 次 Beej 的指南,并尝试了我所做的每一个临时更改只是可以想象,但行为仍然相同。因此,在我开始用特定的代码摘录提出具体问题之前,我想知道我解决这个问题的方法是否正确,或者我在这里是否遇到了一些严重的概念问题。

非常感谢您的回答,

阿达姆

附:我无法在 cmets 中发布此代码部分,因为它太长了;这是管理select-accept-recv循环的代码sn-p:

while ( thread->isActive ) {
   // Accept connection
   timeVal.tv_sec = TIMEOUT_SEC;
   timeVal.tv_usec = TIMEOUT_USEC;
   FD_ZERO( & fileDescriptor );
   FD_SET( socketDescriptor, & fileDescriptor ); // socketDescriptor is the global socket
   result = select ( FD_SETSIZE, & fileDescriptor, NULL, NULL, & timeVal );
   if ( result > 0 ) {
      post ( "select" ); // This line is actually never reached
      connectionDescriptor = accept ( socketDescriptor, ( struct sockaddr * ) & clientAddress, & clientLength ); // connectionDescriptor is the local socket created by accept
      if ( connectionDescriptor < 0 ) { // Some error happened
         outlet_int ( thread->parent->statusOutlet, errno );
      } else {
         // Receive data
         data.clear ( ); // 'data' is an std::vector of chars that stores the incoming data and makes it accessible for the rest of the environment
         size = thread->parent->bufferSize;
         buffer = new unsigned char [ size ]; // this buffer is used for receiving the data from 'recv'
         receivedBytes = 1;
         while ( receivedBytes > 0 ) {
            receivedBytes = recv ( connectionDescriptor, ( char * ) buffer, size, 0 );
            if ( receivedBytes < 0 ) { // Socket error
               outlet_int ( thread->parent->statusOutlet, errno );
            }
            data.insert ( data.end ( ), buffer, buffer + receivedBytes );
         }
         delete [ ] buffer;
#ifdef WIN_VERSION
         closesocket ( connectionDescriptor );
#else
         close ( connectionDescriptor );
#endif
         // Output received data
         ... blah ... blah ... blah
      }
   }
}

【问题讨论】:

  • 总体思路似乎是正确的。您绝对不应该做的一件事是在同一个套接字上调用listenconnect。这是一个或另一个。侦听传入连接或建立传出连接。 bind 对于传出连接也不是必需的,除非您想使用与内核默认选择的 IP 地址不同的 IP 地址。
  • @AlanCurry 谢谢,好吧,我也很不确定listen 的必要性。对于bind,我有点困惑,因为Beej 的指南指出它不是必需的,而许多人在这里和那里都告诉我最好拥有它。无论如何,我已经尝试删除那些代码部分,但仍然没有成功......
  • 您将不得不显示更多代码。尤其是 selectfd_set 初始化。
  • @AlanCurry 谢谢。以下是select 循环的摘录:
  • acceptlisten 一起使用 - 你不应该在执行 connect 的套接字上使用它。

标签: c sockets tcp client bidirectional


【解决方案1】:

首先,您是否在物理上接收来自远程服务器的数据包?我问是因为我在分配期间遇到了类似的问题,当我运行 tcpdump 时,它也没有显示任何接收到的数据包。问题原来是防火墙,它显然允许传出流量但阻止所有传入数据包...

【讨论】:

  • 嗯,好点。我实际上并没有检查。作为现实生活中的测试,我正在做的是,我在端口 80 上打开与 www.google.com 的连接并发送“HEAD”消息。当我使用 telnet 执行此操作时,我确实得到了答案。但是,我的代码没有得到...但是我会用 Wireshark 或一些类似的工具检查一下...
  • 嗯,如果您在客户端上打开连接并且服务器对此进行了回复,那么防火墙应该不是问题。我假设您也可以控制服务器,这在我的情况下是正确的。未经请求的传入连接请求确实会被防火墙丢弃:-) 无论如何,wireshark 测试不会造成任何伤害。您可以通过与相应的 telnet 传出数据包进行逐字节比较来测试传出请求是否格式正确。万事如意!
【解决方案2】:

其他人已经评论了connect(2)listen(2) 的不兼容性。你也不需要bind(2)

使用select(2) 的最常见错误是没有在每次迭代时重新初始化文件描述符集select(2) 的第二个到第四个参数是输入-输出,所以你必须每次都重做。

编辑0:

在您发布您的代码之后,让我补充一点,您并没有从 select(2) 提供的 I/O 解复用中受益。在通过新的连接尝试从select(2) 唤醒和客户端在您进入阻塞accept(2) 之前断开该连接之间也存在众所周知的竞争。

如果您不热衷于走非阻塞路线(使用select(2)poll(2)epoll(7)kqueue(2) 等的首选方式),您不妨摆脱@ 987654337@ 并且只接受和处理循环中的客户端连接。

【讨论】:

  • 感谢您的评论。其实我对这些也有点困惑。关于重新初始化,我每次都在调用select之前这样做。实际上,我什至正在重新初始化超时值,因为我在某处读到 select 也可能会改变某些平台上的那些...
  • 这种烦恼是poll() 不那么令人头疼的原因之一。那,以及更好的 fd 轮询粒度。话虽如此,select() 对微睡眠非常有用。
  • 关于Edit 0:我实际上不知道的是,如果不使用select,我怎么能从我的应用程序的主线程中中断侦听器线程?据我了解,recv 会阻塞线程,并且当不再需要该功能时(例如,如果用户想要与实际主机断开连接,则无法(强制线程终止除外)停止它)并连接到另一个)。有什么我忽略的吗?
  • 好的,我想我明白你为什么这么说,因为我在原来的循环中有另一个 while 循环,它无论如何都会阻塞线程......
【解决方案3】:

所以,这里有几个独立的问题,分为几个答案和cmets,所以我来总结一下。

发送者线程正常。但是,连接器/侦听器线程应如下所示:

socket();
connect();
while (isAlive) {
   if (select(... &readfds ...) > 0) {
      recv();
   }
}
close();

关于最初发布的代码摘录,用于处理 recvwhile 循环是一个概念错误,应该删除,因为它会阻塞线程 - 这将使 select 语句不必要(因为在那种情况下,select 将没有实际效果)。

还应该指出,如果recv 返回 0,则意味着套接字已被远程对等方关闭,因此在这种情况下,即使 isAlive 未设置为,while (isAlive) 循环也应中止父线程为假。但是,循环内不需要额外的close 语句(这也是原始代码中的概念错误)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-12-01
    • 2011-06-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多