只有一个 write() 调用通过套接字连接发送数据

Posted

技术标签:

【中文标题】只有一个 write() 调用通过套接字连接发送数据【英文标题】:Only one write() call sends data over socket connection 【发布时间】:2011-04-12 04:11:02 【问题描述】:

第一个***问题!我已经搜索过......我保证。我还没有找到任何答案来解决我的困境。我有一个……至少可以说是一个严重恶化的问题。长话短说,我正在为一款游戏开发基础架构,其中移动应用程序(android 应用程序和 ios 应用程序)使用套接字与服务器通信,将数据发送到数据库。后端服务器脚本(我称之为 BES 或后端服务器)有几千行代码。本质上,它有一个 main 方法接受到套接字的传入连接并将它们分叉,以及一个从套接字读取输入并确定如何处理它的方法。大多数代码位于从数据库发送和接收数据并将其发送回移动应用程序的方法中。除了我添加的最新方法外,它们都工作正常。此方法从数据库中抓取大量数据,将其编码为 JSON 对象,然后将其发送回移动应用程序,移动应用程序也会从 JSON 对象中对其进行解码并执行它需要执行的操作。我的问题是这个数据非常大,而且大多数时候不会在一次数据写入中通过套接字。因此,我在套接字中添加了一个额外的数据写入,通知应用程序它即将接收的 JSON 对象的大小。但是,在此写入发生后,下一次写入会向移动应用发送空数据。

奇怪的是,当我删除发送 JSON 对象大小的第一个写入时,JSON 对象的实际发送工作正常。它非常不可靠,我必须希望它一口气发送所有内容。更奇怪的是,当我将第二次写入发送的数据大小设置为一个巨大的数字时,iOS 应用程序会正确读取它,但它会将数据放在一个空数组的中间。

世界上到底发生了什么?非常感谢任何见解!下面只是我在服务器端的两个写命令的基本sn-p。

请记住,在此脚本中的其他任何地方,读取和写入都可以正常工作,但这是我执行 2 次背靠背写入操作的唯一地方。

服务器脚本位于使用 Berkeley 套接字的原生 C 语言的 Ubuntu 服务器上,而 iOS 使用称为 AsyncSocket 的包装类。

int n;
//outputMessage contains a string that tells the mobile app how long the next message
//(returnData) will be
n = write(sock, outputMessage, sizeof(outputMessage));
if(n < 0)
   //error handling is here
//returnData is a JSON encoded string (well, char[] to be exact, this is native-C)
n = write(sock, returnData, sizeof(returnData));
if(n < 0)
   //error handling is here

移动应用程序进行两次读取调用,得到outputMessage 就好了,但returnData 始终只是一堆空数据,除非我将sizeof(returnData) 覆盖为一个非常大的数字,在这种情况下,iOS将在一个空数据对象(确切地说是 NSData 对象)的中间接收数据。还需要注意的是,我在 iOS 端的 AsyncSocket 类中使用的方法读取数据的长度达到了它从第一次写入调用接收到的长度。因此,如果我告诉它读取,比如 10000 字节,它将创建一个该大小的 NSData 对象,并在从套接字读取时将其用作缓冲区。

非常感谢任何帮助。提前谢谢大家!

【问题讨论】:

你怎么知道 write 发送的东西是空的(相对于你的阅读应用程序有问题)?这听起来更像是不考虑 TCP 是面向流而不是面向数据包的常见问题。一个 write() 可能需要多次 read() 调用才能接收,反之亦然。无论实验室的情况如何,您都需要考虑到这一点。 我不确定最初的问题是什么,但已通过改用iovec 结构并使用writev 而不是write 来解决这个问题。你是对的,它实际上并没有写空的东西。 write 返回结果始终是正确的字节数。我什至可以让客户端看到它读取任何数据的唯一方法是使客户端将读取的数据放入的缓冲区巨大,并且不知何故,实际上在该缓冲区的中间,就是数据。这很奇怪。但现在我正在阅读它,直到它收到一个空终止符并将其作为iovec 通过套接字发送。 如果是这种情况,它只是在您的测试用例中被“修复”。总有一天,一些用户的连接速度会很慢,连接非常拥塞,或者您偶然会快速发送数据/读取数据缓慢,您(或更可能是毫无戒心的用户)将再次遇到同样的问题。仔细阅读格雷格的回复 @nos 他先写数据包的大小,我想这是为了他可以像 TCP 一样将流转换为数据包,所以他读取大小然后读取由大小指定的整个数据包.如果他这样做,那么客户端不应受到碎片的影响。他现在以原子方式写入大小,然后写入有效负载,因此交错写入不会破坏“流”。 【参考方案1】:

这非常不可靠,我不得不希望它一口气发送所有内容。

使用 TCP 成功编程的关键是在应用程序级别没有 TCP“数据包”或“数据块”的概念。应用程序只看到一个 stream 字节,没有边界。当您使用一些数据在发送端调用 write() 时,TCP 层可能会选择以它认为合适的任何方式对您的数据进行切片和切块,包括将多个块合并在一起。

您可能会写入两次 10 字节,然后读取 5 次然后读取 15 字节。或者您的接收器可能会同时看到 20 个字节。你不能做的只是“希望”你发送的一些字节块会以相同的块到达另一端。

在您的特定情况下可能发生的情况是两个背靠背写入合并为一个,而您的读取逻辑根本无法处理。

【讨论】:

同意,这听起来就像正在发生的事情。在客户端查看read()的返回值。【参考方案2】:

感谢所有反馈!我将每个人的答案都纳入了解决方案。我创建了一个使用writev 而不是writeiovec 结构写入套接字的方法。我在 iOS 端使用的包装类 AsyncSocket (这太棒了,顺便说一下......在这里查看 -->AsyncSocket Google Code Repo )处理接收 iovec 就好了,显然在幕后,因为它不需要我做任何额外的努力就可以正确读取所有数据。 AsyncSocket 类现在不会调用我的委托方法didReadData,直到它接收到iovec 结构中指定的所有数据。

再次感谢大家!这帮助很大。从字面上看,我在一夜之间得到了一个我已经反对了一周的问题的回复。我期待更多地参与 *** 社区!

解决方案示例代码:

//returnData is the JSON encoded string I am returning
//sock is my predefined socket descriptor
struct iovec iov[1];
int iovcnt = 0;
iov[0].iov_base = returnData;
iov[0].iov_len = strlen(returnData);
iovcnt = sizeof(iov) / sizeof(struct iovec);
n = writev(sock, iov, iovcnt)
if(n < 0)
   //error handling here
while(n < iovcnt)
   //rebuild iovec struct with remaining data from returnData (from position n to the end of the string)

【讨论】:

这不是我的意思,你需要在iovec中输入两个条目,第一个是returnData的大小,第二个实际上是returnData。在客户端上,您首先收到大小,然后读取,直到您读取了该大小指定的所有数据。这将 TCP 的类流性质转换为数据包。您也可以使用 htonl() 而不是字符串以网络字节顺序将第一个大小作为 4 字节整数发送。 嘿吉姆。我正在回顾旧的 *** 帖子,并意识到我从未完成过这个讨论。我也意识到我两年前写这篇文章时是多么的菜鸟,哈哈。从长远来看,我做了你的评论,我的iovec中有两个属性,第一个是大小,第二个是实际数据,客户端的逻辑继续从套接字读取数据,直到它拥有收到的达到指定的大小。再次感谢您的指导!【参考方案3】:

您应该真正定义一个函数write_complete,它将缓冲区完全写入套接字。检查write的返回值,它也可能是一个正数,但小于缓冲区的大小。在这种情况下,您需要再次write 缓冲区的剩余部分。

哦,使用sizeof 也容易出错。因此,在上面的write_complete 函数中,您应该打印给定的大小并将其与您期望的大小进行比较。

【讨论】:

【参考方案4】:

理想情况下,您希望在服务器上以原子方式写入标头(大小)和数据,如果有多个线程可以写入同一个套接字的任何机会,我也会使用 scatter/gather 调用 writev()同时,您可能希望在 write 调用中使用互斥锁。

writev() 还会在返回之前写入所有数据(如果您使用阻塞 I/O)。

在客户端上,您可能必须有一个状态机来读取缓冲区的长度,然后循环读取,直到接收到所有数据,因为大型缓冲区将被分割并以各种大小的块进入。

【讨论】:

我将服务器端代码更改为使用 writev 而不是 write,并将要写入的字符缓冲区放入 iovec 结构中,效果很好!我在 iOS 端使用的包装类 AsyncSocket 似乎可以很好地处理接收 iovec 结构,并且在接收到所有数据之前不会调用我的“didReadData”方法!这是一个巨大的帮助!谢谢你,谢谢你,谢谢你!!我现在是 *** 的坚定信徒 :) 除了 writev 之外,您还必须确保按照其他答案中的建议阅读客户端上的整个数据包,正如我在答案中指出的那样,它处于循环中,直到数据包的所有字节已阅读。 在下面查看我的附加评论,你没有按照我的意图做,只是使用 writev 而不是 write 不是重点。似乎有效的唯一原因是 writev 写入了所有数据,而 write 只会写入 TCP 堆栈中可以缓冲的数据。

以上是关于只有一个 write() 调用通过套接字连接发送数据的主要内容,如果未能解决你的问题,请参考以下文章

QSerialPort::write() - 只有签名值? (最多 127 个十进制数)?我需要发送 0xFF (255 dec)

PHP socket_write 失败仅在下次调用时返回错误

close()和shutdown()函数

当客户端在 write() 期间终止连接时,由对等套接字错误重置连接

如何关闭 write() 系统调用的缓冲?

httpd服务和apache