我不是一个非常有经验的Java程序员,所以请原谅我,如果这是一个新手问题。
我正在设计一个大致由3个模块组成的系统。客户端,服务器和应用程序。想法是客户端向服务器发送消息。服务器在应用程序中触发用例。用例的结果返回给服务器,服务器将结果发送给客户端。我选择了这种架构,因为我希望能够同时支持多个客户端,我希望能够在其他应用程序中重用服务器模块,我希望保持代码负责管理客户端连接,因为它与尽可能实现域逻辑的代码,并且因为有机会学习一些更高级的java。
我计划将各种模块与队列绑定在一起。客户很简单。发出消息并阻止响应到达(这可能过于简单,但它现在是一个合理的模型)。该应用程序同样不是问题。它阻塞其输入队列,在收到有效消息时执行用例并将结果推送到输出队列。拥有多个客户端会让事情变得更棘手,但仍然掌握着我的经验水平。服务器为每个打开的连接维护线程,并且服务器出站/应用程序入站队列是同步的,因此如果两个消息一次到达,则第二个线程只需要等待片刻,以便第一个线程将其有效负载传递到队列中。
问题是服务器中间的部分,它必须阻止两个独立的事情。服务器正在观察客户端和应用程序的输出队列(用作服务器的输入队列)。服务器需要阻塞,直到消息从客户端进入(然后转发到应用程序),或者直到应用程序完成任务并将结果推送到应用程序出站/服务器入站队列。
据我所知,Java只能阻止一件事。
是否可以让服务器阻塞,直到客户端发送消息或服务器入站队列不再为空?
更新:
我已经花了一些时间来处理这个问题,并设法将问题缩小到说明问题的最低限度。即使是修剪,也会有一个有点笨重的代码转储,所以道歉。我会尝试尽可能地分解它。
这是服务器的代码:
public class Server implements Runnable {
private int listenPort = 0;
private ServerSocket serverSocket = null;
private BlockingQueue<Message> upstreamMessaes = null;
private BlockingQueue<Message> downstreamMessages = null;
private Map<Integer, Session> sessions = new ConcurrentHashMap ();
private AtomicInteger lastId = new AtomicInteger ();
/**
* Start listening for clients to process
*
* @throws IOException
*/
@Override
public void run () {
int newSessionId;
Session newSession;
Thread newThread;
System.out.println (this.getClass () + " running");
// Client listen loop
while (true) {
newSessionId = this.lastId.incrementAndGet ();
try {
newSession = new Session (this, newSessionId);
newThread = new Thread (newSession);
newThread.setName ("ServerSession_" + newSessionId);
this.sessions.put (newSessionId, newSession);
newThread.start ();
} catch (IOException ex) {
Logger.getLogger (Server.class.getName ()).log (Level.SEVERE, null, ex);
}
}
}
/**
* Accept a connection from a new client
*
* @return The accepted Socket
* @throws IOException
*/
public Socket accept () throws IOException {
return this.getSocket().accept ();
}
/**
* Delete the specified Session
*
* @param sessionId ID of the Session to remove
*/
public void deleteSession (int sessionId) {
this.sessions.remove (sessionId);
}
/**
* Forward an incoming message from the Client to the application
*
* @param msg
* @return
* @throws InterruptedException
*/
public Server messageFromClient (Message msg) throws InterruptedException {
this.upstreamMessaes.put (msg);
return this;
}
/**
* Set the port to listen to
*
* We can only use ports in the range 1024-65535 (ports below 1024 are
* reserved for common protocols such as HTTP and ports above 65535 don't
* exist)
*
* @param listenPort
* @return Returns itself so further methods can be called
* @throws IllegalArgumentException
*/
public final Server setListenPort (int listenPort) throws IllegalArgumentException {
if ((listenPort > 1023) && (listenPort <= 65535)) {
this.listenPort = listenPort;
} else {
throw new IllegalArgumentException ("Port number " + listenPort + " not valid");
}
return this;
}
/**
* Get the server socket, initialize it if it isn't already started.
*
* @return The object's ServerSocket
* @throws IOException
*/
private ServerSocket getSocket () throws IOException {
if (null == this.serverSocket) {
this.serverSocket = new ServerSocket (this.listenPort);
}
return this.serverSocket;
}
/**
* Instantiate the server
*
* @param listenPort
* @throws IllegalArgumentException
*/
public Server ( int listenPort,
BlockingQueue<Message> incomingMessages,
BlockingQueue<Message> outgoingMessages) throws IllegalArgumentException {
this.setListenPort (listenPort);
this.upstreamMessaes = incomingMessages;
this.downstreamMessages = outgoingMessages;
System.out.println (this.getClass () + " created");
System.out.println ("Listening on port " + listenPort);
}
}
我相信以下方法属于Server,但目前已被注释掉。
/**
* Notify a Session of a message for it
*
* @param sessionMessage
*/
public void notifySession () throws InterruptedException, IOException {
Message sessionMessage = this.downstreamMessages.take ();
Session targetSession = this.sessions.get (sessionMessage.getClientID ());
targetSession.waitForServer (sessionMessage);
}
这是我的Session类
public class Session implements Runnable {
private Socket clientSocket = null;
private OutputStreamWriter streamWriter = null;
private StringBuffer outputBuffer = null;
private Server server = null;
private int sessionId = 0;
/**
* Session main loop
*/
@Override
public void run () {
StringBuffer inputBuffer = new StringBuffer ();
BufferedReader inReader;
try {
// Connect message
this.sendMessageToClient ("Hello, you are client " + this.getId ());
inReader = new BufferedReader (new InputStreamReader (this.clientSocket.getInputStream (), "UTF8"));
do {
// Parse whatever was in the input buffer
inputBuffer.delete (0, inputBuffer.length ());
inputBuffer.append (inReader.readLine ());
System.out.println ("Input message was: " + inputBuffer);
this.server.messageFromClient (new Message (this.sessionId, inputBuffer.toString ()));
} while (!"QUIT".equals (inputBuffer.toString ()));
// Disconnect message
this.sendMessageToClient ("Goodbye, client " + this.getId ());
} catch (IOException | InterruptedException e) {
Logger.getLogger (Session.class.getName ()).log (Level.SEVERE, null, e);
} finally {
this.terminate ();
this.server.deleteSession (this.getId ());
}
}
/**
*
* @param msg
* @return
* @throws IOException
*/
public Session waitForServer (Message msg) throws IOException {
// Generate a response for the input
String output = this.buildResponse (msg.getPayload ()).toString ();
System.out.println ("Output message will be: " + output);
// Output to client
this.sendMessageToClient (output);
return this;
}
/**
*
* @param request
* @return
*/
private StringBuffer buildResponse (CharSequence request) {
StringBuffer ob = this.outputBuffer;
ob.delete (0, this.outputBuffer.length ());
ob.append ("Server repsonded at ")
.append (new java.util.Date ().toString () )
.append (" (You said '" )
.append (request)
.append ("')");
return this.outputBuffer;
}
/**
* Send the given message to the client
*
* @param message
* @throws IOException
*/
private void sendMessageToClient (CharSequence message) throws IOException {
// Output to client
OutputStreamWriter osw = this.getStreamWriter ();
osw.write ((String) message);
osw.write ("\r\n");
osw.flush ();
}
/**
* Get an output stream writer, initialize it if it's not active
*
* @return A configured OutputStreamWriter object
* @throws IOException
*/
private OutputStreamWriter getStreamWriter () throws IOException {
if (null == this.streamWriter) {
BufferedOutputStream os = new BufferedOutputStream (this.clientSocket.getOutputStream ());
this.streamWriter = new OutputStreamWriter (os, "UTF8");
}
return this.streamWriter;
}
/**
* Terminate the client connection
*/
private void terminate () {
try {
this.streamWriter = null;
this.clientSocket.close ();
} catch (IOException e) {
Logger.getLogger (Session.class.getName ()).log (Level.SEVERE, null, e);
}
}
/**
* Get this Session's ID
*
* @return The ID of this session
*/
public int getId () {
return this.sessionId;
}
/**
* Session constructor
*
* @param owner The Server object that owns this session
* @param sessionId The unique ID this session will be given
* @throws IOException
*/
public Session (Server owner, int sessionId) throws IOException {
System.out.println ("Class " + this.getClass () + " created");
this.server = owner;
this.sessionId = sessionId;
this.clientSocket = this.server.accept ();
System.out.println ("Session ID is " + this.sessionId);
}
}
测试应用程序非常基础,它只是回复原始请求消息的修改版本。真正的应用程序将在接收消息并向服务器返回有意义的响应时起作用。
public class TestApp implements Runnable {
private BlockingQueue <Message> inputMessages, outputMessages;
@Override
public void run () {
Message lastMessage;
StringBuilder returnMessage = new StringBuilder ();
while (true) {
try {
lastMessage = this.inputMessages.take ();
// Construct a response
returnMessage.delete (0, returnMessage.length ());
returnMessage.append ("Server repsonded at ")
.append (new java.util.Date ().toString () )
.append (" (You said '" )
.append (lastMessage.getPayload ())
.append ("')");
// Pretend we're doing some work that takes a while
Thread.sleep (1000);
this.outputMessages.put (new Message (lastMessage.getClientID (), lastMessage.toString ()));
} catch (InterruptedException ex) {
Logger.getLogger (TestApp.class.getName ()).log (Level.SEVERE, null, ex);
}
}
}
/**
* Initialize the application
*
* @param inputMessages Where input messages come from
* @param outputMessages Where output messages go to
*/
public TestApp (BlockingQueue<Message> inputMessages, BlockingQueue<Message> outputMessages) {
this.inputMessages = inputMessages;
this.outputMessages = outputMessages;
System.out.println (this.getClass () + " created");
}
}
Message类非常简单,只包含一个原始客户端ID和一个有效负载字符串,因此我将其删除了。
最后,主类看起来像这样。
public class Runner {
/**
*
* @param args The first argument is the port to listen on.
* @throws Exception
*/
public static void main (String[] args) throws Exception {
BlockingQueue<Message> clientBuffer = new LinkedBlockingQueue ();
BlockingQueue<Message> appBuffer = new LinkedBlockingQueue ();
TestApp appInstance = new TestApp (clientBuffer, appBuffer);
Server serverInstance = new Server (Integer.parseInt (args [0]), clientBuffer, appBuffer);
Thread appThread = new Thread (appInstance);
Thread serverThread = new Thread (serverInstance);
appThread.setName("Application");
serverThread.setName ("Server");
appThread.start ();
serverThread.start ();
appThread.join ();
serverThread.join ();
System.exit (0);
}
}
虽然真正的应用程序会更复杂,但TestApp会说明基本的使用模式。它会阻塞其输入队列,直到其中存在某些内容,处理它,然后将结果推送到其输出队列。
会话类管理特定客户端和服务器之间的实时连接。它从客户端获取输入并将其转换为Message对象,它从服务器获取Message对象并将它们转换为输出以发送到客户端。
服务器侦听新的传入连接,并为其拥有的每个传入连接设置Session对象。当Session向其传递消息时,它将其放入其上游队列以供应用程序处理。
我遇到的困难是收到返回消息,从TestApp返回到各个客户端。当来自客户端的消息进入时,会话生成一个消息并将其发送到服务器,然后服务器将其放入其上游队列,该队列也是TestApp的输入队列。作为响应,TestApp生成响应消息并将其放入输出队列,该队列也是服务器的下游队列。
这意味着Sessions需要等待两个不相关的事件。他们应该阻止
至于服务器本身,它还必须等待两个不相关的事件。
面对它看起来像一个简单的任务,实现起来比我想象的困难得多。我希望我能忽略一些显而易见的事情,因为我对并发编程仍然很陌生,但如果你能指出我出错的地方,我就会喜欢教学。
答案 0 :(得分:1)
因为您的实现使用方法在客户端会话服务器之间传递数据,所以您实际上已经解决了您的直接问题。但是,这可能不是你的意图。这是正在发生的事情:
Session的run
方法在自己的线程中运行,阻塞了套接字。当服务器调用waitForServer
时,此方法立即在Java中执行服务器线程中的,如果线程调用方法然后该方法在该线程中执行,那么在本例中为Session不需要解锁。为了创建您尝试解决的问题,您需要删除waitForServer
方法并将其替换为BlockingQueue messagesFromServer
队列 - 然后服务器会将消息放入此队列中,而Session将需要阻止它,导致Session需要阻塞两个不同的对象(套接字和队列)。
假设您切换到Session需要阻止两个对象的实现,我认为您可以通过我在评论中描述的两种方法的混合来解决这个问题:
每个Session的套接字都需要一个线程来阻塞它 - 我没有看到任何解决方法,除非你愿意用一个固定的线程池(比如4个线程)替换它来轮询套接字和睡眠如果没有任何东西可以从他们那里读取,那么几十毫秒。
您可以管理所有服务器 - &gt;具有单个队列和阻塞的单个线程的会话流量 - 服务器在其有效负载中包含会话“地址”,以便其上的线程阻塞知道如何处理该消息。如果你发现当你有很多会话时这不会扩展,那么你总是可以增加线程/队列数,例如有32个会话,你可以有4个线程/队列,每个线程/队列8个会话。
答案 1 :(得分:0)
我可能误解了,但似乎在你有代码“监听”消息的地方,你应该能够使用一个简单的OR语句来解决这个问题。
另一个可能有用的事情是为每个客户端添加一个唯一的ID,以便您可以告诉消息的目标客户端。