Java多线程客户端/服务器 - java.net.SocketException:套接字关闭

5
我需要使用Java的socket API编写一个多线程客户端和服务器。客户端和服务器都是多线程的,因此服务器可以处理多个连接,客户端可以测试服务器处理连接的能力。
我的代码在这里: https://github.com/sandyw/Simple-Java-Client-Server 我遇到了一些问题,可能是相关的问题。其中一个问题是,偶尔会有一个客户端线程抛出异常。
java.net.SocketException: Socket closed
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.read(SocketInputStream.java:129)
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
    at java.io.InputStreamReader.read(InputStreamReader.java:167)
    at java.io.BufferedReader.fill(BufferedReader.java:136)
    at java.io.BufferedReader.readLine(BufferedReader.java:299)
    at java.io.BufferedReader.readLine(BufferedReader.java:362)
    at ClientThread.run(ClientThread.java:45)
    at java.lang.Thread.run(Thread.java:680)

由于其位置,这意味着服务器在客户端完成读取之前关闭了套接字。我该如何防止这种情况发生?

另外,即使计算引发Socket closed异常的线程数,似乎并不是所有服务器线程都发送其输出。例如,30个服务器线程中的23个将发送其输出,但只有22个客户端线程会收到任何内容。通信显然在某处丢失,我该如何防止这种情况?


乍一看,我会提出以下问题:(1)您是否在服务器端刷新输出流(以确保没有等待写入的数据),(2)在ClientThread.java的55-57行中,我认为您不需要单独关闭流而不是关闭套接字。尝试不这样做(在客户端和服务器端都是如此),看看会发生什么。最后(3)查看close()方法的JavaDoc,并查看是否有任何内容可以阐明正在发生的情况:http://download.oracle.com/javase/1.5.0/docs/api/java/net/Socket.html#close%28%29,以避免过早关闭。 - jefflunt
是的,每当任一方发送数据时,我都会调用flush()。 - Tick-Tock
但是,你已经通过最后一个参数将PrintWriter设置为自动刷新,所以它已经为你完成了。flush()不是必需的,因为设置为自动刷新的PrintWriter将在每次print*()方法调用后调用flush()。 - chubbsondubs
1个回答

22

你的问题在ClientThread类中:

private static Socket socket = null;
这意味着所有线程为ClientThread的所有实例共享同一个套接字实例。这意味着您的套接字将与对话状态不同步。您的对话如下:
客户端状态:
1. 客户端连接 2. 客户端发送请求 3. 客户端等待响应 4. 客户端接收响应 5. 客户端关闭套接字(每个都应该这样做)。
服务器状态:
1. 服务器等待客户端 2. 服务器读取请求 3. 服务器处理命令 4. 服务器发送响应 5. 服务器关闭套接字
然而,您有30个对话同时尝试处于不同状态,并且服务器和客户端无法跟踪它们。第一个线程创建套接字,发送请求并转移到状态2,同时在等待中另一个线程创建另一个套接字,将其写入并转移到状态2,当第一个线程醒来时,它开始通过线程2创建的新套接字交谈,而线程2还没有完成处理命令。现在第三个线程开始并再次覆盖该引用,如此往复。第一个命令永远不会被读取,因为当线程2覆盖它时失去了对原始套接字的引用,并且它开始读取线程2的命令并将其输出。一旦线程1到达关闭语句,它就关闭套接字,而其他线程正在尝试读取/写入它,于是异常被抛出。
基本上,每个ClientThread应该创建自己的套接字实例,然后每个对话可以独立地进行,而不会与其他正在进行的对话相互干扰。想象一下如果您的客户端是单线程的。然后启动两个单独的进程(两次运行Java应用程序)。每个进程将创建自己的套接字,并且每个对话都将独立工作。现在您拥有的是30个线程通过同一个扩音器向服务器发出命令的一个套接字。当所有人都在使用同一个扩音器时,无法按顺序进行工作。
因此,在ClientThread中从套接字成员中删除static修饰符,并且它应该工作得很好。
只是顺便提一句,永远不要将这个代码发布到公共领域。它存在严重的安全问题,因为客户端可以以您的服务器进程的安全级别执行任何命令。因此,任何人都可以很容易地拥有您的计算机或捕获您的帐户。从客户端执行这样的命令意味着他们可以发送:
sudo cat /etc/passwd

比如获取你的密码哈希值。我知道你只是在学习,但我觉得应该提醒你注意安全。

另外一个问题是,服务器仅在对话符合预期时才关闭套接字。你真的应该将close()调用移动到你已有的try块中的finally块中。否则,如果客户端过早地关闭其套接字(这种情况很常见),那么你的服务器将泄漏套接字,并最终会在操作系统中耗尽套接字。

public void run() {
   try {
   } catch( SomeException ex ) {
      logger.error( "Something bad happened", ex );
   } finally {
      out.close();   <<<< not a bad idea to try {} finally {} these statements too.
      in.close();    <<<< not a bad idea to try {} finally {} these statements too.
      socket.close();
   }
}

另一个你可能想要尝试的东西是在服务器上使用线程池而不是为每个新连接创建一个新线程。在你的简单示例中,这很容易实现并且有效。然而,如果你正在构建一个真正的服务器,线程池有两个主要作用。1.创建线程会带来一定的开销,因此,通过让一个线程保持等待以服务于传入请求,你可以获得一些性能响应时间,从而节省响应客户端的时间。2.更重要的是,物理计算机无法无限制地创建线程。如果你有很多客户端,比如1000+,你的机器将难以使用1000个线程来回应所有这些请求。线程池意味着你最多创建50个线程,并且每个线程将被多次使用来处理每个请求。当新连接进入时,它们会等待线程释放后再进行处理。如果有太多的连接进入,客户端将超时而不是摧毁你的计算机并需要重新启动。它允许你更快地处理更多的请求,同时防止你在同时收到太多连接时死机。

最后,关闭套接字异常可能发生很多合理的原因。如果客户端在对话的过程中突然关闭,服务器将会遇到这个异常。最好在发生时适当地关闭和清理自己。我的意思是你永远无法防止它,你只需要回应它。


谢谢!那真是说得太清楚了,我没有在那里加上静态修饰符,所以一直没意识到。我已经为此困扰了几天了。 - Tick-Tock

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