Java套接字InputStream在客户端和服务器上都会挂起/阻塞

4

最近我在开发一个小程序,旨在远程关闭浏览器。基本流程如下:

服务器端:

  1. 创建一个SocketServer以监听特定的端口。
  2. 接受连接并创建相应的socket对象。
  3. 从创建的socket读取输入流(此操作被阻塞)。

客户端:

  1. 创建一个socket对象与服务器建立连接。
  2. 通过向输出流写入字节向服务器发送关闭浏览器的命令。
  3. 通过使用socket的输入流上的read()方法读取服务器的反馈(此操作被阻塞)。

代码如下:

Server.java

package socket;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Enumeration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server {

    private static ExecutorService es = Executors.newFixedThreadPool(5);
    
    public static void main(String[] args) throws IOException {
        
        InetAddress targetAddress = null;
        NetworkInterface ni = NetworkInterface.getByName("eth2");
        System.out.println(ni);
        Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
        while(inetAddresses.hasMoreElements()) {
            InetAddress inetAddress = inetAddresses.nextElement();
            if(inetAddress.toString().startsWith("/10")) {
                targetAddress = inetAddress;
                break;
            }
        }
        ServerSocket sSocket = new ServerSocket(11111, 0, targetAddress);
        while(true) {
            System.out.println("Server is running...");
            Socket client = sSocket.accept();
            System.out.println("Client at: " + client.getRemoteSocketAddress());
            es.execute(new ClientRequest(client));
        }
        
    }
}

ClientRequest.java

package socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class ClientRequest implements Runnable {

    private Socket client;
    
    public ClientRequest(Socket client) {
        this.client = client;
    }
    
    @Override
    public void run() {
        
        try {
            System.out.println("Handled by: " + Thread.currentThread().getName());
            // get input/output streams for client socket
            InputStream cis = client.getInputStream();
            OutputStream cos = client.getOutputStream();
            
            // buffer size : 1024 ?
            byte[] buffer = new byte[1024];
            int recvSize;
            int totalRecvSize = 0;
            while(-1 != (recvSize = cis.read(buffer, totalRecvSize, 1024 - totalRecvSize))) {
                totalRecvSize += recvSize;
            }
            
            String command = new String(buffer, "utf-8");
            System.out.println("Command from client: " + command);
            
            String commandNative = CommandMap.getNativeCommand(command.trim());
            if(null != commandNative) {
                Process np = Runtime.getRuntime().exec(commandNative);
                InputStream is = np.getInputStream();
                byte[] bufferProcess = new byte[1024];
                int bytesRead;
                int totalBytesRead = 0;
                while(-1 != (bytesRead = is.read(bufferProcess, totalBytesRead, 1024 - totalBytesRead))) {
                    totalBytesRead += bytesRead;
                }
                // give feed back of process output
                cos.write(bufferProcess);
                
                // close process input stream
                is.close();
            } 
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(null != client) {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

最后是 Client.java
package socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.Charset;

public class Client {

    private static final int BUF_SIZE = 1024;
    
    // feedback message size will not exceed 1024 bytes
    private static final byte[] BUFFER = new byte[BUF_SIZE];
    
    public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {
        
        Socket socket = new Socket("10.117.37.176", 11111);
        System.out.println("Connected to Server...");
        OutputStream os = socket.getOutputStream();
        InputStream is = socket.getInputStream();
        
        String command = "kill ie";
        byte[] commandBytes = command.getBytes(Charset.forName("utf-8"));
        
        System.out.println("Send: " + command);
        os.write(commandBytes);
        System.out.println("After send: " + command);
        
        int totalRecv = 0;
        int recvSize;
        
        while(-1 != (recvSize = is.read(BUFFER, totalRecv, BUF_SIZE - totalRecv))) {
            totalRecv += recvSize;
        }
        
        String feedback = new String(BUFFER, "utf-8");
        System.out.println("Feedback: " + feedback);
        
        socket.close();
    }
}

再次强调问题:

  • 服务器端无法通过调用套接字InputStream对象上的read(buffer, offset, len)来读取命令。它会被阻塞。
  • 客户端无法通过调用其套接字InputStream对象上的read(buffer, offset, len)来读取反馈。它会被阻塞。
  • 但是,当我在Client.java中注释掉反馈读取操作时,服务器和客户端都可以正常工作。

我想知道这段代码中隐藏的原因是什么?

2个回答

12

您的套接字读取代码一直读取,直到遇到EOF(文件结束标志)。只有在关闭套接字之后才会收到EOF。因此,您的代码永远无法继续执行。

如果您只想在套接字连接上发送单个命令,则在写入该命令后关闭OutputStream(使用Socket.shutdownOutput())。

如果您想在单个套接字连接上发送多个命令,则需要想出一种分隔每个命令的方法(这里有一个简单的例子)。


谢谢您的出色答案!但是还有一个问题存在,客户希望得到一些反馈,在我的情况下,反馈应该是本地命令的输出(旨在关闭浏览器)。如果我添加了 os.close(),当我调用 InputStream 上的 read() 方法时,将抛出 SocketException 异常,消息为“socket closed”,我该如何处理此问题?非常感谢。 - destiny1020
@destiny1020 - 你试过我的建议了吗?关闭输出流与关闭输入流是独立的。 - jtahlborn
是的,我尝试了你的建议,在Client.java中在os.write()后添加了一行os.close()。但是当我尝试调用in.read()方法时出现了一个异常:Socket closed,也许关闭底层OutputStream会间接关闭套接字连接?我对此感到困惑。谢谢你的回复! - destiny1020
我使用它自己的close()方法关闭输出流,实际上应该调用socket.shutdownOutput()方法,我已经弄清楚了。感谢你的建议,太棒了! - destiny1020
@destiny1020 - 啊,对不起,我会更新我的答案。我以为直接关闭输出流就可以了。已经有一段时间没有使用套接字了。 - jtahlborn

1
您正在两端都读取EOS,这是一个不好的选择,因为您无法“发送”EOS而不失去在同一连接上发送进一步数据的能力。您需要重新设计应用程序协议,以便可以一次读取一个命令,而不需要关闭连接来完成它。例如,发送行、长度字前缀、自描述协议(如XML或Java对象序列化)或任何您可以通过DataInputStream读取而不依赖于EOFException的内容。

感谢您的建议,我的应用确实存在一些设计问题。非常感谢。 - destiny1020

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