当必须接受许多连接时,我在客户端/服务器类型应用程序中遇到 Java 套接字问题

Posted

技术标签:

【中文标题】当必须接受许多连接时,我在客户端/服务器类型应用程序中遇到 Java 套接字问题【英文标题】:I'm having troubles with Java sockets in a client/server type application when having to accept many connections 【发布时间】:2011-08-03 09:36:13 【问题描述】:

首先,感谢您的阅读。这是我第一次以用户身份使用 ***,尽管我一直在阅读并找到有用的解决方案:D。顺便说一句,对不起,如果我解释得不够清楚,我知道我的英语不是很好。

我的基于套接字的程序有一个奇怪的行为,以及一些性能问题。客户端和服务器通过以多线程方式将序列化对象读/写到对象输入和输出流中来相互通信。让我向您展示代码基础知识。我已将其简化为更具可读性,并且故意省略了完整的异常处理。服务器的工作方式如下:

服务器:

// (...)

public void serve() 
    if (serverSocket == null) 
        try 
            serverSocket = (SSLServerSocket) SSLServerSocketFactory
                                    .getDefault().createServerSocket(port);
            serving = true;
            System.out.println("Waiting for clients...");
            while (serving) 
                SSLSocket clientSocket = (SSLSocket) serverSocket.accept();
                System.out.println("Client accepted.");
                //LjServerThread class is below
                new LjServerThread(clientSocket).start();
            
         catch (Exception e) 
            // Exception handling code (...)
        
    


public void stop() 
    serving = false;
    serverSocket = null;


public boolean isServing() 
    return serving;

LjServerThread 类,每个客户端创建一个实例:

private SSLSocket clientSocket;
private String IP;
private long startTime;

public LjServerThread(SSLSocket clientSocket) 
        this.clientSocket = clientSocket;
        startTime = System.currentTimeMillis();
        this.IP = clientSocket.getInetAddress().getHostAddress();


public synchronized String getClientAddress() 
    return IP;


@Override
public void run() 
    ObjectInputStream in = null;
    ObjectOutputStream out = null;
    //This is my protocol handling object, and as you will see below,
    //it works processing the object received and returning another as response.
    LjProtocol protocol = new LjProtocol();
    try 
        try 
            in = new ObjectInputStream(new BufferedInputStream(
                                     clientSocket.getInputStream()));
            out = new ObjectOutputStream(new BufferedOutputStream(
                                    clientSocket.getOutputStream()));
            out.flush();
         catch (Exception ex) 
            // Exception handling code (...)
        
        LjPacket output;
        while (true) 
            output = protocol.processMessage((LjPacket) in.readObject());
            // When the object received is the finish mark, 
            // protocol.processMessage()object returns null.
            if (output == null) 
                break;
            
            out.writeObject(output);
            out.flush();
            out.reset();
        
        System.out.println("Client " + IP + " finished successfully.");
     catch (Exception ex) 
        // Exception handling code (...)
     finally 
        try 
            out.close();
            in.close();
            clientSocket.close();
         catch (Exception ex) 
            // Exception handling code (...)
         finally 
            long stopTime = System.currentTimeMillis();
            long runTime = stopTime - startTime;
            System.out.println("Run time: " + runTime);
        
    

而且,客户端是这样的:

    private SSLSocket socket;

    @Override
    public void run() 
        LjProtocol protocol = new LjProtocol();
        try 
            socket = (SSLSocket) SSLSocketFactory.getDefault()
                     .createSocket(InetAddress.getByName("here-goes-hostIP"),
                                                                       4444);
         catch (Exception ex) 

        
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        try 
        out = new ObjectOutputStream(new BufferedOutputStream(
                                         socket.getOutputStream()));
        out.flush();
        in = new ObjectInputStream(new BufferedInputStream(
                                          socket.getInputStream()));
        LjPacket output;
        // As the client is which starts the connection, it sends the first 
        //object.
        out.writeObject(/* First object */);
        out.flush();
        while (true) 
            output = protocol.processMessage((LjPacket) in.readObject());
            out.writeObject(output);
            out.flush();
            out.reset();
        
         catch (EOFException ex) 
            // If all goes OK, when server disconnects EOF should happen.
            System.out.println("suceed!");
         catch (Exception ex) 
            // (...)
         finally 
            try 
                // FIRST STRANGE BEHAVIOUR:
                // I have to comment the "out.close()" line, else, Exception is
                // thrown ALWAYS.
                out.close();
                in.close();
                socket.close();
             catch (Exception ex) 
                System.out.println("This shouldn't happen!");
            
        
    

嗯,如您所见,LjServerThread 类在服务器端处理接受的客户端,测量它所花费的时间......通常,它需要 75 到 120 毫秒(其中 x 是 IP):

客户端 x 成功完成。 运行时间:82 客户端 x 成功完成。 运行时间:80 客户端 x 成功完成。 运行时间:112 客户端 x 成功完成。 运行时间:88 客户端 x 成功完成。 运行时间:90 客户端 x 成功完成。 运行时间:84

但突然之间,没有可预测的模式(至少对我而言):

客户端 x 成功完成。 运行时间:15426

有时会达到 25 秒! 偶尔一小群线程会慢一点,但我并不担心:

客户端 x 成功完成。 运行时间:239 客户端 x 成功完成。 运行时间:243

为什么会这样?这可能是因为我的服务器和我的客户端在同一台机器上,具有相同的 IP? (要进行此测试,我在同一台机器上执行服务器和客户端,但它们使用我的公共 IP 通过 Internet 连接)。

这就是我测试的方式,我在 main() 中向服务器发出这样的请求:

    for (int i = 0; i < 400; i++) 
        try 
            new LjClientThread().start();
            Thread.sleep(100);
         catch (Exception ex) 
            // (...)
        
    

如果我在没有“Thread.sleep(100)”的情况下在循环中执行此操作,我会收到一些连接重置异常(7 或 8 个连接在 400 中重置,或多或少),但我想我理解它发生的原因:当serverSocket.accept() 接受一个连接,需要花费很短的时间才能再次到达 serverSocket.accept()。在此期间,服务器无法接受连接。会不会是因为这个?如果不是,为什么?很少有 400 个连接同时到达我的服务器,但它可能会发生。如果没有“Thread.sleep(100)”,时间问题也会更糟。

提前致谢!


更新:

多么愚蠢,我在 localhost 中测试了它......它没有给出任何问题!有无“Thread.sleep(100)”,没关系,它工作正常!为什么!所以,正如我所看到的,我关于为什么连接重置被抛出的理论是不正确的。这让事情变得更加奇怪了!我希望有人可以帮助我...再次感谢! :)


更新(2):

我发现不同操作系统中的行为明显不同。我通常在 Linux 中开发,我解释的行为是关于我的 Ubuntu 10.10 中发生的事情。在 Windows 7 中,当我在连接之间暂停 100 毫秒时,一切正常,并且所有线程都快速点亮,没有人花费超过 150 毫秒左右(没有连接速度慢的问题!)。这不是在 Linux 中发生的事情。但是,当我删除“Thread.sleep(100)”时,不是只有一些连接得到连接重置异常,而是所有连接都失败并抛出异常(在 Linux 中只有其中一些,400 个中有 6 个左右失败了)。

呸!我刚刚发现不仅操作系统,JVM 环境也有一点影响!没什么大不了的,但值得注意。我在 Linux 中使用 OpenJDK,现在使用 Oracle JDK,我发现随着我减少连接之间的睡眠时间,它开始更早地失败(50 ms OpenJDK 工作正常,没有抛出异常,但 Oracle 的一个相当很多有 50 毫秒的睡眠时间,而有 100 毫秒的工作正常)。

【问题讨论】:

文字的圣墙,蝙蝠侠!人们需要一段时间才能筛选出来。也许你需要一个“tl;dr”? 您应该检查垃圾收集器日志 (-verbose:gc -Xloggc:file) 以排除这些暂停是由 GC 周期引起的。它们不太可能是,但 GC 看似任意打嗝的来源。 对不起,我知道它真的很长......但这主要是因为代码引用,有些可能对读者有用。无论如何我都会尝试阅读减少它:)。 谢谢马克,我会检查的。但我认为GC这次是无罪的。主要是因为我在 localhost 中对此进行了测试,既没有暂停也没有异常。此外,我使用 VisualVM 来查看它是否与 GC 相关,线程过多,等等……我似乎没有任何问题 你当然可以在 run 方法中添加更多的时间来确定慢的地方。 【参考方案1】:

服务器套接字有一个队列来保存传入的连接尝试。如果该队列已满,客户端将遇到连接重置错误。如果没有Thread.sleep(100) 语句,您的所有客户端都在尝试相对同时连接,这会导致其中一些客户端遇到连接重置错误。

【讨论】:

非常感谢史蒂夫!这解释了连接重置异常的原因,尽管我认为如果它们是连接拒绝异常会更有意义。 但是,我仍然不知道为什么有些连接速度变慢了这么多。顺便说一句,我更新了这个问题,因为我发现不同操作系统中的行为明显不同。 抛出的异常确实是连接被拒绝的异常,我的错误,对不起,我不知道如何但我很困惑。现在它更有意义了! 嗯,我一点也不困惑...通常会抛出连接被拒绝,但通常不会但有时会重置连接【参考方案2】:

我认为您可以进一步考虑研究两点。抱歉这里有点含糊,但这是我的想法。

1)在底层,在 tcp 级别,很少有依赖于平台的东西控制通过套接字发送/接收数据所需的时间。不一致的延迟可能是由于 tcp_syn_retries 等设置造成的。你可能有兴趣看这里http://www.frozentux.net/ipsysctl-tutorial/chunkyhtml/tcpvariables.html#AEN370

2)您计算的执行时间不仅是完成执行所花费的时间,还包括完成完成之前的时间,这不能保证在对象准备好完成时立即发生。

【讨论】:

第二点被丢弃,我添加了更多的计时测试代码,发现“clientSocket.getInputStream()”是导致问题的原因。但我会考虑进一步研究第一个想法;)。

以上是关于当必须接受许多连接时,我在客户端/服务器类型应用程序中遇到 Java 套接字问题的主要内容,如果未能解决你的问题,请参考以下文章

使用 gen_tcp 时的套接字接受率

无法接受服务器上的套接字连接

连接超时 - 端口 4444(java 应用程序)

当目前没有 wsarecv 时,传入数据会发生啥

tcp服务器和客户端交互

Netty SSL:具有自定义密钥库的Chat Client示例无法接受多个连接