您的问题是在 ClientThread 中:
private static Socket socket = null;
这意味着所有线程都为 ClientThread 的所有实例共享同一个套接字实例。这意味着您的套接字将与会话状态不同步。你们的对话是:
客户声明:
- 客户端连接
- 客户端发送请求
- 客户端等待响应
- 客户端收到响应
- 客户端关闭套接字(应由每个客户端完成)。
服务器状态:
- 服务器等待客户端
- 服务器读取请求
- 服务器处理命令
- 服务器发送响应
- 服务器关闭套接字
但是,您有 30 个对话都试图同时处于不同的状态,而服务器和客户端无法跟踪它们。第一个线程创建套接字,发送请求,移动到状态 2,在等待时,另一个线程创建另一个套接字,将其写入移动到状态 2,当第一个线程唤醒时,它开始与线程 2 创建的新套接字进行通信尚未完成处理其命令。现在第三个启动并再次覆盖该引用,依此类推。第一个命令永远不会被读取,因为当线程 2 覆盖它时,它丢失了对原始套接字的引用,并且它开始读取线程 2 的命令并将其吐出。一旦线程 1 到达 close 语句,它就会关闭套接字,而其他线程正在尝试读/写它,并抛出异常。
本质上,每个 ClientThread 都应该创建自己的套接字实例,然后每个对话都可以独立于其他正在进行的对话进行。想想你是否将客户端编写为单线程。然后启动两个单独的进程(运行 Java 应用程序两次)。每个进程都会创建自己的套接字,每个对话都会独立工作。您现在拥有的是一个带有 30 个线程的套接字,通过一个扩音器向服务器发出命令。当每个人都用同一个扩音器大声喊叫时,工作就无法有序进行。
因此,总而言之,更改从 ClientThread 中的套接字成员中删除静态修饰符,它应该开始工作得很好。
顺便说一句,永远不要将此代码发布到世界上。它存在严重的安全问题,因为客户端可以在服务器进程运行的安全级别上执行任何命令。因此,任何人都可以轻松地拥有您的机器或相对轻松地获取您的帐户。从客户端执行这样的命令意味着他们可以发送:
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 个线程,并且每个线程将被多次使用来处理每个请求。当新连接进入时,它们会等待线程释放后再进行处理。如果您收到太多连接,客户端将超时,而不是破坏您的计算机并需要重新启动。它可以让您更快地处理更多请求,同时防止同时出现太多连接而导致死亡。
最后,由于许多合理的原因,可能会发生关闭套接字异常。通常,如果客户端在会话过程中关闭,服务器就会收到该异常。发生这种情况时,最好正确关闭并清理自己。你永远无法阻止这是我的观点。你只需要回应它。