本节的目的是比较在高负载下如何阻塞服务器,即使你写的是正确的。这是我们在教育过程中可能经历过的练习:在原始的套接字上写一个服务器。我们将会实现一个非常简单的HTTP服务器,每个请求都响应200 ok。事实上,为了简单起见,我们将完全忽略这个请求。

Single threaded server

最简单的实现只是打开一个ServerSocket并在客户端连接到来时处理它们。当一个客户端正在被服务时,所有其他请求都将排队进排队。下面的代码片段实际上非常简单:


import org.apache.commons.io.IOUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

class SingleThread {
    public static final byte[] RESPONSE = (
            "HTTP/1.1 200 OK\r\n" +
                    "Content-length: 2\r\n" +
                    "\r\n" +
                    "OK").getBytes();

    public static void main(String[] args) throws IOException {
        final ServerSocket serverSocket = new ServerSocket(8080, 100);
        while (!Thread.currentThread().isInterrupted()) {
            final Socket client = serverSocket.accept();
            handle(client);
        }
    }

    private static void handle(Socket client) {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                readFullRequest(client);
                client.getOutputStream().write(RESPONSE);
            }
        } catch (Exception e) {
            e.printStackTrace();
            IOUtils.closeQuietly(client);
        }
    }

    private static void readFullRequest(Socket client) throws IOException {
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(client.getInputStream()));
        String line = reader.readLine();
        while (line != null && !line.isEmpty()) {
            line = reader.readLine();
        }
    }
}

在大学之外,您不会看到类似的低级实现,但它是可以运行的。对于每个请求,我们忽略了发送给我们的任何东西,并返回200 OK响应。在浏览器中打开localhost:8080将会成功收到一个OK的文本回复。这个类被命名为SingleThread是有原因的。 ServerSocket.accept()会一直阻塞,直到任何客户机与我们建立连接。然后,它返回一个客户端的套接字。当我们与那个套接字(读和写)交互时,我们仍然侦听传入的连接,但是没有人接会处理他们,因为我们的线程正在忙着处理第一个客户端。就像在医生的办公室里:一个病人进去了,其他人都必须排队等候。在设置8080(监听端口)之后,您注意到我们填写的100这个参数了吗?此值(缺省值为50)限制可以在队列中等待的挂起连接的最大数量。如果等到的队列长度大于这个数值,那么他们将被拒绝了连接。更糟糕的是,我们假装实现了HTTP / 1.1,因为HTTP /1.1它在默认情况下使用持久连接。在客户端断开连接之前,我们保持TCP / IP连接打开以防万一,并阻塞新客户端。

现在,回到我们的客户端连接,我们首先必须读取整个请求(即readFullRequest方法),然后编写响应。这两种操作都有潜在的阻塞,并受到网络缓慢和拥塞的影响。如果一个客户端建立了连接,但在发送请求前等待几秒钟,所有其他客户端都必须跟着等待。对于所有传入的连接,只有一个线程来处理是不太具有可伸缩的,我们几乎连C1(一个并发连接)问题都没有解决 - -。

附录A包含源代码和关于其他阻塞服务器的讨论。与其花更多时间分析非可伸缩的阻塞体系结构,不如简单地总结一下,这样我们就可以一边快速的进行基准测试,一边比较快慢。

在第327页的“fork() Procedure in C Language”中,您将会找到使用fork()编写的C语言的简单服务器的源代码。尽管表面上简单,为每个客户端连接(特别是短时间的客户机连接)提供了一个新的进程,但是这给操作系统带来了很大的负担。每个进程需要相当多的内存,初始启动需要一些时间。还有成千上万个进程无时无刻的开始和停止,不必要地占用系统资源。

ThreadPerConnection(见第329页上的“Thread per Connection”)展示了如何实现这样一个阻塞服务器:该服务器为每个客户机连接创建一个新线程。这可能会很好地扩展,但是这样的实现在C中会遇到与fork()相同的问题:启动一个新线程需要一些时间和资源,这对于短时间的连接是特别浪费的。此外,我们没有限制同时运行的客户机线程的最大数量。当你在计算机系统中没有限制某些东西时,这个限制将会被应用到最坏的和最意想不到的地方。例如,我们的程序将变得不稳定,并最终在数千个并发连接的情况下发生OutOfMemoryError崩溃。

ThreadPool(参见第331页上的“Thread Pool of Connections”)也使用了一个连接一个线程的模式,但是当客户端断开连接时线程会被循环使用,这样我们就不会为每个客户机支付线程预热的代价。这差不多就是所有受欢迎的servlet容器,比如Tomcat和Jetty工作原理,默认情况下,在一个线程池中管理100到200个线程 。Tomcat有一个所谓的NIO连接器,它可以异步地处理套接字上的一些操作,但是在它们之上构建的servlet和框架的真正工作原理仍然是阻塞式。这意味着传统的应用程序本质上仅限于几千个连接,即使是构建在最好的现代Servlet容器上。

results matching ""

    No results matching ""