多线程服务

由于Java的内置多线程功能,多线程服务器相当容易实现。但并不是所有的服务器设计都是一样的。

本文将介绍不同的服务器设计,并讨论它们的优缺点。

在Java中多线程服务器上的这种跟踪仍在进行中。

单线程

本文将展示如何在Java中实现单线程服务器。单线程服务器不是服务器的最佳设计,但是代码很好地说明了服务器的生命周期。多线程服务器上的以下文本将构建在这个代码模板上。

代码示例

下面是一个简单的单线程服务器:

  • Server.java
  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SingleThreadServer implements Runnable { /** * 端口号 */ private final int serverPort; /** * 服务端 socket */ private final ServerSocket serverSocket; /** * 服务是否终止 */ private volatile boolean isStop; /** * 构造器 * * @param serverPort 服务端口号 * @throws IOException io 异常 */ public SingleThreadServer(int serverPort) throws IOException { this.serverPort = serverPort; this.serverSocket = new ServerSocket(this.serverPort); } @Override public void run() { this.isStop = false; System.out.println("服务器启动,端口号: " + this.serverPort); while (!isStop) { Socket clientSocket; try { clientSocket = this.serverSocket.accept(); System.out.println("新的 clientSocket: " + clientSocket); processClientRequest(clientSocket); } catch (IOException e) { if (isStop) { System.out.println("Server Stopped."); return; } throw new RuntimeException("Error accepting client connection", e); } catch (Exception e) { //log exception and go on to next request. e.printStackTrace(); } } } /** * 处理客户端请求 * * @param clientSocket 客户端 socket * @throws Exception if any */ private void processClientRequest(Socket clientSocket) throws Exception { try (InputStream input = clientSocket.getInputStream(); OutputStream output = clientSocket.getOutputStream()) { long time = System.currentTimeMillis(); String responseDocument = "<html><body>" + "Singlethreaded Server: " + time + "</body></html>"; String responseHeader = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Content-Length: " + responseDocument.length() + "\r\n\r\n"; output.write(responseHeader.getBytes()); output.write(responseDocument.getBytes()); System.out.println("Request processed: " + time); } } /** * 停止服务 */ public void stop() { try { this.isStop = true; this.serverSocket.close(); System.out.println("服务器关闭"); } catch (IOException e) { this.isStop = false; System.out.println("服务器关闭失败"); e.printStackTrace(); } } }
  • main()
  [java]
1
2
3
4
public static void main(String[] args) throws IOException { SingleThreadServer server = new SingleThreadServer(9000); new Thread(server).start(); }
  • 测试

使用浏览器,打开 http://localhost:9000/。可以看到浏览器对应的内容。

但是有个问题,后天的日志却是执行 2 遍的?

  [plaintext]
1
2
3
4
5
服务器启动,端口号: 9000 新的 clientSocket: Socket[addr=/0:0:0:0:0:0:0:1,port=51554,localport=9000] Request processed: 1537840937790 新的 clientSocket: Socket[addr=/0:0:0:0:0:0:0:1,port=51555,localport=9000] Request processed: 1537840937792

线程循环

核心的服务器代码:

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (!isStop) { Socket clientSocket; try { clientSocket = this.serverSocket.accept(); System.out.println("新的 clientSocket: " + clientSocket); processClientRequest(clientSocket); } catch (IOException e) { if (isStop) { System.out.println("Server Stopped."); return; } throw new RuntimeException("Error accepting client connection", e); } catch (Exception e) { //log exception and go on to next request. e.printStackTrace(); } }
  • 具体过程

简而言之,服务器所做的是:

  1. 等待客户端请求

  2. 处理客户端请求

  3. 重复从1。

对于Java中实现的大多数服务器来说,这个循环几乎是相同的。

将单线程服务器与多线程服务器区分开来的是,单线程服务器在接受客户机连接的同一线程中处理传入的请求。多线程服务器将连接传递给处理请求的工作线程。

在接受客户机连接的同一线程中处理传入请求不是一个好主意。

客户机只能在服务器位于 serverSocket.accept() 方法调用中时连接到服务器。

侦听线程在 serverSocket.accept() 调用之外花费的时间越长,客户机被拒绝访问服务器的可能性就越大。

这就是多线程服务器将传入的连接传递给工作线程的原因,工作线程将处理请求。

这样,侦听线程在 serverSocket.accept() 调用之外花费的时间就越少。

多线程

单线程的例子中,当一个新的 clientSocket 到来时,是直接在当前线程中进行处理的,这会造成其他的 clientSocket 等待。

WorkerRunnable

我们建立一个线程类,单独处理每一个新的 socket 链接。

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class WorkerRunnable implements Runnable { /** * 客户端套接字 */ private final Socket clientSocket; public WorkerRunnable(Socket clientSocket) { this.clientSocket = clientSocket; } @Override public void run() { try (InputStream input = clientSocket.getInputStream(); OutputStream output = clientSocket.getOutputStream()) { long time = System.currentTimeMillis(); String responseDocument = "<html><body>" + "Multi Server: " + time + "</body></html>"; String responseHeader = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Content-Length: " + responseDocument.length() + "\r\n\r\n"; output.write(responseHeader.getBytes()); output.write(responseDocument.getBytes()); System.out.println("Request processed: " + time); } catch (IOException e) { e.printStackTrace(); } } }

服务端的调整

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class MultiThreadServer implements Runnable { /** * 端口号 */ private final int serverPort; /** * 服务端 socket */ private final ServerSocket serverSocket; /** * 服务是否终止 */ private volatile boolean isStop; /** * 构造器 * * @param serverPort 服务端口号 * @throws IOException io 异常 */ public MultiThreadServer(int serverPort) throws IOException { this.serverPort = serverPort; this.serverSocket = new ServerSocket(this.serverPort); } @Override public void run() { this.isStop = false; System.out.println("服务器启动,端口号: " + this.serverPort); while (!isStop) { Socket clientSocket; try { clientSocket = this.serverSocket.accept(); System.out.println("新开一个线程,单独处理 socket 信息 clientSocket: " + clientSocket); new Thread(new WorkerRunnable(clientSocket)).start(); } catch (IOException e) { if (isStop) { System.out.println("Server Stopped."); return; } throw new RuntimeException("Error accepting client connection", e); } } } /** * 停止服务 */ public void stop() { try { this.isStop = true; this.serverSocket.close(); System.out.println("服务器关闭"); } catch (IOException e) { this.isStop = false; System.out.println("服务器关闭失败"); e.printStackTrace(); } } }
  • main()

服务的启动

  [java]
1
2
3
4
public static void main(String[] args) throws IOException { MultiThreadServer server = new MultiThreadServer(9000); new Thread(server).start(); }

优点

多线程服务器与单线程服务器相比的优点总结如下:

  1. 花费在accept()调用之外的时间更少。

  2. 长时间运行的客户机请求不会阻塞整个服务器

如前所述,线程在此方法调用中花费的时间越多,服务器的响应能力就越强。只有当侦听线程位于accept()调用内部时,客户机才能连接到服务器。否则客户端就会得到一个错误。

在单线程服务器中,长时间运行的请求可能使服务器长时间不响应。对于多线程服务器来说并非如此,除非长时间运行的请求占用所有CPU时间和/或网络带宽。

线程池

当然了,说到线程的执行,因为每次线程的资源分配都比较消耗资源。所以有线程池的概念。

多线程的其他代码不做改变,只需要 WorkerRunnable 在执行的时候,使用线程池即可。

优点

与多线程服务器相比,线程池服务器的优点是可以控制同时运行的线程的最大数量。这有一定的优势。

首先,如果请求需要大量的CPU时间、RAM或网络带宽,如果同时处理许多请求,这可能会减慢服务器的速度。例如,如果内存消耗导致服务器在磁盘内外交换内存,这将导致严重的性能损失。通过控制最大线程数,您可以将资源耗尽的风险降至最低,这一方面是由于处理请求所占用的内存有限,另一方面也是由于线程的限制和重用。每个线程也占用一定的内存,只是为了表示线程本身。

此外,并发执行多个请求会减慢所有处理的请求的速度。例如,如果您同时处理1 000个请求,每个请求需要1秒,那么所有请求将需要1 000秒来完成。如果你将请求排队,每次处理10个请求,前10个请求会在10秒后完成,后10个会在20秒后完成,等等。只有最后10个请求会在1000秒后完成。这为客户提供了更好的服务。

参考资料

http://tutorials.jenkov.com/java-multithreaded-servers/index.html