使用Socket实现HttpServer(二)
前面我们使用 Socket 实现了一个简易的 HttpServer,接下来我们将对我们的服务器进行优化:
- 面向对象的封装
- 优化线程模型(引入多线程)
- Request/Response 对象抽象
Step1(面向对象的封装)
对我们之前所写的 HttpServer 进行面向对象封装。
主要封装了 listen() 和 accept() 方法。
package com.fengsir.network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.function.Function;
/**
* @Author FengZeng
* @Date 2022-01-24 13:35
* @Description TODO
*/
public class Step1Server {
ServerSocket socketServer;
Function<String, String> handler;
public Step1Server(Function<String, String> handler) {
this.handler = handler;
}
/**
* @param port listen port
* @throws IOException
*/
public void listen(int port) throws IOException {
socketServer = new ServerSocket(port);
while (true) {
accept();
}
}
private void accept() throws IOException {
Socket socket = socketServer.accept();
System.out.println("a socket created");
// 拿到 socket 的请求内容,也就是 inputStream,封装成 bufferedReader,方便读取
InputStream inputStream = socket.getInputStream();
BufferedReader bfReader = new BufferedReader(new InputStreamReader(inputStream));
// 按行读取,把内容放到 stringBuilder 中
StringBuilder requestBuilder = new StringBuilder();
String line = "";
// 由于有时候 readLine() 会读取到 null,就会报错 nullException,
// 所以在这里进行了一点修改
while (true) {
line = bfReader.readLine();
if (line == null || line.isBlank()) {
break;
}
requestBuilder.append(line);
}
// 打印请求内容
String request = requestBuilder.toString();
System.out.println(request);
// 封装 response
BufferedWriter bfWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String response = this.handler.apply(request);
bfWriter.write(response);
// 一定要 flush
bfWriter.flush();
socket.close();
}
public static void main(String[] args) throws IOException {
Step1Server step1Server = new Step1Server(res->{
return "HTTP/1.1 200 ok\n\nGood!\n";
});
step1Server.listen(8000);
}
}
到这里也没有什么太大的问题,不过我们的服务器还是单线程的,如果只是单纯的返回一个字符串,我们的架构也没有问题,如果此时每个请求会去做别的处理,比如去查一下数据库,那么我们的服务器性能就很低了,还会出现服务器拒绝的错误(因为操作系统的 pending Queue 已经满了),所以我们需要进一步优化。
Step2(引入多线程)
这里引入了多线程,不再是一个线程单独在跑了,试了一下 Jemeter 的压测,10000个线程的并发就会把内存拉得很高,主要还是因为创建了线程后 Java 的垃圾回收还没有启动,所以现在一般不会用这种方式去创建线程,后面会引入线程池来优化。
package com.fengsir.network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.function.Function;
/**
* @Author FengZeng
* @Date 2022-01-24 14:01
* @Description TODO
*/
public class Step2Server {
ServerSocket socketServer;
Function<String, String> handler;
public Step2Server(Function<String, String> handler) {
this.handler = handler;
}
/**
* @param port listen port
* @throws IOException
*/
public void listen(int port) throws IOException {
socketServer = new ServerSocket(port);
while (true) {
accept();
}
}
private void accept() throws IOException {
// 如果不把 accept 提到这里,那么上面的 while循环会一直创建线程
// 这也体现了 accept() 是 blocking 的
Socket socket = socketServer.accept();
System.out.println("a socket created");
new Thread(()->{
try {
handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private void handler(Socket socket) throws IOException {
// 拿到 socket 的请求内容,也就是 inputStream,封装成 bufferedReader,方便读取
InputStream inputStream = socket.getInputStream();
BufferedReader bfReader = new BufferedReader(new InputStreamReader(inputStream));
// 按行读取,把内容放到 stringBuilder 中
StringBuilder requestBuilder = new StringBuilder();
String line = "";
while (true) {
line = bfReader.readLine();
if (line == null || line.isBlank()) {
break;
}
requestBuilder.append(line);
}
// 打印请求内容
String request = requestBuilder.toString();
System.out.println(request);
// 封装 response
BufferedWriter bfWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String response = this.handler.apply(request);
bfWriter.write(response);
// 一定要 flush
bfWriter.flush();
socket.close();
}
public static void main(String[] args) throws IOException {
Step2Server step2Server = new Step2Server(res->{
return "HTTP/1.1 200 ok\n\nGood!\n";
});
step2Server.listen(8000);
}
}
Step3(封装Request/Response)
由于 step2 中,我们使用的是 Function 对象来实现的请求和响应,我们进一步抽象封装,创建 IHandlerInterface 接口:
package com.fengsir.network.step3;
import java.io.IOException;
/**
* @Author FengZeng
* @Date 2022-01-24 14:26
* @Description TODO
*/
@FunctionalInterface
public interface IHandlerInterface {
/**
* 处理响应
* @param request request
* @param response response
* @throws IOException
*/
void handler(Request request, Response response) throws IOException;
}
然后我们创建 Request 类:
package com.fengsir.network.step3;
import org.apache.commons.httpclient.HttpParser;
import java.io.*;
import java.net.Socket;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author FengZeng
* @Date 2022-01-24 14:27
* @Description TODO
*/
public class Request {
// 使用正则表达式提取请求的方法
static Pattern methodRegex = Pattern.compile("(GET|PUT|POST|DELETE|OPTIONS|TARCE|HEAD)");
private final String body;
private final String method;
private final HashMap<String, String> headers;
public String getBody() {
return body;
}
public String getMethod() {
return method;
}
public HashMap<String, String> getHeaders() {
return headers;
}
public Request(Socket socket) throws IOException {
// 这是 DataInputStream 和 InputStream 的区别
// DataInputStream -> primitives(char,float)
// InputStream -> bytes
DataInputStream iptStream = new DataInputStream(socket.getInputStream());
BufferedReader bfReader = new BufferedReader(new InputStreamReader(iptStream));
// 从第一行读取请求的方法,HttpParser 是我引入的 commons-httpClient 包中的对象
String methodLine = HttpParser.readLine(iptStream,"UTF-8");
Matcher matcher = methodRegex.matcher(methodLine);
matcher.find();
String method = matcher.group();
// 解析请求头
// Content-Type: xxxx
var headers = HttpParser.parseHeaders(iptStream, "UTF-8");
HashMap<String, String> headMap = new HashMap<>();
for (var h : headers) {
headMap.put(h.getName(), h.getValue());
}
// 解析请求体
var bufferReader = new BufferedReader(new InputStreamReader(iptStream));
var body = new StringBuilder();
char[] buffer = new char[1024];
while (iptStream.available() > 0) {
bufferReader.read(buffer);
body.append(buffer);
}
this.body = body.toString();
this.method = method;
this.headers = headMap;
}
}
创建 Response 类:
package com.fengsir.network.step3;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.HashMap;
/**
* @Author FengZeng
* @Date 2022-01-24 14:27
* @Description TODO
*/
public class Response {
Socket socket;
private int status;
static HashMap<Integer, String> codeMap;
public Response(Socket socket) {
this.socket = socket;
if (codeMap == null) {
codeMap = new HashMap<>();
codeMap.put(200, "ok");
}
}
/**
* http 标准响应
* @param msg message
* @throws IOException
*/
public void send(String msg) throws IOException {
this.status = 200;
var resp = "HTTP/1.1 " + this.status + " " + codeMap.get(this.status) + "\n";
resp += "\n";
resp += msg;
this.sendRaw(resp);
}
/**
* 发送原始的响应
* @param msg message
* @throws IOException
*/
public void sendRaw(String msg) throws IOException {
var bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(msg);
bufferedWriter.flush();
bufferedWriter.close();
}
}
到此,我们再重构一下主函数:
package com.fengsir.network.step3;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @Author FengZeng
* @Date 2022-01-24 14:28
* @Description TODO
*/
public class Step3Server {
ServerSocket serverSocket;
IHandlerInterface httpHandler;
public Step3Server(IHandlerInterface httpHandler) {
this.httpHandler = httpHandler;
}
public void listen(int port) throws IOException {
serverSocket = new ServerSocket(port);
while (true) {
this.accept();
}
}
private void accept() throws IOException {
Socket socket = serverSocket.accept();
new Thread(() -> {
try {
this.handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
private void handler(Socket socket) throws IOException {
Request request = new Request(socket);
Response response = new Response(socket);
this.httpHandler.handler(request, response);
}
public static void main(String[] args) throws IOException {
var server = new Step3Server((req,resp)->{
System.out.println(req.getHeaders());
resp.send("Greetings\n");
});
server.listen(8000);
}
}
可以看到,我们现在的主函数已经很简短了,其实大多数的框架都是从这么一个过程过来的,所以抽象,封装,重构能力是很重要的,优秀的程序员在coding的时候,都会去往这几个方面去想,下一章我打算引入NIO继续优化我们的 Http Server