==========TCP 编程的相关问题==========

2021-11-11 14:03:22 +08:00
 FreeWong
对方通过单片机使用 TCP 客户端的形式(短连接)与我写的服务端进行通讯
他们设计的协议是基于文本的,类似以下格式

@Start:A01:25.41,A02:36.1#

其中#号表示一条完整的可解析的数据的结尾,同时 TCP 客户端要求当服务器收到一条完整的数据后,要回复 @Received 用于表示接收到这条数据。
并且客户端会等待我的这个回复,才会断开 TCP 客户端一侧的连接,十几秒都没有收到则主动断开。(我们不考虑它主动断开的情况)
如果服务端没有回复,则到下一个通讯时间节点时,再次将前面没有收到回复的数据补发,然后再接着发送当前时间节点应该发送的。
像这样
@Start:A01:25.41,A02:36.1#@Start:A01:26.18,A02:38.2#

很多编程语言提供类似 ReadTo() 方法,意思是这个方法不返回,一直读到指定的字符时返回,所以 ReadTo("#") 就会返回一条完整的数据。 但是由于存在补发数据的情况,所以得循环调用这个方法像这样
for{
string 完整的数据 =ReadTo("#")
开始处理完整的数据的函数(完整的数据)
}

你们发现没有,由于我根本不知道客户端会有多少个完整的数据的指示符,所以我的服务端必须要循环调用 ReadTo("#") 并且我根本不知道什么时候该退出循环。
也就无法向 TCP 客户端发送 @Received 让来客户端断开连接。
现在就变成了,我根本不知道何时结束循环也就无法发送 @Received,而客户端也在等待我给它指示断开的指令,这样面临一个类似“死锁”状态。

这里只讨论 ReadTo("#") 这个方法

所以我的看法就,TCP 客户端应该将多条补发数据的格式修改为

@Start:A01:25.41,A02:36.1;@Start:A01:26.18,A02:38.2#

将中间的 #号修改为 ; 号,这样的话, 我都不需要使用循环来读。
请问,我的想法有没有考虑不周全的地方?感谢大家指正。
1727 次点击
所在节点    问与答
24 条回复
ksc010
2021-11-11 14:45:32 +08:00
太长了,没细看
一般直接用 tcp 协议通讯的话,都设置自定义消息包格式
消息头+消息体
消息头长度固定,头里面包含消息体的长度,这样就知道读取多少位数据停止了
rrfeng
2021-11-11 14:49:01 +08:00
不要 ReadTo ,每次用 ReadAll 全读出来,然后另外拆分处理?
bfdh
2021-11-11 15:17:23 +08:00
如果对端支持 udp 的话,换 udp 吧,感觉 udp 这种协议设计。使用 tcp 确实存在你说的问题。
如果非得用 tcp ,又不愿意改协议,那就你的接收端加超时,一定时间内没有新的数据达到,就认为是一条消息结束。
bfdh
2021-11-11 15:18:41 +08:00
@bfdh #3 更正一下 感觉 udp 这种协议设计 ==> 感觉 udp 更适合这种协议设计
ysc3839
2021-11-11 16:35:41 +08:00
这个设计是有问题,看起来 Received 的作用只是控制是否补发,那客户端发完后就应该直接 shutdown ( https://man7.org/linux/man-pages/man2/shutdown.2.html ),这样服务端就能知道数据已经发完了,这个方案甚至不需要修改数据包格式。
ysc3839
2021-11-11 16:38:44 +08:00
@rrfeng 这里的问题是客户端发完数据后不会 shutdown ,ReadAll 要等客户端 shutdown 后才会返回。
momocraft
2021-11-11 16:42:02 +08:00
想办法避免无限阻塞的 API
ipwx
2021-11-11 16:42:42 +08:00
解决方案:

1 、每个客户端给一个 shutdownRequested 变量。
2 、自己开缓冲区处理 ReadTo('#') 的逻辑,用异步 read 。
3 、read 读不到就返回,那么进入 epoll 等待队列。
4 、如果 shutdownRequested ,就唤醒这个 fd 去处理。。

总结:

需要一个完整的 event loop 。但是有了就很容易做
FreeWong
2021-11-11 16:49:43 +08:00
@rrfeng 实际编程语言没有 ReadAll , 如果你的意思是 总是从缓冲区去读,每次读一批,然后接收缓冲区就有了位置,对端才可以继续发送数据过来。但是你如何判断你接收完了? 像这样

声明 512 长度的字节数组
for{
得到的数据= 读到 512 长度的字节数组()
如果( 得到的数据以用 # 号结尾则是一个完成的数据 )
}

但这是有问题的,如果你正好读的最后一个字符是两条数据中间的 # ,你会认为所有的数据都接收完了 ,实际上还有余下的一个完整的没有被接收下来, 如果你认为读到一个 # 不是所有数据都接收完了,那何时能判断哪个 # 号才表示接收完成?
FreeWong
2021-11-11 16:51:12 +08:00
@ksc010 TCP 客户端的实现是对方协议制定好的,我无法修改它,只是用技术角度来分析这个协议的缺陷
FreeWong
2021-11-11 16:52:27 +08:00
@bfdh 超时是个办法,但不是一个好办法,我可以定义 5 秒钟都收完,如果收不完,也就结束 了,但这种方式真的不好
FreeWong
2021-11-11 16:54:16 +08:00
@ysc3839 TCP 客户端不会主动断开的,它一定要待我回 @Received ,所以这就是我说的 “死锁” 双方都在等,我在等待判断如何才能全部收完,即循环结束 ,对方在等待我回复 @Received
FreeWong
2021-11-11 16:56:04 +08:00
@ipwx 这是一个很常见的网络编程问题,应该不会需要这么复杂去解决。。我只是认定对方协议有瑕疵
ysc3839
2021-11-11 16:58:08 +08:00
客户端改不了的情况下有一个可能可行的方案,就是服务端一收到数据就立即发送 Received ,然后 read all 直到断开连接,这个方案可行的前提是客户端会把所有数据发完再断开连接。假设客户端是发送数据、接收数据、断开连接顺序执行的话,这个方案是可行的。
FreeWong
2021-11-11 17:01:14 +08:00
@ysc3839 看看我的提问的倒数第四行,看看这个办法是否可行
ipwx
2021-11-11 17:01:57 +08:00
@FreeWong 不这就是很常见的解决方案。

只不过在比如 python 语言里面你可以用协程 asyncio ,在 node.js 里面可以用回调,把上面的逻辑实现是个很简单的事情。在 C++ 里面你就不得不用多线程或者 event loop 了。
ysc3839
2021-11-11 17:03:35 +08:00
@FreeWong 所以你到底能不能改客户端?能改的话那个方法是可行的。本质是用 # 代替 shutdown 来表示“数据已发完”。
FreeWong
2021-11-11 19:50:49 +08:00
@ipwx 有兴趣的也愿意的话,用 nodejs 写一个看看。。。
FreeWong
2021-11-11 19:51:25 +08:00
@ysc3839 单片机是由供应商提供的,如果方法可行可以让供应商下个版本修改
ysc3839
2021-11-11 20:26:57 +08:00
@FreeWong 那建议使用 shutdown 的方式,服务端读到连接关闭即可。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/814687

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX