V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
FreeWong
V2EX  ›  问与答

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

  •  
  •   FreeWong · 2021-11-11 14:03:22 +08:00 · 1737 次点击
    这是一个创建于 1106 天前的主题,其中的信息可能已经有所发展或是发生改变。
    对方通过单片机使用 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#

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

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

    总结:

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

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

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

    只不过在比如 python 语言里面你可以用协程 asyncio ,在 node.js 里面可以用回调,把上面的逻辑实现是个很简单的事情。在 C++ 里面你就不得不用多线程或者 event loop 了。
    ysc3839
        17
    ysc3839  
       2021-11-11 17:03:35 +08:00
    @FreeWong 所以你到底能不能改客户端?能改的话那个方法是可行的。本质是用 # 代替 shutdown 来表示“数据已发完”。
    FreeWong
        18
    FreeWong  
    OP
       2021-11-11 19:50:49 +08:00
    @ipwx 有兴趣的也愿意的话,用 nodejs 写一个看看。。。
    FreeWong
        19
    FreeWong  
    OP
       2021-11-11 19:51:25 +08:00
    @ysc3839 单片机是由供应商提供的,如果方法可行可以让供应商下个版本修改
    ysc3839
        20
    ysc3839  
       2021-11-11 20:26:57 +08:00
    @FreeWong 那建议使用 shutdown 的方式,服务端读到连接关闭即可。
    Sricecake
        21
    Sricecake  
       2021-11-15 13:14:25 +08:00
    问题点在于 TCP 是流式的,你却把它当 HTTP 用。
    正常应该是每条数据应该有个 ID 你收到一个# 就回一条 Received(ID)
    你只负责一直循环 ReadTo 处理完一条就回一条 Received
    客户端判断所有消息都 Received 了再断开连接就行。
    FreeWong
        22
    FreeWong  
    OP
       2021-11-18 11:02:36 +08:00
    @Sricecake 客户端的现在的错误逻辑,就是我只要回一条接收到的消息, 它就断开了。。如果客户端自己还有很多数据要发,它照样断 。。。这本来就是在一个错误的逻辑上修补。
    客户端是硬件厂商做的,我无法修改
    Sricecake
        23
    Sricecake  
       2021-11-19 19:22:40 +08:00
    按 #19 的说法 方法可行的话让供应商下个版本修改,按正确逻辑改了就可以了吧。
    FreeWong
        24
    FreeWong  
    OP
       2021-11-23 08:50:00 +08:00
    我的问题是,我的方法是否可行,有没有逻辑上说不通的地方。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2731 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 15:23 · PVG 23:23 · LAX 07:23 · JFK 10:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.