我正在使用Java实现服务器/客户端系统。服务器侦听来自客户端的传入连接,并在客户端连接后,服务器创建一个新的套接字并将其传递给一个新的线程,该线程仅用于接收数据:
while (true){
clientSocket=serverSocket.accept();
new ClientReceiver(clientSocket,this.clientsManager).start();
}
clientReceiver类如下:
public class ClientReceiver extends Thread {
private Socket clientSocket=null;
private Client client=null;
private ClientsManager clientsManager;
private ClientActionParser clientActionParser=new ClientActionParser();
ClientHandlerState clientHandlerState;
PrintWriter outputStream=null;
BufferedReader inputStream=null;
public ClientReceiver(Socket clientSocket, ClientsManager clientsManager){
this.clientSocket=clientSocket;
this.clientsManager=clientsManager;
this.setClientHandlerState(ClientHandlerState.Connected);
}
public void run(){
String actionString;
try{
//define output and input stream to client
outputStream =new PrintWriter(clientSocket.getOutputStream(),true);
inputStream = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//while client is connected read input to actionString
while((actionString=inputStream.readLine()) != null){
AbstractClientAction clientAction= this.clientActionParser.parse(actionString);
if(this.getClientHandlerState()==ClientHandlerState.Connected){
if(clientAction instanceof ClientLoginAction){
ClientLoginAction clientLoginAction=(ClientLoginAction) clientAction;
if(this.authenticate(clientLoginAction)){
}
else{
throw new AuthenticationException();
}
}
else{
throw new AuthenticationException();
}
}
}
if(this.getClientHandlerState()==ClientHandlerState.Authorized){
//receive other client actions: transfer barge ....
}
try {
Thread.sleep(400);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
catch(IOException e){
}
catch (AuthenticationException e) {
// TODO: handle exception
}
//clean up the resources
try{
outputStream.close();
inputStream.close();
clientSocket.close();
}
catch(Exception e){
}
}
private boolean authenticate(ClientLoginAction clientLoginAction){
//perform authentication. If authentication successful:
this.client=this.clientsManager.authenticateClient(clientLoginAction.getUsername(), clientLoginAction.getPassword());
if(this.client==null){
return false;
}
else{
ClientSender clientSender=new ClientSender(this.outputStream, this.client);
this.clientsManager.addClientSender(clientSender);
this.setClientHandlerState(ClientHandlerState.Authorized);
clientSender.start();
return true;
}
}
public ClientHandlerState getClientHandlerState(){
return this.clientHandlerState;
}
public void setClientHandlerState(ClientHandlerState clientHandlerState){
this.clientHandlerState=clientHandlerState;
}
在接收器线程中成功验证后,创建一个新线程用于向客户端发送数据,并将套接字的outputStream传递给新线程。 clientSender类包含一个队列作为缓冲区,其中包含应发送给客户端的数据。这是class clientSender:
public class ClientSender extends Thread {
private Client client=null;
private final Log logger = LogFactory.getLog(getClass());
PrintWriter outputStream=null;
private Queue<String> clientEventsQueue= new LinkedList<String>();
public ClientSender(PrintWriter outputStream, Client client){
this.outputStream=outputStream;
this.client=client;
}
public void run(){
//System.out.println("ClientSender run method called.");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(!this.clientEventsQueue.isEmpty()){
this.outputStream.println(this.clientEventsQueue.remove());
}
}
}
public Client getClient(){
return this.client;
}
public void insertClientEvent(String clientEvent){
this.clientEventsQueue.add(clientEvent);
}
每当我想向我使用的客户端发送内容时:
clientSender.insertClientEvent("some text");
问题是,如果我删除Thread.sleep(10),我将不会在客户端收到任何内容。由于TCP套接字阻塞,我认为这不应该发生。这是正常的还是我做错了什么?
修改 发送方线程没有“终止”。服务器应该在收到来自另一个系统的事件时向所有客户端发送适当的信息。所以我认为最好的方案是在没有数据发送时停止线程,并在有数据时启动它。所以我在clientSender类中尝试了这个:
public void run(){
while(true){
if(this.clientEventsQueue.isEmpty()){
break;
}
else{
try {
this.outputStream.println(this.clientEventsQueue.take());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
但现在问题是何时启动线程?每当我想发送数据时我都尝试启动它,但正如我预期的那样,它无法正常工作并且只发送第一个包:
clientSender.insertClientEvent(clientEvent.getEventString());
clientSender.start();
EDIT2 我提出了这个想法。它非常简单,我认为它消耗的CPU时间要少得多。
while(true){
while(this.clientEventsQueue.isEmpty()){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
this.outputStream.println(this.clientEventsQueue.take());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
尽管我测试了它,它工作得很好。你觉得怎么样?
答案 0 :(得分:4)
我看到您使用LinkedList
作为多个线程访问的队列,并且您正在ClientSender
中忙着等待它。
此实现不是线程安全的,可能会导致clientEvents
上发布的ClientSender
问题不会被BlockingQueue
线程看到,CPU被浪费等等。
您可以改为使用take()
并在其上调用sleep(400)
来阻止队列,直到发布事件为止。
我也看到你依靠String stuff = queue.take();
if (stuff == null) break;
等待沟通。这也会引起问题。使用套接字资源的线程可以在完成后关闭它,而不是这个。
修改强>
有许多技术可以处理终止线程。我认为“毒丸”在这种情况下效果很好。基本上你做:
null
当你要终止它时,并在队列上发布"terminate"
(不必为空,可以是任何内容,例如Runnable
等。
<强> EDIT2:强>
你的终止方式不起作用,因为它会在任何人发布事件之前立即终止。从理论上讲,你可能会产卵,然后立即反复杀死线程等。最简单的解决方法是使用特殊的消息(又名“毒丸”)来终止条件。
至于只在有事件时才有线程,那时我建议使用线程池。您可以将事件发送封装到{{1}}中,并将套接字保存在Map中。然而,这实现起来非常复杂,需要很好地理解多线程以使其正确。多线程很难,如果做错了会引起严重的头痛。 Tbh我不建议在不学习更多关于多线程编程的情况下尝试这样做。
<强> EDIT3:强> @ user2355734:像许多人一样,按照间隔轮询队列,但不鼓励。 take()方法实际上将“休眠”并且只有在队列中存在某些内容时才会唤醒,因此理论上通过删除“睡眠”循环,您应该获得更低的CPU使用率和更短的延迟。通常,您应该尽量避免在多线程代码中使用“sleep”。您真正需要它并且它经常是破坏/次优代码的标志。至于测试,尽管它们很有用,但通过测试保证多线程代码的正确性很难甚至几乎不可能。您的代码可能在您的测试中运行良好但在生产中失败,在高负载下,在不同的环境下等。因此,重要的是要检查代码并确保它在理论上是正确的。