15. Solve the problem that the browser transmits Chinese
Since the HTTP protocol only supports ISO8859-1, this character set does not support Chinese, so when the request contains Chinese, the Chinese cannot be transmitted correctly. In UTF-8, each Chinese is represented by three bytes. One scheme is that before the request, First convert Chinese into 3 bytes according to UTF-8 and then transmit it. For example, if you want to transmit a name, you need 24 bytes, which brings problems. The transmission speed of large data volume is slow, and it is too verbose.
Another solution is to use hexadecimal to simplify binary expression! This brings another problem: how to distinguish it from the actual combination of English numbers? Solution: If you want to represent hexadecimal content instead of English, every two digits of hexadecimal use % before
Final solution: In the parseParameters() method in the HttpServletRequest class, use API URLDecode(line, "UTF-8") to convert the Chinese part of the request path to Chinese
Expanding knowledge: Why do we need UTF-8 when we have Unicode encoding?
Example: In QQ chat, A sent a Chinese to B. Since Unicode is used to express one Chinese with two bytes, and one English/symbol with one byte, the bottom layer of the computer transmits all binary words during the transmission process. Section, then B should treat these two bytes as two English or symbols or Chinese, using UTF-8 solves this problem
private void parseParameters(String line){ //Convert the parameters to Chinese first try { line = URLDecoder.decode(line,"UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String[] data = line.split("&");//Split the parameter part into each group of parameters according to "&" for(String para : data){ //para: username=zhangsan String[] paras = para.split("="); parameters.put(paras[0],paras.length>1?paras[1]:""); } }
16. Implement processing dynamic pages
The original solution: set up the /userList mapping process in the front controller class, and generate a userList.html response in the processor to the browser. This solution has concurrency security issues and performance issues
- It involves reading and writing files from the hard disk, and there are performance problems
- Multiple browsers write to userList.html at the same time, which is not thread-safe
Solution: Use ByteArrayOutputStream to store dynamic data, and whether the data is null, refactor the sendContent() method, add the getOutputStream method, getWriter() method, setContentType() method
This requirement is whether the request is a business process, if it is not a business process (that is, ByteArrayOutputStream is null
) but whether there is a corresponding resource for the requested resource, if so, set the static resource of the response, if not, redirect to 404
public class HttpServletResponse { private Socket socket; //Information about the status line private int statusCode = 200; //status code private String statusReason = "OK"; //status description //Response header related information private Map<String,String> headers = new HashMap<>(); //response header //Information about the response body private File contentFile; //Entity file corresponding to the text private ByteArrayOutputStream baos; public HttpServletResponse(Socket socket){ this.socket = socket; } /** * Send the content of the current response object to the client (browser) in a standard HTTP response format */ public void response() throws IOException { //Preparation before sending sendBefore(); //3.1 Send status line sendStatusLine(); //3.2 Sending response headers sendHeaders(); //3.3 Sending the response body sendContent(); } //Preparation before sending private void sendBefore(){ //Determine whether dynamic data exists if(baos!=null){ addHeader("Content-Length",baos.size()+""); } } //send status line private void sendStatusLine() throws IOException { println("HTTP/1.1" + " " + statusCode + " " +statusReason); } //send response headers private void sendHeaders() throws IOException { /* When sending response headers, everything to be sent should be stored in headers as key-value pairs headers: key value Content-Type text/html Content-Length 245 Server WebServer ... ... */ Set<Map.Entry<String,String>> entrySet = headers.entrySet(); for(Map.Entry<String,String> e : entrySet){ String name = e.getKey(); String value = e.getValue(); println(name + ": " + value); } //Sending a group of carriage return + line feed alone indicates that the response header part has been sent! println(""); } //send response body private void sendContent() throws IOException { if(baos!=null){//dynamic data exists byte[] data = baos.toByteArray(); OutputStream out = socket.getOutputStream(); out.write(data); }else if(contentFile!=null) { try ( FileInputStream fis = new FileInputStream(contentFile); ) { OutputStream out = socket.getOutputStream(); int len; byte[] data = new byte[1024 * 10]; while ((len = fis.read(data)) != -1) { out.write(data, 0, len); } } } } public void sendRedirect(String path){ statusCode = 302; statusReason = "Moved Temporarily"; addHeader("Location",path); } /** * Send a line of string to the browser (auto-complement CR+LF) * @param line */ private void println(String line) throws IOException { OutputStream out = socket.getOutputStream(); out.write(line.getBytes(StandardCharsets.ISO_8859_1)); out.write(13);//send carriage return out.write(10);//send newline } public int getStatusCode() { return statusCode; } public void setStatusCode(int statusCode) { this.statusCode = statusCode; } public String getStatusReason() { return statusReason; } public void setStatusReason(String statusReason) { this.statusReason = statusReason; } public File getContentFile() { return contentFile; } public void setContentFile(File contentFile) throws IOException { this.contentFile = contentFile; String contentType = Files.probeContentType(contentFile.toPath()); //If the value of Content-Type is not analyzed according to the file, this header will not be added. The HTTP protocol stipulates that the browser determines the type by itself when the server does not send this header. if(contentType!=null){ addHeader("Content-Type",contentType); } addHeader("Content-Length",contentFile.length()+""); } public void addHeader(String name,String value){ headers.put(name,value); } /** * The bytes written out through the returned byte output stream are eventually sent to the client as the response body content * @return */ public OutputStream getOutputStream(){ if(baos==null){ baos = new ByteArrayOutputStream(); } return baos; } public PrintWriter getWriter(){ OutputStream out = getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw,true); return pw; } /** * Add response header Content-Type * @param mime */ public void setContentType(String mime){ addHeader("Content-Type",mime); } }
17. Use the reflection annotation mechanism to process requests
When we get the value of this request path path, we first need to check whether it is a request business:
- Scan all classes under the controller package
- View which classes have been annotated with the annotation @Controller (only the classes annotated with this annotation are recognized as business processing classes)
- Traverse these classes and get all their methods and see which business methods are only business methods annotated with @RequestMapping
- When traversing the business method, is the parameter value passed in @RequestMapping on the method consistent with the path value of the request path? If it is consistent, it means that the request should be processed by this method, so the reflection mechanism is used to call this method for processing .
- If all business methods in all Controller s are scanned, no path matching this request is found
It means that this request is not processing business, then perform the following operation of requesting static resources
public class DispatcherServlet { private static DispatcherServlet servlet; private static File rootDir; private static File staticDir; static { servlet = new DispatcherServlet(); try { //Locate to: target/classes rootDir = new File( DispatcherServlet.class.getClassLoader().getResource(".").toURI() ); //locate the static directory staticDir = new File(rootDir, "static"); } catch (URISyntaxException e) { e.printStackTrace(); } } private DispatcherServlet() { } public void service(HttpServletRequest request, HttpServletResponse response) throws IOException { //The uri cannot be used directly as the request path, because it may contain parameters, and the content of the parameters is not fixed information. String path = request.getRequestURI(); //Determine whether this request is a request for a service try { File dir = new File(rootDir,"com/webserver/controller"); File[] subs = dir.listFiles(f->f.getName().endsWith(".class")); for(File sub : subs){ String fileName = sub.getName(); String className = fileName.substring(0,fileName.indexOf(".")); className = "com.webserver.controller."+className; Class cls = Class.forName(className); if(cls.isAnnotationPresent(Controller.class)){ Object obj = cls.newInstance();//instantiate this Controller Method[] methods = cls.getDeclaredMethods(); for(Method method : methods){ if(method.isAnnotationPresent(RequestMapping.class)){ RequestMapping rm = method.getAnnotation(RequestMapping.class); String value = rm.value(); if(path.equals(value)){//If the request path is consistent with the parameter value of the @RequestMapping annotation in the method method.invoke(obj,request,response); return;//After the business is processed, the operation of directly contacting the processing request (do not go to the following processing static page operation) } } } } } } catch (Exception e) { e.printStackTrace(); } File file = new File(staticDir, path); System.out.println("does the page exist:" + file.exists()); if (file.isFile()) {//The resource requested by the user exists in the static directory and is a file response.setContentFile(file); } else { response.setStatusCode(404); response.setStatusReason("NotFound"); response.setContentFile(new File(staticDir, "/root/404.html")); } //Test adding other response headers response.addHeader("Server", "WebServer"); } public static DispatcherServlet getInstance() { return servlet; } }
17. Refactoring DispatcherServlet
Extract the code for judging whether this request is a request for a business into the newly created class HandlerMapping to realize decoupling
public class HandlerMapping { private static Map<String, Method> mapping = new HashMap<>(); static{ init(); } private static void init(){ try { File dir = new File( HandlerMapping.class.getClassLoader().getResource( "./com/webserver/controller" ).toURI() ); File[] subs = dir.listFiles(f->f.getName().endsWith(".class")); for(File sub : subs){ String fileName = sub.getName(); String className = fileName.substring(0,fileName.indexOf(".")); className = "com.webserver.controller."+className; Class cls = Class.forName(className); if(cls.isAnnotationPresent(Controller.class)){ // Object obj = cls.newInstance();//Instantiate this Controller Method[] methods = cls.getDeclaredMethods(); for(Method method : methods){ method.getDeclaringClass(); if(method.isAnnotationPresent(RequestMapping.class)){ RequestMapping rm = method.getAnnotation(RequestMapping.class); String value = rm.value(); mapping.put(value,method); } } } } } catch (Exception e) { e.printStackTrace(); } } /** * Obtain the corresponding method of a Controller that handles the request according to the request path * @param path * @return */ public static Method getMethod(String path){ return mapping.get(path); }
DispatchServlet
public class DispatcherServlet { private static DispatcherServlet servlet; private static File rootDir; private static File staticDir; static { servlet = new DispatcherServlet(); try { //Locate to: target/classes rootDir = new File( DispatcherServlet.class.getClassLoader().getResource(".").toURI() ); //locate the static directory staticDir = new File(rootDir, "static"); } catch (URISyntaxException e) { e.printStackTrace(); } } private DispatcherServlet() { } public void service(HttpServletRequest request, HttpServletResponse response) throws IOException { //The uri cannot be used directly as the request path, because it may contain parameters, and the content of the parameters is not fixed information. String path = request.getRequestURI(); //Determine whether to process a business according to the request path try { Method method = HandlerMapping.getMethod(path);//path:/userList if(method!=null){ method.invoke(method.getDeclaringClass().newInstance(),request,response); return; } } catch (Exception e) { e.printStackTrace(); } File file = new File(staticDir, path); System.out.println("does the page exist:" + file.exists()); if (file.isFile()) {//The resource requested by the user exists in the static directory and is a file response.setContentFile(file); } else { response.setStatusCode(404); response.setStatusReason("NotFound"); response.setContentFile(new File(staticDir, "/root/404.html")); } //Test adding other response headers response.addHeader("Server", "WebServer"); } public static DispatcherServlet getInstance() { return servlet; } }