How Tomcat works读书笔记之简单的HttpServer服务器实现

一个简单的HttpServer实现

通常我们写后台,浏览器请求后台接口,后台服务响应请求。对于其中的响应部分,我们只需要负责响应逻辑,即业务逻辑。、

而对于浏览器的请求是如何正确的访问到对应的后台接口,这一部分,很少有涉猎。

How Tomcat works 这本书向我揭秘了其中的神奇之处。

中文译本为《深入剖析Tomcat》

演示

访问存在的静态文件

首先在WEB_ROOT目录下创建一个Hello.html,内容如下图所以,必须加上响应信息。否则浏览器无法解析信息。

image-20220210165129642

Postman请求服务

image-20220210165045046

服务打印请求信息

image-20220210165008479

访问不存在的资源

image-20220210170104262

这里有个注意点:使用最新版本的postman测试。每次都是访问一次后台。是正常的。

如果使用浏览器测试,我使用google测试,每次访问结束,关闭Socket以后,会再次访问后台,也就是后台的控制台打印信息,不是停留在等待连接,而是停留在第二次的连接中的处理请求处,阻塞在输入流的read方法中。

虽然设置了Connection: close,还是会这样。原因不明。所以,我改用了Postman测试。


Socket

响应浏览器请求的所有的神奇之处,都源自与Socket

Java基础的时候,学到后面,会接触到一点点的网络编程,会学到Socket,然后会利用Socket实现一个简单的聊天程序。大家应该都是这么过来的,我第一次接触Socket就是在这个时候。

浏览器请求后台,后台响应浏览器的请求。把这个模型简化,就是建立一个连接,传输数据。而这个连接就是Socket连接。

所以,我们想搭建一个简单的httpServer服务器,首先需要在双方之间建立一个连接。浏览器端不需要我们处理,浏览器会自行的创建Socket与我们的后台连接。

我们需要做的部分,仅仅只有一个后台的Socket连接。


ServerSocket

javaSocket分两种,客户端的Socket就是Socket,服务端的Socket则是ServerSockethhtpServer需要的SocketServerSocket.

MyHttpServer

package com.chapter_1;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 一个简易的http服务器
 */
public class MyHttpServer {

    // 关机指令
    // 浏览器访问该地址,即可关闭服务器
    private static final String SHUTDOWN_COMMAND = "/ShutdownByYiazzz";


    private boolean shutdown = false;
    //    WEB_ROOT 根目录
    //    System.getProperty("user.dir")  --- 获取JVM属性
    //    user.dir   --- 当前工作目录,即当前项目的根目录
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "WEB_ROOT";


    public static void main(String[] args) {
        System.out.println(" ---- 服务器启动 ---");
        MyHttpServer myHttpServer = new MyHttpServer();
        myHttpServer.await();
        System.out.println(" ---- 服务器关闭 ---");
    }


    // 处理 连接
    // 逻辑:
	//    1.建立serverSocket
	//    2.循环等待连接
    public void await() {
        ServerSocket serverSocket = null;
        int port = 8080;

    //            第二个参数,代表等待队列长度
    //            服务器同时接收到许多客户端的连接请求,会将请求放在队列中,这个参数就是设置队列的长度
    //            队列,满了以后,将不再接收新的连接。accept 方法,会从队列中一个一个的移除已经连接的请求。
    //            操作系统的默认值是50。这里设置的参数,将覆盖操作系统的默认值。
    //            但是,当设置的参数超过操作系统的最大值或者没设置,再或者小于等于0,则还是用操作系统的默认值
        try {
            serverSocket = new ServerSocket(port, 10, InetAddress.getByName("127.0.0.1"));
        } catch (IOException e) {
            e.printStackTrace();
        }

//            循环等待连接
        while (!shutdown) {
            try {
                Socket socket = null;
                InputStream input = null;
                OutputStream output = null;

                // 阻塞方法,等待连接成功,才会返回
                System.out.println("等待连接");
                socket = serverSocket.accept();
                System.out.println("--- 建立连接 ---"+socket.toString());
                input = socket.getInputStream();
                output = socket.getOutputStream();

                System.out.println("处理请求");
//                封装 request,response 对象
//                实际上,就是为了处理请求,因为对请求的处理逻辑,在它们两个内部
                MyRequest myRequest = new MyRequest(input);
//                处理请求数据
                myRequest.parse();
                MyResponse myResponse = new MyResponse(output, myRequest);
//                响应请求
                myResponse.sendStaticResource();

//                关闭 socket
//                通过此socket创建的 input output 流都会自动被关闭
//                关闭之前,会强制刷新下缓冲区,所以信息会被写到浏览器。
                System.out.println("----关闭连接----"+socket.toString()+"\n");
                socket.close();

//                判断是否停机
                shutdown = myRequest.getUri().equals(SHUTDOWN_COMMAND);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("---");
                continue;
            }
        }
    }
}

关机指令

代码中,我们可以看到,有个关机指令。访问该地址,就会关闭服务器,其实就是设置ShutDown的值,如果是我们设置的指令,myRequest.getUri().equals(SHUTDOWN_COMMAND);判断就会为真,就会跳出while循环,程序就会结束,所以服务器就会终止。


WEB_ROOT 根目录

如果是直接使用SpringBoot,打成jar包,内嵌Tomcat,这种情况的使用者,可能会对这个WEB_ROOT根目录的概念比较陌生。

这个目录下面是放静态文件。是对浏览器暴露的,浏览器可以访问该目录下的文件。后面的代码,会将这个目录的作用展示出来,目前先不谈。

我们需要在代码结构目录中,创建一个WEB_ROOT 根目录。静态文件放在这个目录下面,其中WEB_ROOT目录,放在哪里,需要配合代码中的路径:

 public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "WEB_ROOT";

代码中写明了,WEB_ROOT的上级目录为System.getProperty("user.dir")的值,即项目的根路径,所以WEB_ROOT放在项目根目录下。

image-20220210162444901

image-20220210162905022


await 方法

方法内部创建了一个ServerSocket对象,指定监听的portIP 。然后就是while循环,循环的处理。浏览器的连接请求。

serverSocket.accept()方法

这个方法是一个阻塞方法,具体的细节,在注释中已经写好了。它会依次拿取队列中的Socket对象。每一个Socket对象,代表着一个连接。


因为每一个Socket代表一个已经建立好的连接。所以可以从这个连接中,获取输入流和输出流。

从输入流中,获取浏览器发来的请求信息,包括请求头,请求行,请求体。

在输出流中,向浏览器回写响应信息,同样也要包括,响应头,响应行,响应体。干巴巴的直接回写信息,浏览器是无法正确解析信息的。

HTTP/1.1 200 yiazzz
Content-type: text/html; charset=utf-8

<html>
<head>
    <title>HTTP Response Example</title>
</head>
<body>
Welcome to Http Server Demo
</body>
</html>

前面写好响应头,然后空一行,写上响应体。这里熟悉http的同学应该很熟悉。不熟悉的去补下相关的知识,具体补 http协议的格式

这点和我们直接使用容器,不一样,因为Tomcat容器中,帮我们写好了响应头,响应行,响应体。而我们自己搭建HttpServer服务器,是需要我们自己写的。


MyRequest

MyRequest中处理请求。

package com.chapter_1;

import java.io.IOException;
import java.io.InputStream;

public class MyRequest {
    //    接收连接进来的socket的inputStream
    private InputStream input;
    //    保存 URI,注意区别URL URI
    private String uri;

    public MyRequest(InputStream input) {
        this.input = input;
    }


    /**
     * 解析 请求,将请求信息从 input 输入流中拿出卡,转成字符串
     * 获取请求行。
     */
    public void parse() {
//        读取整个请求数据
//        缓冲区大小为 2k ,意味着,我们实现的request,请求数据 最长是2K数据
        StringBuffer request = new StringBuffer(2048);
        int length = -1;
        byte[] buffer = new byte[2048];

        try {
            length = input.read(buffer);
            System.out.println("完成读取请求");
            for (int i = 0; i < length; i++) {
//                将请求信息,转成字符串
                request.append((char) buffer[i]);
            }
            System.out.println("请求信息如下:");
            System.out.println(request.toString());
            this.uri = parseUri(request.toString());
            System.out.println(this.uri);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    /**
     * 从整个请求信息中解析出 URI
     *
     * @param requestString 请求行
     * @return
     */
    private String parseUri(String requestString) {
        int firstBlank = requestString.indexOf(' ');
        if (firstBlank < 0) {
            return null;
        }
        int secondBlank = requestString.indexOf(' ', firstBlank + 1);
//        如果 secondBlank <= firstBlank,说明 secondBlank 为 -1
        if (secondBlank <= firstBlank) {
            return null;
        }
        return requestString.substring(firstBlank+1, secondBlank);
    }


    public String getUri() {
        return this.uri;
    }


}

可以看到,是我们人为的创建了一个Request对象,然后在内部处理了请求信息。

也和使用容器不一样,使用容器是我们人为的获取request对象。

目前,我们自己实现的request对象,功能很弱,只是提取了请求的信息,从请求中,截取出访问的URI


MyResponse

MyResponse中响应请求。

package com.chapter_1;

import java.io.*;

public class MyResponse {
    private static final String FILE_NOT_FOUND = "404.html";
    //    用于返回数据
    private OutputStream output;
    //    用于获取 请求的URI
    private MyRequest myRequest;

    public MyResponse(OutputStream output, MyRequest myRequest) {
        this.output = output;
        this.myRequest = myRequest;
    }

    /**
     * 回写 静态资源
     * <p>
     * 逻辑:
     * 读取资源,返回
     */
    public void sendStaticResource() throws IOException {
        byte[] buffer = new byte[2048];
        FileInputStream fileInputStream = null;
        // 创建文件对象
        File file = new File(MyHttpServer.WEB_ROOT, myRequest.getUri());
        try {
//        判断文件是否存在
            if (file.exists()) {
                fileInputStream = new FileInputStream(file);
                int length = fileInputStream.read(buffer, 0, 2048);

                while (length != -1) {
                    output.write(buffer,0,length);
                    length = fileInputStream.read(buffer, 0, 2048);
                }

            } else {
                // 返回 404.html
//                file = new File(MyHttpServer.WEB_ROOT, FILE_NOT_FOUND);
//
//                fileInputStream = new FileInputStream(file);
//                int length = fileInputStream.read(buffer, 0, 2048);
//
//                while (length != -1) {
//                    output.write(buffer,0,length);
//                    length = fileInputStream.read(buffer, 0, 2048);
//                }

                // 这里 404 直接硬编码了 响应头 和 响应行 
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +"Content-type: text/html\r\n" +"Content-Length: 23\r\n"+"\r\n"+"<h1>File not found</h1>" ;
                output.write(errorMessage.getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //  output 和 input 都是外部传进来的,不需要内部关闭
            //  在 MyHttpServlet 中 关闭。
            if (fileInputStream != null) {
                fileInputStream.close();
            }
        }
    }


}

代码中展示了,前面提到的WEB_ROOT目录的作用了。请求的URI,配上WEB_ROOT形成一个完整的文件路径,然后读取该文件,回写到浏览器。

如果该文件不存在,则返回404


总结

只需要这三个类,即可搭建一个简单的HttpServer服务器。

MyHttpServer,启动一个循环,监听具体的端口和地址。

MyRequest ,处理请求,主要是读取输入流中的请求信息。

MyResponse,响应请求,主要是配合WEB_ROOT,读取对应的静态文件,回写到浏览器。

目前,该服务器,仅仅可以返回静态资源,后面会对其进行增强。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值