【Java开发】网络编程

基础知识

域名解析服务器 DNS 负责把域名翻译成对应的 IP,客户端再根据 IP 地址访问服务器。可以使用nslookup查看域名对应的IP地址。

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

互联网实际使用的 TCP/IP 模型并不是对应到 OSI 的 7 层模型,而是大致对应 OSI 的 5 层模型:

常见协议

IP 协议是一个分组交换,它不保证可靠传输。而 TCP 协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP 协议是建立在 IP 协议之上的,简单地说,IP 协议只负责发数据包,不保证顺序和正确性,而 TCP 协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP 协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP 协议允许双向通信,即通信双方可以同时发送和接收数据。

TCP 协议也是应用最广泛的协议,许多高级协议都是建立在 TCP 协议之上的,例如 HTTP、SMTP 等。
TCP(传输控制协议)和 IP(互联网协议)通常一起使用,被称为 TCP/IP 协议。

TCP/IP 协议栈分为四层:应用层、传输层、网络层和链路层。IP 协议位于网络层,负责将数据包从源主机路由到目标主机;TCP 协议位于传输层,负责在源主机和目标主机之间建立可靠的连接并确保数据的有序传输。

UDP 协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为 UDP 协议在通信前不需要建立连接,因此它的传输效率比 TCP 高,而且 UDP 协议比 TCP 协议要简单得多。

Java Socket

套接字(Socket)是一个抽象层,应用程序可以通过它发送或接收数据;就像操作文件那样可以打开、读写和关闭。套接字允许应用程序将 I/O 应用于网络中,并与其他应用程序进行通信。网络套接字是 IP 地址与端口的组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try (Socket socket = new Socket("bbs.newsmth.net", 23);) {
InputStream is = socket.getInputStream();
Scanner scanner = new Scanner(is, "gbk");

while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}

} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

1)建立套接字连接非常简单,只需要一行代码:

1
Socket socket = new Socket(host, port)

host 为主机名,port 为端口号(23 为默认的 telnet 端口号)。如果无法确定主机的 IP 地址,则抛出 UnknownHostException 异常;如果在创建套接字时发生 IO 错误,则抛出 IOException 异常。

需要注意的是,套接字在建立的时候,如果远程主机不可访问,这段代码就会阻塞很长时间,直到底层操作系统的限制而抛出异常。所以一般会在套接字建立后设置一个超时时间。

1
2
Socket socket = new Socket(...);
socket.setSoTimeout(10000); // 单位为毫秒

2)套接字连接成功后,可以通过 java.net.Socket 类的 getInputStream() 方法获取输入流。有了 InputStream 对象后,可以借助文本扫描器类(Scanner)将其中的内容打印出来。

1
2
3
4
5
6
7
InputStream is = socket.getInputStream();
Scanner scanner = new Scanner(is, "gbk");

while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}

ServerSocket 实例

接下来,我们模拟一个远程服务,通过 java.net.ServerSocket 实现。代码示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();

Scanner scanner = new Scanner(is))
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "gbk"), true);
pw.println("你好啊,欢迎关注「沉默王二」 公众号,回复关键字「2048」 领取程序员进阶必读资料包");

boolean done = false;
while (!done && scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);

if ("2048".equals(line)) {
done = true;
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

1)建立服务器端的套接字也比较简单,只需要指定一个能够独占的端口号就可以了(0~1023 这些端口都已经被系统预留了)。

1
ServerSocket server = new ServerSocket(8888);

2)调用 ServerSocket 对象的 accept() 等待客户端套接字的连接请求。一旦监听到客户端的套接字请求,就会返回一个表示连接已建立的 Socket 对象,可以从中获取到输入流和输出流。

1
2
3
Socket socket = server.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();

客户端套接字发送的所有信息都会包裹在服务器端套接字的输入流中;而服务器端套接字发送的所有信息都会包裹在客户端套接字的输出流中。

3)服务器端可以通过以下代码向客户端发送消息。

1
2
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "gbk"), true);
pw.println("你好啊,欢迎关注「沉默王二」 公众号,回复关键字「2048」 领取程序员进阶必读资料包");

4)服务器端可以通过以下代码读取客户端发送过来的消息。

1
2
3
4
5
6
7
8
9
10
Scanner scanner = new Scanner(is);
boolean done = false;
while (!done && scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);

if ("2048".equals(line)) {
done = true;
}
}

运行该服务后,可以通过 telnet localhost 8888 命令连接该远程服务,不出所料,你将会看到以下信息。

04、为多个客户端服务

非常遗憾的是,上面的例子中,服务器端只能为一个客户端服务——这不符合服务器端一对多的要求。
优化方案也非常简单(你应该也能想得到):服务器端接收到客户端的套接字请求时,可以启动一个线程来处理,而主程序继续等待下一个连接。代码示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try (ServerSocket server = new ServerSocket(8888)) {

while (true) {
Socket socket = server.accept();
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// 套接字处理程序
}
});
thread.start();

}
} catch (IOException e) {
e.printStackTrace();
}

线程内部(run(){} 方法里)用来处理套接字,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
Scanner scanner = new Scanner(is);

// 其他代码省略
// 向客户端发送消息
// 读取客户端发送过来的消息
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}

服务器端代码优化后重新运行,你就可以通过 telnet 命令测试了。打开一个命令行窗口输入 telnet localhost 8888,再打开一个新的命令行窗口输入 telnet localhost 8888,多个窗口都可以和服务器端进行通信,除非服务器端代码中断运行。

05、加入多线程

多线程我们后面会详细讲,这里就主要是写个例子,好让大家感觉更有趣一些,其实也非常简单。

来看服务端:

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
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiThreadedServer {
public static void main(String[] args) throws IOException {
int port = 12345;
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server is listening on port " + port);

while (true) {
Socket socket = serverSocket.accept();
System.out.println("Client connected");
new ClientHandler(socket).start();
}
}
}
class ClientHandler extends Thread {
private Socket socket;

public ClientHandler(Socket socket) {
this.socket = socket;
}

public void run() {
try {
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));

OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);

String line;
while ((line = reader.readLine()) != null) {
System.out.println("Received: " + line);
writer.println("Server: " + line);
}

socket.close();
} catch (IOException e) {
System.out.println("Client disconnected");
}
}
}

在这个示例中,我们使用了一个 ClientHandler 类,该类继承自 Thread 类。这使得每个客户端连接都可以在单独的线程中处理,从而允许服务器同时处理多个客户端连接。当一个新客户端连接到服务器时,服务器会创建一个新的 ClientHandler 对象,并使用 start() 方法启动线程。ClientHandler 类的 run() 方法包含处理客户端请求的逻辑。

来看客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.*;
import java.net.Socket;

public class Client {
public static void main(String[] args) throws IOException {
String hostname = "localhost";
int port = 12345;

Socket socket = new Socket(hostname, port);
System.out.println("Connected to the server");

InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));

OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);

writer.println("Hello, server!");
String response = reader.readLine();
System.out.println("Server response: " + response);

socket.close();
}
}

08、DatagramSocket 实例

DatagramSocket 类是 Java 中实现 UDP 协议的核心类。与基于 TCP 的 Socket 和 ServerSocket 类不同,DatagramSocket 类提供了无连接的通信服务,发送和接收数据包。由于无需建立连接,UDP 通常比 TCP 更快,但可能不如 TCP 可靠。
以下是一个简单的 DatagramSocket 示例,展示了如何使用 UDP 协议在客户端和服务器之间发送和接收消息。

服务器端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServer {
public static void main(String[] args) throws IOException {
int port = 12345;
DatagramSocket serverSocket = new DatagramSocket(port);
System.out.println("Server is listening on port " + port);

byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

serverSocket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received: " + message);

serverSocket.close();
}
}

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
import java.net.*;

public class UDPClient {
public static void main(String[] args) throws IOException {
String hostname = "localhost";
int port = 12345;

InetAddress address = InetAddress.getByName(hostname);
DatagramSocket clientSocket = new DatagramSocket();

String message = "Hello, server!";
byte[] buffer = message.getBytes();

DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);
clientSocket.send(packet);
System.out.println("Message sent");

clientSocket.close();
}
}

在这个示例中,服务器端创建一个 DatagramSocket 对象并监听端口 12345。然后,它创建一个 DatagramPacket 对象,用于存储接收到的数据包。serverSocket.receive(packet) 方法阻塞,直到收到一个数据包。收到数据包后,服务器从数据包中提取并打印消息。
客户端首先解析服务器的 IP 地址,然后创建一个 DatagramSocket 对象。接着,客户端创建一个包含要发送消息的 DatagramPacket 对象,并指定目标地址和端口。最后,客户端通过调用 clientSocket.send(packet) 方法发送数据包。

Java Socket实现HTTP服务器

HTTP 协议

ServerSocket 走的是 TCP 协议,HTTP 协议本身是在 TCP 协议之上的一层。

TCP 是一种面向连接的、可靠的、基于字节流的传输层协议。TCP 在两个网络节点之间提供了一条可靠的通信信道,确保数据在传输过程中不会丢失、重复或乱序。TCP 使用握手过程建立连接,通过确认和重传机制确保数据可靠传输,并使用流量控制和拥塞控制算法来优化网络性能。

HTTP 是一个用于在 Web 浏览器和 Web 服务器之间传输超文本、图像、视频和其他媒体资源的应用层协议。HTTP 使用请求-响应模型,即客户端(通常是 Web 浏览器)发送请求给服务器,服务器处理请求并返回响应。HTTP 协议定义了一组方法(如 GET、POST、PUT、DELETE 等),用于指定请求的类型和目的。此外,HTTP 协议还定义了一组状态代码(如 200、404、500 等),用于表示响应的结果。

HTTP 协议依赖于 TCP 协议来传输数据。当 Web 浏览器向 Web 服务器发送 HTTP 请求时,它首先使用 TCP 协议与服务器建立连接。一旦连接建立,HTTP 请求消息会被封装在 TCP 数据包中,然后通过 TCP 信道发送给服务器。服务器收到 TCP 数据包后,解包提取 HTTP 请求消息,处理请求并生成 HTTP 响应消息。最后,HTTP 响应消息被封装在 TCP 数据包中,并通过相同的 TCP 信道发送回客户端。客户端收到 TCP 数据包后,解包提取 HTTP 响应消息并显示给用户。

请求消息
HTTP 请求消息由请求行(Request Line)、请求头(Request Headers)、空行(Empty Line)、请求体(Request Body,可选)几个部分组成。

①、请求行又包含三个部分,HTTP 方法(例如 GET, POST, PUT, DELETE 等)、请求的目标 URL(通常是相对 URL,但也可以是绝对 URL)、HTTP 版本(例如 HTTP/1.1 或 HTTP/2),这些部分用空格分隔,例如:

1
GET /index.html HTTP/1.1

②、请求头是一系列以键值对表示的元数据,用于描述请求的附加信息。每个请求头占一行,键和值之间用冒号(:)分隔。请求头包含诸如 Host、User-Agent、Content-Type、Content-Length、Accept 等信息。例如:

1
2
3
Host: www.tobebetterjavaer.com
User-Agent: Mozilla/5.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

③、请求头和请求体之间有一个空行,表示请求头的结束。

④、对于某些 HTTP 方法(例如 POST、PUT 等),还可以在请求消息中包含请求体。请求体用于传输要发送给服务器的数据。请求体的格式和内容取决于 Content-Type 请求头的值。

例如,当提交 HTML 表单时,请求体可能如下所示:

1
username=沉默王二&password=123456

将这些部分放在一起,就构成了一个完整的 HTTP 请求消息:

1
2
3
4
5
6
7
8
POST /login HTTP/1.1
Host: Host: www.tobebetterjavaer.com
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

username=沉默王二&password=123456

我用一张思维导图来表示下:

响应消息
一个典型的 HTTP 响应消息由三部分组成:状态行(Status Line)、响应头(Response Headers)、响应体(Response Body)。

不管是请求消息还是响应消息,都可以划分为三部分

  • 第一行:状态行
  • 第二行到第一个空行:header(请求头/相应头)
  • 剩下所有:正文

HTTP 服务器设计

接下来进入正题,基于 Socket 创建一个 HTTP 服务器,使用 Socket 基本没啥太大的问题,我们需要额外关注以下两点:

  • 对请求数据进行解析
  • 封装返回结果

a. 请求数据解析

我们从 Socket 中拿到所有的数据,然后解析为对应的 HTTP 请求,我们先定义个 Request 对象,内部保存一些基本的 HTTP 信息,接下来重点就是将 Socket 中的所有数据都捞出来,封装为 request 对象。

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
@Data
public static class Request {
/**
* 请求方法 GET/POST/PUT/DELETE/OPTION...
*/
private String method;
/**
* 请求的uri
*/
private String uri;
/**
* HTTP版本
*/
private String version;

/**
* 请求头
*/
private Map<String, String> headers;

/**
* 请求参数相关
*/
private String message;
}

根据前面的 HTTP 协议介绍,解析过程如下,我们先看请求行的解析过程。

请求行,包含三个基本要素:请求方法 + URI + HTTP 版本,用空格进行分割,所以解析代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据标准的HTTP协议,解析请求行
*
* @param reader
* @param request
*/
private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {
String[] strs = StringUtils.split(reader.readLine(), " ");
assert strs.length == 3;
request.setMethod(strs[0]);
request.setUri(strs[1]);
request.setVersion(strs[2]);
}

请求头的解析,从第二行,到第一个空白行之间的所有数据,都是请求头;请求头的格式也比较清晰,形如 key:value, 具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 根据标准 HTTP 协议,解析请求头
*
* @param reader 读取请求头的 BufferedReader 对象
* @param request 存储请求信息的 Request 对象
* @throws IOException 当读取请求头信息时发生 I/O 异常时,将抛出该异常
*/
private static void decodeRequestHeader(BufferedReader reader, Request request) throws IOException {
// 创建一个 Map 对象,用于存储请求头信息
Map<String, String> headers = new HashMap<>(16);
// 读取请求头信息,每行都是一个键值对,以空行结束
String line = reader.readLine();
String[] kv;
while (!"".equals(line)) {
// 将每行请求头信息按冒号分隔,分别作为键和值存入 Map 中
kv = StringUtils.split(line, ":");
assert kv.length == 2;
headers.put(kv[0].trim(), kv[1].trim());
line = reader.readLine();
}
// 将解析出来的请求头信息存入 Request 对象中
request.setHeaders(headers);
}

最后就是正文的解析了,这一块需要注意一点,正文可能为空,也可能有数据;有数据时,我们要如何把所有的数据都取出来呢?

先看具体实现如下:

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
/**
* 根据标注HTTP协议,解析正文
*
* @param reader 输入流读取器,用于读取请求中的数据
* @param request Request 对象,表示 HTTP 请求
* @throws IOException 当发生 I/O 错误时抛出
*/
private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {
// 从请求头中获取 Content-Length,如果没有,则默认为 0
int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));

// 如果 Content-Length 为 0,表示没有请求正文,直接返回。
// 例如 GET 和 OPTIONS 请求通常不包含请求正文
if (contentLen == 0) {
return;
}

// 根据 Content-Length 创建一个字符数组来存储请求正文
char[] message = new char[contentLen];

// 使用 BufferedReader 读取请求正文
reader.read(message);

// 将字符数组转换为字符串,并将其设置为 Request 对象的 message
request.setMessage(new String(message));
}

注意上面我的使用姿势,首先是根据请求头中的Content-Type的值,来获得正文的数据大小,因此我们获取的方式是创建一个这么大的char[] 数组来读取流中所有数据,如果我们的数组比实际的小,则读不完;如果大,则数组中会有一些空的数据;

最后将上面的几个解析封装一下,完成 request 解析:

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
/**
* HTTP 请求可以分为三部分:
* 1. 请求行:包括请求方法、URI 和 HTTP 协议版本
* 2. 请求头:从第二行开始,直到一个空行为止
* 3. 消息正文:紧跟在空行后的所有内容,长度由请求头中的 Content-Length 决定
*
* 本方法将 InputStream 中的 HTTP 请求数据解析为一个 Request 对象
*
* @param reqStream 包含 HTTP 请求数据的输入流
* @return 一个表示 HTTP 请求的 Request 对象
* @throws IOException 当发生 I/O 错误时抛出
*/
public static Request parse2request(InputStream reqStream) throws IOException {
// 使用 BufferedReader 和 InputStreamReader 读取输入流中的数据
BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));

// 创建一个新的 Request 对象
Request httpRequest = new Request();

// 解析请求行并设置到 Request 对象中
decodeRequestLine(httpReader, httpRequest);

// 解析请求头并设置到 Request 对象中
decodeRequestHeader(httpReader, httpRequest);

// 解析消息正文并设置到 Request 对象中
decodeRequestMessage(httpReader, httpRequest);

// 返回解析后的 Request 对象
return httpRequest;
}

接下来,是请求结果的封装,给一个简单的进行演示:

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
/**
* Response 类表示一个 HTTP 响应,包括版本、状态码、状态信息、响应头和响应正文。
*/
@Data
public static class Response {
private String version;
private int code;
private String status;
private Map<String, String> headers;
private String message;
}

/**
* 根据给定的 Request 对象和响应字符串构建一个 HTTP 响应。
*
* @param request 用于构建响应的 Request 对象
* @param response 响应字符串
* @return 一个表示 HTTP 响应的字符串
*/
public static String buildResponse(Request request, String response) {
// 创建一个新的 Response 对象,并设置版本、状态码和状态信息
Response httpResponse = new Response();
httpResponse.setCode(200);
httpResponse.setStatus("ok");
httpResponse.setVersion(request.getVersion());

// 设置响应头
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Content-Length", String.valueOf(response.getBytes().length));
httpResponse.setHeaders(headers);

// 设置响应正文
httpResponse.setMessage(response);

// 构建响应字符串
StringBuilder builder = new StringBuilder();
buildResponseLine(httpResponse, builder);
buildResponseHeaders(httpResponse, builder);
buildResponseMessage(httpResponse, builder);
return builder.toString();
}

/**
* 构建响应行,包括版本、状态码和状态信息。
*
* @param response 用于构建响应行的 Response 对象
* @param stringBuilder 用于拼接响应字符串的 StringBuilder 对象
*/
private static void buildResponseLine(Response response, StringBuilder stringBuilder) {
stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ")
.append(response.getStatus()).append("\n");
}

/**
* 构建响应头。
*
* @param response 用于构建响应头的 Response 对象
* @param stringBuilder 用于拼接响应字符串的 StringBuilder 对象
*/
private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {
for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {
stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
}
stringBuilder.append("\n");
}

/**
* 构建响应正文。
*
* @param response 用于构建响应正文的 Response 对象
* @param stringBuilder 用于拼接响应字符串的 StringBuilder 对象
*/
private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {
stringBuilder.append(response.getMessage());
}

b. 请求任务 HttpTask

每个请求,单独分配一个任务来干这个事情,就是为了支持并发,对于 ServerSocket 而言,接收到了一个请求,那就创建一个 HttpTask 任务来实现 HTTP 通信。
那么这个 httptask 干啥呢?

  • 从请求中捞数据
  • 响应请求
  • 封装结果并返回
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
/**
* HttpTask 类实现了 Runnable 接口,用于处理一个 HTTP 请求。
* 当在一个线程中执行时,该任务将处理一个 Socket 连接上的 HTTP 请求,
* 并发送响应消息。
*/
public class HttpTask implements Runnable {
// 用于处理 HTTP 请求的 Socket
private Socket socket;

/**
* 构造一个新的 HttpTask,用于处理指定的 Socket 连接。
*
* @param socket 用于处理 HTTP 请求的 Socket
*/
public HttpTask(Socket socket) {
this.socket = socket;
}

/**
* 实现 Runnable 接口的 run 方法,用于处理 HTTP 请求并发送响应消息。
*/
@Override
public void run() {
// 检查 socket 是否为 null,如果为 null 则抛出异常
if (socket == null) {
throw new IllegalArgumentException("socket can't be null.");
}

try {
// 获取 Socket 的输出流,并创建一个 PrintWriter 对象
OutputStream outputStream = socket.getOutputStream();
PrintWriter out = new PrintWriter(outputStream);

// 从 Socket 的输入流中解析 HTTP 请求
HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream());

try {
// 根据请求结果进行响应,省略返回
String result = null;

// 根据请求和结果构建 HTTP 响应
String httpRes = HttpMessageParser.buildResponse(httpRequest, result);

// 将 HTTP 响应发送到客户端
out.print(httpRes);
} catch (Exception e) {
// 如果发生异常,构建一个包含异常信息的 HTTP 响应
String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString());
out.print(httpRes);
}

// 刷新输出流,确保响应消息被发送
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭 Socket 连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

c. HTTP 服务搭建

前面的基本上把该干的事情都干了,剩下的就简单了,创建ServerSocket,绑定端口接收请求,我们在线程池中跑这个 HTTP 服务

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
public class BasicHttpServer {
// 创建一个单线程执行器,用于启动 HTTP 服务器
private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor();
// 创建一个线程池,用于处理来自客户端的 HTTP 请求
private static ExecutorService taskExecutor;
// 设置服务器监听的端口号
private static int PORT = 8999;

// 启动 HTTP 服务器的方法
static void startHttpServer() {
// 获取处理器可用核心数,用于设置线程池大小
int nThreads = Runtime.getRuntime().availableProcessors();
// 初始化线程池,设置线程池大小,队列大小和丢弃策略
taskExecutor =
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.DiscardPolicy());

// 循环尝试启动服务器,如果启动失败,则等待10秒后重试
while (true) {
try {
ServerSocket serverSocket = new ServerSocket(PORT);
bootstrapExecutor.submit(new ServerThread(serverSocket));
break;
} catch (Exception e) {
try {
// 重试,等待 10 秒
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}

// 关闭启动执行器
bootstrapExecutor.shutdown();
}

// HTTP 服务器主要任务类
private static class ServerThread implements Runnable {
// 保存传递给构造函数的 ServerSocket 实例
private ServerSocket serverSocket;

// 构造函数
public ServerThread(ServerSocket s) throws IOException {
this.serverSocket = s;
}

// 任务主体方法
@Override
public void run() {
while (true) {
try {
// 等待客户端连接
Socket socket = this.serverSocket.accept();
// 创建一个 HttpTask 实例,将 Socket 实例作为参数传递
HttpTask eventTask = new HttpTask(socket);
// 将 HttpTask 提交给 taskExecutor 执行
taskExecutor.submit(eventTask);
} catch (Exception e) {
e.printStackTrace();
try {
// 如果发生异常,等待 1 秒后继续尝试
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
}
}

这段代码是一个简单的 HTTP 服务器实现。以下是关于这个 HTTP 服务器的主要组件和功能的详细解释:
1、bootstrapExecutor:一个单线程的 ExecutorService,用于执行 HTTP 服务器的启动任务。
2、taskExecutor:一个线程池,用于处理来自客户端的 HTTP 请求。线程池的大小等于处理器可用核心数,队列大小为100,使用 DiscardPolicy 丢弃策略。
3、PORT:服务器侦听的端口号,默认为 8999。
4、startHttpServer() 方法:
- a.创建一个线程池 taskExecutor 用于处理 HTTP 请求。
- b.在一个循环中,尝试创建一个 ServerSocket 实例并绑定到指定端口。如果失败,则等待 10 秒后重试。
- c.当成功创建 ServerSocket 实例后,将其作为参数提交给 bootstrapExecutor 执行 ServerThread 任务。
- d.关闭 bootstrapExecutor。
5、ServerThread 类实现了 Runnable 接口,它是 HTTP 服务器的主要任务:
- a.serverSocket 成员变量:保存传递给构造函数的 ServerSocket 实例。
- b.run() 方法:
- 在一个无限循环中,调用 serverSocket.accept() 方法等待客户端的连接。
- 当接受到一个新的客户端连接时,创建一个 HttpTask 实例,将 Socket 实例作为参数传递。
- 将 HttpTask 提交给 taskExecutor 执行。

这个 HTTP 服务器的主要逻辑是:使用一个线程来监听客户端连接,当有新的客户端连接时,创建一个 HttpTask 来处理客户端的 HTTP 请求,并将这个任务提交给线程池 taskExecutor 执行。这样可以实现多个客户端请求的并发处理。
到这里,一个基于 Socket 实现的 HTTP 服务器基本上就搭建完了,接下来就可以进行测试了