33

我正在研究以下基本 Java 套接字代码(source)。这是一个 Knock-Knock-Joke 客户端/服务器应用程序。

Client中,我们照常设置套接字:

try {
  kkSocket = new Socket("localhost", 4444);
  out = new PrintWriter(kkSocket.getOutputStream(), true);
  in = new BufferedReader(new InputStreamReader(kkSocket.getInputStream()));
} catch( UnknownHostException uhe ){ /*...more error catching */

稍后,我们只是对服务器进行读写:

BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String fromServer;
String fromUser;

while ((fromServer = in.readLine()) != null) {
  System.out.println("Server: " + fromServer);
  if (fromServer.equals("bye."))
      break;

  fromUser = stdIn.readLine();

  if (fromUser != null){
      System.out.println("Client: " + fromUser);
      out.println(fromUser);
  }

而在服务器上,我们有相应的代码,来获取笑话的妙语。

    KnockKnockProtocol kkp = new KnockKnockProtocol();

    outputLine = kkp.processInput(null);
    out.println(outputLine);

    while ((inputLine = in.readLine()) != null) {
         outputLine = kkp.processInput(inputLine);
         out.println(outputLine);
         if (outputLine.equals("Bye."))
            break;

我想在整个事情上附加一个心跳,当它检测到对方死亡时,它将打印到控制台。因为如果我杀死对方现在会发生什么是一个例外 - 如下所示:

在此处输入图像描述

因此,如果我同时运行 KnockKnockClient 和 KnockKnockServer,那么我关闭了 KnockKnockServer,应该发生的是在客户端上我看到以下输出:

>The system has detected that KnockKnockServer was aborted

我正在寻找任何提示。到目前为止,我主要是在尝试运行一个定期创建与另一端的新连接的守护线程。但是我对要检查的条件感到困惑(但我认为这只是一个boolean值?)。这是正确的方法吗?我刚在网上发现有一个名为JGroups 的多播网络库——那会是更好的方法吗?我正在寻找任何提示。

到目前为止我的服务器代码(对不起,它很乱)

&

客户端

谢谢

4

8 回答 8

16

但是你得到的例外就是这个!它告诉你,对方刚刚死了。只需捕获异常并打印到控制台,即“系统检测到 KnockKnockServer 已中止”。

您正在使用 TCP 连接,并且 TCP 具有内置的心跳(keepalive)机制,可以为您执行此操作。只需在套接字上设置 setKeepAlive() 即可。话虽如此 - 可以控制每个连接的保活频率,但我不知道如何在 java 中做到这一点。

http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html

https://stackoverflow.com/a/1480259/706650

于 2013-04-06T15:20:47.797 回答
12

你有一个同步通信。要获得心跳消息,请使用异步通信。会有2个线程。一个将从套接字读取,另一个将继续写入套接字。如果您使用异步通信,服务器将每 10 秒发送一次消息。客户端线程将从服务器读取消息,如果没有消息,则表示服务器已关闭。在您的情况下,服务器将消息发送回客户端(如果客户端有消息)或发送自动回复。您的服务器代码可以像这样修改。

  1. 创建一个服务器线程,它将每 10 秒向客户端发送消息。

    public class receiver extends Thread{
    
      public static bool hearbeatmessage=true;
    
      Socket clientSocket=new Socket();
      PrintWriter out=new PrintWriter();
      public receiver(Socket clientsocket){
      clientSocket=clientsocket;
      out = new PrintWriter(clientSocket.getOutputStream(), true);
    }
    
      public void run(){
    
        while(true)
        {
    
          if(heartbeatmessage){
            thread.sleep(10000);
            out.println("heartbeat");
    
          }
        }            
      }
    }
    

在您的服务器代码中:

KnockKnockProtocol kkp = new KnockKnockProtocol();

outputLine = kkp.processInput(null);
out.println(outputLine);
receiver r=new reciver(clientSocket);
r.run(); /*it will start sending hearbeat messages to clients */

while ((inputLine = in.readLine()) != null) {
     outputLine = kkp.processInput(inputLine);
     reciver.hearbeatMessage=false; /* since you are going to send a message to client now, sending the heartbeat message is not necessary */
     out.println(outputLine);
     reciver.hearbeatMessage=true; /*start the loop again*/
     if (outputLine.equals("Bye."))
        break;

客户端代码也将被修改,线程将继续从套接字读取消息,如果超过 11 秒(额外 1 秒)没有收到消息,它将声明服务器不可用。

希望这可以帮助。逻辑上也可能存在一些缺陷。让我知道。

于 2013-03-25T04:46:39.437 回答
10

以下是我们在与硬件接口(使用套接字)时日常应用的最佳实践。

好习惯1:SoTimeout

此属性启用读取超时。这样做的目的是避免汤姆遇到的问题。他在以下行中写道:“您需要等到下一条客户端消息到达”。好吧,这为该问题提供了解决方案。它也是实现心跳和许多其他检查的关键

默认情况下,该InputStream#read()方法将永远等待,直到消息到达。改变了这种setSoTimeout(int timeout)行为。它现在将应用超时。当它超时时,它会抛出SocketTimeoutException. 只需捕获异常,检查几件事并继续阅读(重复)。所以基本上,你把你的阅读方法放在一个循环中(甚至可能放在一个专用线程中)。

// example: wait for 200 ms
connection.setSoTimeout(200);

您可以使用这些中断(由超时引起)来验证状态:例如,自从我收到最后一条消息以来已经过了多长时间。

下面是一个实现循环的例子:

while (active)
{
  try
  {
    // some function that parses the message
    // this method uses the InputStream#read() method internally.
    code = readData();

    if (code == null) continue; 
    lastRead = System.currentTimeMillis();

    // the heartbeat message itself should be ignored, has no functional meaning.
    if (MSG_HEARTBEAT.equals(code)) continue;

    //TODO FORWARD MESSAGE TO ACTION LISTENERS

  }
  catch (SocketTimeoutException ste)
  {
    // in a typical situation the soTimeout should be about 200ms
    // the heartbeat interval is usually a couple of seconds.
    // and the heartbeat timeout interval a couple of seconds more.
    if ((heartbeatTimeoutInterval > 0) &&
        ((System.currentTimeMillis() - lastRead) > heartbeatTimeoutInterval))
    {
      // no reply to heartbeat received.
      // end the loop and perform a reconnect.
      break;
    }
    // simple read timeout
  }
}

此超时的另一种用途:它可用于通过设置干净地停止会话active = false。使用超时检查此字段是否为true. 如果是这样,那么break循环。没有SoTimeout逻辑,这是不可能的。您将被迫执行socket.close()或等待下一条客户端消息(这显然没有意义)。

良好做法 2:内置 Keep-Alive

connection.setKeepAlive(true);

好吧,基本上这就是你的心跳逻辑所做的。它会在一段时间不活动后自动发送信号并检查回复。但是,保持活动间隔取决于操作系统,并且有一些缺点。

良好做法 3:Tcp 无延迟

当您经常连接需要快速处理的小命令时,请使用以下设置。

try
{
  connection.setTcpNoDelay(true);
}
catch (SocketException e)
{
}
于 2013-08-11T17:26:30.677 回答
4

我认为你把事情复杂化了。

从客户端:
如果客户端得到一个IOException连接重置,那么这意味着服务器已经死了。一旦您知道服务器已关闭,只需执行您需要做的事情,而不是打印堆栈跟踪。您已经知道服务器由于异常而关闭。

从服务器端:
要么启动一个计时器,如果您在超过时间间隔的时间内没有收到请求,则假定客户端已关闭。
或者在客户端启动一个后台服务器线程(使客户端和服务器对等)并让服务器发送一个“虚拟”心跳请求(服务器现在充当客户端)。如果您遇到异常,则客户端已关闭。

于 2013-04-19T17:48:43.603 回答
3

想我会对此有所了解...我从 Java 站点上的KnockKnockServerKnockKnockClient开始(在您的原始问题中)。

我没有添加任何线程或心跳;我只是将 KnockKnockClient 更改为以下内容:

    try {    // added try-catch-finally block
      while ((fromServer = in.readLine()) != null) {
        System.out.println("Server: " + fromServer);
        if (fromServer.equals("Bye."))
          break;

        fromUser = stdIn.readLine();
        if (fromUser != null) {
          System.out.println("Client: " + fromUser);
          out.println(fromUser);
        }
      }
    } catch (java.net.SocketException e) {   // catch java.net.SocketException
      // print the message you were looking for
      System.out.println("The system has detected that KnockKnockServer was aborted");
    } finally {
      // this code will be executed if a different exception is thrown,
      // or if everything goes as planned (ensure no resource leaks)
      out.close();
      in.close();
      stdIn.close();
      kkSocket.close();
    }

这似乎可以满足您的要求(即使我修改了原始 Java 网站示例,而不是您的代码 - 希望您能够看到它的插入位置)。我用您描述的情况对其进行了测试(在客户端连接时关闭服务器)。

这样做的缺点是,当客户端等待用户输入时,您看不到服务器已经死了;你必须输入客户端输入,然后你会看到服务器已经死了。如果这不是您想要的行为,请发表评论(也许这就是问题的全部重点 - 看起来您可能已经走了一条比您需要的更长的路才能到达您想去的地方)。

于 2013-04-19T17:36:21.730 回答
2

这里对客户端稍作修改。它不使用明确的心跳,但只要您继续从服务器读取,无论如何您都会立即检测到断开连接。

这是因为 readLine 会立即检测到任何读取错误。

// I'm using an anonymous class here, so we need 
// to have the reader final.
final BufferedReader reader = in;

// Decouple reads from user input using a separate thread:
new Thread()
{
   public void run()
   {
      try
      {
         String fromServer;
         while ((fromServer = reader.readLine()) != null)
         {
            System.out.println("Server: " + fromServer);
            if (fromServer.equals("Bye."))
            {
                System.exit(0);
            }
         }
      }
      catch (IOException e) {}

      // When we get an exception or readLine returns null, 
      // that will be because the server disconnected or 
      // because we did. The line-break makes output look better if we 
      // were in the middle of writing something.
      System.out.println("\nServer disconnected.");
      System.exit(0);
   }
}.start();

// Now we can just read from user input and send to server independently:
while (true)
{
   String fromUser = stdIn.readLine();
   if (fromUser != null)
   {
      System.out.println("Client: " + fromUser);
      out.println(fromUser);
   }
}

在这种情况下,即使我们正在等待来自服务器的回复,我们也允许客户端写入。对于更稳定的应用程序,我们希望在等待回复时锁定输入,方法是添加一个控制何时开始读取的信号量。

这些是我们为控制输入所做的修改:

final BufferedReader reader = in;

// Set up a shared semaphore to control client input.
final Semaphore semaphore = new Semaphore(1);

// Remove the first permit.
semaphore.acquireUninterruptibly();

new Thread()

... code omitted ...

           System.out.println("Server: " + fromServer);
           // Release the current permit.
           semaphore.release();
           if (fromServer.equals("Bye."))

... code omitted ...

while (true)
{
    semaphore.acquireUninterruptibly();
    String fromUser = stdIn.readLine();

... rest of the code as in the original ...
于 2013-04-03T08:36:57.977 回答
1

我认为@Bala 的答案在服务器端是正确的。我想在客户端做一个补充。

在客户端,您应该:

  1. 使用变量来保存来自服务器的最后一条消息的时间戳;
  2. 启动一个定期运行的线程(例如每 1 秒)以比较当前时间戳和最后一条消息的时间戳,如果它长于所需的超时时间(例如 10 秒),则应报告断开连接。

以下是一些代码片段:

TimeoutChecker(线程):

static class TimeoutChecker implements Runnable {

    // timeout is set to 10 seconds
    final long    timeout = TimeUnit.SECONDS.toMillis(10);
    // note the use of volatile to make sure the update to this variable thread-safe
    volatile long lastMessageTimestamp;

    public TimeoutChecker(long ts) {
        this.lastMessageTimestamp = ts;
    }

    @Override
    public void run() {
        if ((System.currentTimeMillis() - lastMessageTimestamp) > timeout) {
            System.out.println("timeout!");
        }
    }
}

TimeoutChecker建立连接后启动:

try {
  kkSocket = new Socket("localhost", 4444);
  // create TimeoutChecker with current timestamp.
  TimeoutChecker checker = new TimeoutChecker(System.currentTimeMillis());
  // schedule the task to run on every 1 second.
  ses.scheduleAtFixedRate(, 1, 1,
            TimeUnit.SECONDS);
  out = new PrintWriter(kkSocket.getOutputStream(), true);
  in = new BufferedReader(new InputStreamReader(kkSocket.getInputStream()));
} catch( UnknownHostException uhe ){ /*...more error catching */

ses是一个ScheduledExecutorService

ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);

并且记住在从服务器接收消息时更新时间戳:

BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String fromServer;
String fromUser;

while ((fromServer = in.readLine()) != null) {
  // update the message timestamp
  checker.lastMessageTimestamp = System.currentTimeMillis();
  System.out.println("Server: " + fromServer);
  if (fromServer.equals("bye."))
    break;
于 2013-04-04T14:26:56.733 回答
1

Adel,正在查看您的代码http://pastebin.com/53vYaECK

您可以尝试以下解决方案。不确定它是否会起作用。我们可以每次都创建一个 BufferedReader 实例,而不是使用输入流创建一次缓冲读取器。当 kkSocket.getInputStream 为 null 时,退出 while 循环并将 completeLoop 设置为 false,这样我们就退出了 while 循环。它有 2 个 while 循环,并且每次都会创建对象。如果连接打开但其中没有数据 inputstream 不会为空,则 BufferedReader.readLine 将为空。

bool completeLoop=true;
while(completeLoop) {

while((inputstream is=kkSocket.getInputStream())!=null) /*if this is null it means the socket is closed*/
{  
BufferedReader in = new BufferedReader( new InputStreamReader(is));
while ((fromServer = in.readLine()) != null) {
            System.out.println("Server: " + fromServer);
            if (fromServer.equals("Bye."))
                break;
            fromUser = stdIn.readLine();
        if (fromUser != null) {
                System.out.println("Client: " + fromUser);
                out.println(fromUser);
           }
        } 
}
completeLoop=false;
System.out.println('The connection is closed');
}
于 2013-04-09T03:04:43.070 回答