How Tomcat Works 第一章 - 了解HTTP、Socket、ServerSocket构建简单的 Web 服务器

文章目录


文章简介

本章主要是对How Tomcat Works 第一章内容进行归纳,了解一个简单的HTTP 服务器是如何运行,以及尝试搭建一个简单的服务器,所需要java.net 里面的SocketServerSocket 类的基本运用。在了解SocketServerSocket 之后,手动简单搭建一个最基础的HTTP服务器。


一、了解超文本传输协议 - HTTP

HTTP 是一种传输协议,全称HyperText Transfer Protocol ,我们通常称为超文本传输协议。HTTP 允许 web 服务器和浏览器通过互联网进行来发送和接受数据。它是一种请求和响应协议。客户端请求一个文件而服务器响应请求。HTTP 使用可靠的 TCP 连接,TCP 默认

使用 80 端口。第一个 HTTP 版是 HTTP/0.9,然后被 HTTP/1.0 所替代。而现在我们常用的协议版本已升至HTTP/1.1

HTTP 中,始终都是客户端通过建立连接和发送一个 HTTP 请求从而开启一个事务。web 服务器不需要联系客户端或者对客户端做一个回调连接。无论是客户端或者服务器都可以提前终止连接。举例来说,当你正在使用一个 web 浏览器的时候,可以通过点击浏览器上的停止按钮来停止一个文件的下载进程,从而有效的关闭与 web 服务器的 HTTP 连接。

1.1、HTTP 请求

一个标准的HTTP请求应该包含以下三个部分:

(1)请求方法---统一资源标识符(URI)---请求协议/版本

(2)请求头

(3)请求内容

这里我为大家提供了一个HTTP请求的例子:

powershell 复制代码
GET /index HTTP/1.1
Host: localhost:8080
Connection: keep-alive
sec-ch-ua: "Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: Idea-2110620b=1a0ceb21-c419-46ec-9d3e-3a72f70217f0; token=dc565de2-f7e5-41b0-b22d-447c340a5a86

name=TOMCAT&age=18

请求方法---统一资源标识符(URI)---请求协议/版本对应第一行: GET /index HTTP/1.1

这里GET是请求方法,/indexURI ,而 HTTP/1.1 是协议/版本。HTTP1.1一共支持7种类型的请求,分别是:GET、POST、HEAD、OPTIONS、PUT、DELETE、TRACE。当然互联网中最常用的就是GETPOST方法。

请求头对应以下这一段:

powershell 复制代码
Host: localhost:8080
Connection: keep-alive
sec-ch-ua: "Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: Idea-2110620b=1a0ceb21-c419-46ec-9d3e-3a72f70217f0; token=dc565de2-f7e5-41b0-b22d-447c340a5a86

请求的头部包含了关于客户端环境和请求的主体内容的有用信息。例如它可能包括浏览器设

置的语言,主体内容的长度等等。每个头部通过一个回车换行符(CRLF)来分隔的。

对于 HTTP 请求格式来说,头部和主体内容之间有一个回车换行符(CRLF)是相当重要的。CRLF

告诉HTTP服务器主体内容是在什么地方开始的。

第三部分请求内容则对应: name=TOMCAT&age=18


1.2、HTTP 响应

类似于 HTTP 请求,一个 HTTP 响应也包括三个组成部分:

(1)请求方法---统一资源标识符(URI)---请求协议/版本

(2)响应的头部

(3)主体内容

下面是一个 HTTP 响应的例子:

powershell 复制代码
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 112

<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>

响应头部的第一行类似于请求头部的第一行。第一行告诉你该协议使用 HTTP 1.1,请求成功(200=成功),表示一切都运行良好,响应头部和请求头部类似,也包括很多有用的信息。响应的主体内容是响应本身的 HTML 内容。头部和主体内容通过 CRLF 分隔开来。


二、Socket 基本概念

套接字作为网络接入的一个端点,允许一个应用可以从网络中读取数据或者是写入数据,两个不同的计算机上的应用软件可以通过套接字进行通信,接收或者发送数据给对方。如果你想将一条信息从你的应用软件发送到另一台计算机上的应用,因此你应该知道另一个应用的IP地址和套接字端口。然后在JAVA程序语言中,套接字则是指的是java.net.Socket类。

2.1、创建一个套接字

创建一个套接字Socket对象,你可以使用Socket类中众多构造方法中的一个:

public Socket(String host, int port) 构造方法算是最常用的一个构造方法。这里的参数host 主机指的是远程机器名称或者机器的IP地址,port 端口指的是远程应用的端口号。比如你想要创建一个连接百度的套接字,可以这样写:Socket socket = new Socket("www.baidu.com", 80);,一旦你成功创建一个Socket实例,你就可以用它来发送或者是接受字符流。要发送字节流,你首先要调用Socket 实例的getOutputStream 方法来获取一个java.io.OutputStream对象。要 发 送信息到 一 个 远 程 应 用 , 你 经 常 要 从 返 回 的 OutputStream 对 象 中 构 造 一 个java.io.PrintWriter 对象。要从连接的另一端接受字节流,你可以调用 Socket 类的getInputStream 方法用来返回一个 java.io.InputStream 对象。

以下的代码片段创建了一个套接字,可以和本地 HTTP 服务器(127.0.0.1 是指本地主机)进行通讯,发送一个 HTTP 请求,并从服务器接受响应。它创建了一个 StringBuffer 对象来保存响

应并在控制台上打印出来。

java 复制代码
import java.io.*;
import java.net.Socket;

/**
 * @Author: Greyfus
 * @Create: 2024-08-02 21:45
 * @Version:
 * @Description:
 */
public class SocketDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        Socket socket = new Socket("localhost", 8088);
        OutputStream os = socket.getOutputStream();
        PrintWriter out = new PrintWriter(os, true);
        out.println("GET /hl-template/authority/verifyCode HTTP/1.1");
        out.println("Host: localhost:8080");
        out.println("Connection: Close");
        out.println("X-TENANT_ID: HL_TEMPLATE");
        out.println("lang: en_US");
        out.println();

        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        StringBuffer sb = new StringBuffer();
        boolean loop = true;
        while (loop) {
            if (in.ready()) {
                int read = 0;
                while (read != -1) {
                    read = in.read();
                    sb.append((char) read);
                }
                loop = false;
            }
            Thread.currentThread().sleep(50);
        }
        System.out.println("==========响应结果========");
        System.out.println(sb.toString());
    }
}

请注意,为了从 web 服务器获取适当的响应,你需要发送一个遵守 HTTP 协议的 HTTP 请求。 假如你已经阅读了前面一节超文本传输协议(HTTP),你应该能够理解上面代码提到的 HTTP 请求。

本地端口8088 我已经启动了一个tomcat 服务器,请求地址/hl-template/authority/verifyCode 是用于获取验证码接口。我们分别使用PostMan 请求工具和Socket 请求该接口,得到的结果是一致的。要明白JAVA通信都是基于Socket的,不管是RestTemplate还是httpTemplate底层都是基于Socket来通信



二、ServerSocket 基本概念

Socket 类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字。现在,假如你想实施一个服务器应用,例如一个 HTTP 服务器或者 FTP 服务器,你需要另一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用 java.net.ServerSocket 类。这是服务器套接字的实现。ServerSocketSocket 不同在于,服务器套接字ServerSocket 的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它将创建一个 Socket 实例来与客户端进行通信。

要创建一个服务器套接字,你需要使用 ServerSocket 类提供的四个构造方法中的一个。你需要指定 IP 地址和服务器套接字将要进行监听的端口号。通常,IP 地址将会是 127.0.0.1,也就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的 IP 地址被称为是绑定地址。服务器套接字的另一个重要的属性是 backlog,这是服务器套接字开始拒绝传入的请求之前,传入的连接请求的最大队列长度。

public ServerSocket(int port, int backLog, InetAddress bindingAddress)ServerSocket 最为常用的构造方法,绑定地址必须是 java.net.InetAddress 的一个实例,通常我们可以使用InetAddress.getByName("127.0.0.1"); 构造一个实例。一旦你有一个 ServerSocket 实例,你可以让它在绑定地址和服务器套接字正在监听的端口上等待传入的连接请求。你可以通过调用 ServerSocket 类的 accept 方法做到这点。这个方法只会在有连接请求时才会返回,并且返回值是一个 Socket 类的实例。

2.1、使用ServerSocket构建简单HTTPServer

我们将构建三个类用于组装一个简易的HTTP ServerHttpServer 类用于接受外来HTTP 请求,通过accept 方法获取Socket 实例,并将InputStreamOutputStream 封装成HttpRequest 对象和HttpResone 对象。HttpRequest 用于解析请求信息(请求地址请求头请求参数)等,并将这些信息打印到控制台。而HttpResone 用于对不同的请求地址,给与不同的响应信息,并将这些响应信息返还给客户端,当客户的请求地址是/HELLO时,将响应Hello, nice to meet给客户端,否则响应Sorry, no corresponding resources found。当请求地址是/SHUTDOWN时,服务器将会被关闭。

HttpServer代码如下:

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @Author: Greyfus
 * @Create: 2024-08-06 21:21
 * @Version: 1.0.0
 * @Description:HTTP 服务器 Demo
 */
public class HttpServer {

    private static final String SHUTDOWN = "/SHUTDOWN"; //关机指令

    // the shutdown command received
    private boolean shutdown = false;

    public static void main(String[] args) {
        HttpServer httpServer = new HttpServer();
        httpServer.startServer();//启动服务器
    }

    public void startServer() {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(80, 1, InetAddress.getByName("127.0.0.1"));
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!shutdown) {
            Socket socket = null;
            InputStream input = null;
            OutputStream output = null;
            try {
                socket = serverSocket.accept();//等待获取连接
                input = socket.getInputStream();
                output = socket.getOutputStream();
                HttpRequest request = new HttpRequest(input);
                request.parseRequest();//解析请求

                HttpResponse response = new HttpResponse(output);
                response.setRequest(request);
                response.loadStaticResource();
                socket.close();
                shutdown = request.getUri().equals(SHUTDOWN);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}

HttpRequest代码如下:

java 复制代码
import java.io.IOException;
import java.io.InputStream;

/**
 * @Author: Greyfus
 * @Create: 2024-08-06 21:27
 * @Version: 1.0.0
 * @Description: 用于处理请求信息
 */
public class HttpRequest {

    private InputStream input;//记录输入流
    private String uri; //记录请求地址

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

    /**
     * 解析请求
     */
    public void parseRequest() {
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer);
        }
        catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for (int j=0; j<i; j++) {
            request.append((char) buffer[j]);
        }
        System.out.print("解析请求信息如下:\n\n");
        System.out.println("======================================");
        System.out.println(request.toString());
        System.out.println("======================================");
        uri = parseUri(request.toString());
    }

    /**
     * 解析请求路径
     * @param requestString
     * @return
     */
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }

    /**
     * 返回请求地址
     * @return uri
     */
    public String getUri() {
        return uri;
    }
}

HttpResponse代码如下:

java 复制代码
import java.io.IOException;
import java.io.OutputStream;

/**
 * @Author: Greyfus
 * @Create: 2024-08-06 21:31
 * @Version: 1.0.0
 * @Description: 响应信息处理
 */
public class HttpResponse {

    HttpRequest request;
    OutputStream output;

    public HttpResponse(OutputStream output) {
        this.output = output;
    }

    public void setRequest(HttpRequest request) {
        this.request = request;
    }

    /**
     * 加载静态资源
     * 如果访问路径是HELLO,则响应 HELLO! Welcome!
     * 否则响应404 错误
     */
    public void loadStaticResource() throws IOException {

        if (request.getUri().equalsIgnoreCase("/HELLO")) {
            String successMessage = "HTTP/1.1 200 ok\n" +
                    "Content-Type: text/html\r\n" +
                    "Content-Length: 24\r\n" +
                    "\r\n" +
                    "<h1>Hello, nice to meet you</h1>";
            output.write(successMessage.getBytes());
        } else {
            String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                    "Content-Type: text/html\r\n" +
                    "Content-Length: 48\r\n" +
                    "\r\n" +
                    "<h1>Sorry, no corresponding resources found</h1>";
            output.write(errorMessage.getBytes());
        }
    }

}

使用浏览器演示结果:

控制台输出结果:


相关推荐
小林熬夜学编程1 小时前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
njnu@liyong12 小时前
图解HTTP-HTTP报文
网络协议·计算机网络·http
ZachOn1y12 小时前
计算机网络:应用层 —— 应用层概述
计算机网络·http·https·应用层·dns
Graceful_scenery1 天前
https双向认证
服务器·网络·网络协议·http·https
梁萌1 天前
Docker快速安装Tomcat
docker·容器·tomcat·镜像
njnu@liyong1 天前
图解HTTP-HTTP状态码
网络协议·计算机网络·http
代码洁癖症患者2 天前
HTTP请求的奇幻旅程:从发起至响应的全方位探索
网络·网络协议·http
全栈老实人_2 天前
时间管理系统|Java|SSM|JSP|
java·开发语言·tomcat·maven
寻找沙漠的人2 天前
HTTP—02
网络·网络协议·http
2401_850410832 天前
LVS简介
运维·nginx·tomcat·lvs