多线程服务
由于Java的内置多线程功能,多线程服务器相当容易实现。但并不是所有的服务器设计都是一样的。
本文将介绍不同的服务器设计,并讨论它们的优缺点。
在Java中多线程服务器上的这种跟踪仍在进行中。
单线程
本文将展示如何在Java中实现单线程服务器。单线程服务器不是服务器的最佳设计,但是代码很好地说明了服务器的生命周期。多线程服务器上的以下文本将构建在这个代码模板上。
代码示例
下面是一个简单的单线程服务器:
- Server.java
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()
public static void main(String[] args) throws IOException {
SingleThreadServer server = new SingleThreadServer(9000);
new Thread(server).start();
}
- 测试
使用浏览器,打开 http://localhost:9000/。可以看到浏览器对应的内容。
但是有个问题,后天的日志却是执行 2 遍的?
服务器启动,端口号: 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
线程循环
核心的服务器代码:
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。
对于Java中实现的大多数服务器来说,这个循环几乎是相同的。
将单线程服务器与多线程服务器区分开来的是,单线程服务器在接受客户机连接的同一线程中处理传入的请求。多线程服务器将连接传递给处理请求的工作线程。
在接受客户机连接的同一线程中处理传入请求不是一个好主意。
客户机只能在服务器位于 serverSocket.accept()
方法调用中时连接到服务器。
侦听线程在 serverSocket.accept() 调用之外花费的时间越长,客户机被拒绝访问服务器的可能性就越大。
这就是多线程服务器将传入的连接传递给工作线程的原因,工作线程将处理请求。
这样,侦听线程在 serverSocket.accept() 调用之外花费的时间就越少。
多线程
单线程的例子中,当一个新的 clientSocket 到来时,是直接在当前线程中进行处理的,这会造成其他的 clientSocket 等待。
WorkerRunnable
我们建立一个线程类,单独处理每一个新的 socket 链接。
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();
}
}
}
服务端的调整
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()
服务的启动
public static void main(String[] args) throws IOException {
MultiThreadServer server = new MultiThreadServer(9000);
new Thread(server).start();
}
优点
多线程服务器与单线程服务器相比的优点总结如下:
-
花费在accept()调用之外的时间更少。
-
长时间运行的客户机请求不会阻塞整个服务器
如前所述,线程在此方法调用中花费的时间越多,服务器的响应能力就越强。只有当侦听线程位于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