Java - 网络编程 - 最佳实践 - 混合同步/异步命令

6
我正在开发一个Java小型客户端-服务器程序。客户端和服务器通过一个tcp连接相连。大多数通信部分是异步的(可以在任何时候发生),但我希望某些部分是同步的(例如发送命令的ACK)。我使用一个线程从套接字的InputStream中读取命令,并引发onCommand()事件。命令本身由Command-Design-Pattern处理。在Java中,最佳实践方法是什么,以使等待ACK而不错过可能同时出现的其他命令?
con.sendPacket(new Packet("ABC"));
// wait for ABC_ACK

编辑1

可以将其类比为FTP连接,但数据和控制命令都在同一连接上。我想捕获对控制命令的响应,同时后台正在运行数据流。

编辑2

所有内容都以块的形式发送,以便在同一TCP连接上进行多个(不同的)传输(复用)。

Block:
1 byte - block's type
2 byte - block's payload length
n byte - block's paylod

你发送了数据包后,希望该方法会阻塞直到接收到 ACK 数据包? - khellang
你想发送“ABC”并等待确认,但也希望能够发送“EFG”,而不必因为ABC_ACK而延迟吗? - Cratylus
是的,我有控制命令(例如ABC)和数据流在同一个连接上。读取线程同时读取它们。因此,我的问题是如何从读取线程“传递”ABC_ACK到发送ABC的(阻塞)方法。 - kazu
你不需要为TCP命令发送ACK。它会自动为你处理。你为什么还要搞清楚确认呢? - Chris Dennett
对我来说,这更像是多路复用而不是异步。在连接上同时传输2个数据流可能需要以块的形式发送数据,每个响应块都要用请求ID进行标识。就像TCP/IP一样 :) - extraneon
我需要ACK,因为命令在另一侧可能会失败(错误的参数,已经在使用中,...)。我正在通过TCP流发送命令包。每个“数据包”都以1字节描述其类型(数据,控制,...)开头,接着是2个字节的长度,然后是数据。 - kazu
2个回答

4
原则上,您需要一个被阻塞的线程(或更好的是,它们等待的锁)的注册表,这些线程由远程端发送的某个标识符作为键。对于异步操作,您只需发送消息并继续执行。对于同步操作,在发送消息后,您的发送线程(或启动此线程的线程)创建一个锁定对象,将其与某些键一起添加到注册表中,然后等待该锁定,直到得到通知。当读取线程收到某个答案时,它会在注册表中查找锁对象,将答案添加到其中,并调用 notify()。然后它会去读取下一个输入。这里的难点在于适当的同步以避免死锁,以及错过通知(因为它在我们加入注册表之前返回)。我在实现我们Fencing-applet的远程方法调用协议时做了类似的事情。原则上,RMI的工作方式相同,只是没有异步消息。

那么我会这样做:reader.waitForCmd("ABC_ACK"),它在内部将某些内容添加到列表中,等待并在通过读取线程接收到ABC_ACK时得到通知(恢复)? - kazu
是的,有人会这样做。我会使用映射而不是列表(即使有很多线程在等待,也可以高效地检索正确的锁)。 - Paŭlo Ebermann
一个澄清的问题:我是否正确地假设,我启动一个“new Thread()”(写入线程)只是为了发送同步请求?如果是这样,我如何在发送线程中获取响应?据我所知,我甚至不等待响应,在那里,但锁对象(我假设是响应对象类型?)通过注册表中的读取线程引用而简单地“填充”了? - cobby
进一步说:
  1. 这不需要向下转型吗?例如,如果我选择将注册表设置为 Map<Object, long> locksAndRequestIdsMap;,那么我要么需要维护无数个不同的注册表(类型),要么总是在被通知(恢复)的发送线程中向下转换 Object
  2. 我什么时候从注册表中删除锁定对象/响应对象?
如果我完全弄错了什么,您手头有例子吗?
- cobby
@cobby,据我理解,“同步”意味着您不希望在执行此操作的线程中继续执行,直到收到答案。因此,无需创建新线程,因为您已经在一个线程中了。 锁对象可以是普通对象(例如new Object()),我们可以使用.wait().notify(),或者我们可以使用来自java.util.concurrent.locks的专用锁对象。注册表将是例如Map<Long, Object>,从请求ID映射到相应的锁定。 - Paŭlo Ebermann
显示剩余2条评论

1

@Paulo的解决方案是我之前使用过的一个。然而,可能会有更简单的解决方案。

假设您没有一个后台线程用于读取连接中的结果。此时,您可以选择使用当前线程来读取任何结果。

// Asynchronous call
conn.sendMessage("Async-request");
// server sends no reply.

// Synchronous call.
conn.sendMessage("Sync-request");
String reply = conn.readMessage();

是的,我之前用过你的方法,但这只适用于我知道下一个数据包会出现的情况。如果有后台数据流,下一个数据包可能是数据包而不是我等待的ACK。这就是我的问题所在。后台线程主要处理数据包,因为这很容易且不需要同步。 - kazu
如果您有任何后台数据流,请使用单独的连接。另一种选择是让readMessage等待直到它读取到匹配的requestId。您也可以在发送消息时检查是否有数据要读取。这仅在定期检查流并且不使用阻塞IO时才有用。 - Peter Lawrey
@PeterLawrey,既然您提到了Paulo的回答(并且似乎以类似的方式做到了这一点),我也想知道您对我的评论有何看法 :) - cobby
@PeterLawrey 每个客户端创建两个套接字(一个用于后台数据流,另一个用于同步调用)是行不通的。服务器如何知道下一个两个套接字连接属于同一客户端而不是两个不同的客户端? - cobby
@cobby 客户端可以在连接时发送 UUID 或登录信息。如果客户端失去连接并重新连接,这可能对跟踪客户端很有用。 - Peter Lawrey
@cobby 这只是一个建议,具体情况取决于使用情况,这可能会使事情变得更容易或更复杂。 - Peter Lawrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接