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请求的例子:

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方法。

请求头对应以下这一段:

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 响应的例子:

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 对象来保存响
应并在控制台上打印出来。

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代码如下:

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代码如下:

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代码如下:

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());
        }
    }

}

使用浏览器演示结果:
在这里插入图片描述
在这里插入图片描述
控制台输出结果:
在这里插入图片描述


  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值