Implementing HttpServer with Socket (II)
Earlier, we implemented a simple HttpServer using Socket. Next, we will optimize our server:
- Object oriented encapsulation
- Optimized threading model (introduction of multithreading)
- Request/Response object abstraction
Step 1 (object oriented encapsulation)
Object oriented encapsulation of HttpServer we wrote before.
It mainly encapsulates the listen() and accept() methods.
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"); // Get the request content of the socket, that is, inputStream, and package it into bufferedReader for easy reading InputStream inputStream = socket.getInputStream(); BufferedReader bfReader = new BufferedReader(new InputStreamReader(inputStream)); // Read by line and put the content into stringBuilder StringBuilder requestBuilder = new StringBuilder(); String line = ""; // Because sometimes readLine() reads null, it will report an error nullException, // So a little modification has been made here while (true) { line = bfReader.readLine(); if (line == null || line.isBlank()) { break; } requestBuilder.append(line); } // Print request content String request = requestBuilder.toString(); System.out.println(request); // Encapsulate response BufferedWriter bfWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String response = this.handler.apply(request); bfWriter.write(response); // Be sure to 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); } }
There is no big problem here, but our server is still single threaded. If we simply return a string, our architecture is no problem. If we do other processing for each request at this time, such as checking the database, our server performance is very low, There will also be a server reject error (because the pending Queue of the operating system is full), so we need to further optimize it.
Step 2 (Introducing multithreading)
Multithreading is introduced here. It is no longer a single thread running alone. Try Jemeter's pressure test. The concurrency of 10000 threads will pull the memory very high, mainly because Java garbage collection has not been started after the thread is created, so it is generally not used to create threads in this way now, and thread pool will be introduced later to optimize.
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 { // If accept is not mentioned here, the while loop above will always create threads // This also shows that accept() is 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 { // Get the request content of the socket, that is, inputStream, and package it into bufferedReader for easy reading InputStream inputStream = socket.getInputStream(); BufferedReader bfReader = new BufferedReader(new InputStreamReader(inputStream)); // Read by line and put the content into stringBuilder StringBuilder requestBuilder = new StringBuilder(); String line = ""; while (true) { line = bfReader.readLine(); if (line == null || line.isBlank()) { break; } requestBuilder.append(line); } // Print request content String request = requestBuilder.toString(); System.out.println(request); // Encapsulate response BufferedWriter bfWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String response = this.handler.apply(request); bfWriter.write(response); // Be sure to 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 (encapsulate Request/Response)
In step 2, we use Function objects to implement requests and responses. We further Abstract encapsulation and create IHandlerInterface interface:
package com.fengsir.network.step3; import java.io.IOException; /** * @Author FengZeng * @Date 2022-01-24 14:26 * @Description TODO */ @FunctionalInterface public interface IHandlerInterface { /** * Processing response * @param request request * @param response response * @throws IOException */ void handler(Request request, Response response) throws IOException; }
Then we create the Request class:
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 { // Method of extracting request using regular expression 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 { // This is the difference between DataInputStream and InputStream // DataInputStream -> primitives(char,float) // InputStream -> bytes DataInputStream iptStream = new DataInputStream(socket.getInputStream()); BufferedReader bfReader = new BufferedReader(new InputStreamReader(iptStream)); // Read the requested method from the first line. HttpParser is the object in the Commons httpclient package I introduced String methodLine = HttpParser.readLine(iptStream,"UTF-8"); Matcher matcher = methodRegex.matcher(methodLine); matcher.find(); String method = matcher.group(); // Parse request header // 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()); } // Parse request body 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; } }
Create Response class:
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 Standard response * @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); } /** * Send original response * @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(); } }
Here, let's reconstruct the main function:
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); } }
It can be seen that our main function is very short now. In fact, most frameworks come from such a process, so the ability of abstraction, encapsulation and reconstruction is very important. Excellent programmers will think about these aspects when coding. In the next chapter, I plan to introduce NIO to continue to optimize our Http Server