您的问题是在ClientThread中:
private static Socket socket = null;
这意味着所有线程都为ClientThread的所有实例共享相同的套接字实例.这意味着你的套接字将与对话状态不同步.你的谈话是:
客户说:
>客户端连接
>客户端发送请求
>客户等待回复
>客户收到回复
>客户端关闭套接字(应由每个完成).
服务器状态:
>服务器等待客户端
>服务器读取请求
>服务器进程命令
>服务器发送响应
>服务器关闭套接字
但是,您有30个会话同时尝试处于不同的状态,并且服务器和客户端无法跟踪它们.第一个线程创建套接字发送请求移动到状态2,而它等待另一个线程创建另一个套接字,写入移动到状态2,当第一个线程唤醒它然后开始通过线程2创建的新套接字进行通话尚未完成处理它的命令.现在第三个启动并再次覆盖该引用,依此类推.第一个命令永远不会被读取,因为当线程2覆盖它时,它会丢失对原始套接字的引用,并且它开始读取线程2的命令并将其吐出.一旦线程1进入close语句,它就会关闭套接字,而其他线程正在尝试读取/写入它,并且抛出异常被抛出.
基本上每个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.创建线程有一个与之相关的开销,这样你就可以通过让一个线程等待服务传入的请求来获得一些性能响应时间.您可以节省一些时间来回应客户.更重要的是,物理计算机无法无休止地创建线程.如果你有很多客户说1000你的机器很难回答所有使用1000线程的机器.线程池意味着您创建最大线程数,比如50个线程,并且每个线程将被多次使用以处理每个请求.当新连接进入时,它们等待线程在处理之前释放.如果你的连接太多,客户端会超时,而不是破坏需要重启的机器.它允许您更快地获得更多请求,同时如果有太多连接立即进入,则可以避免死亡.
最后,出于许多正当理由,可能会发生关闭套接字异常.通常,如果客户端在对话中间关闭,服务器将获得该异常.当发生这种情况时,最好正确关闭并清理自己.你无法阻止它是我的观点.你只需回应它.