1 Maven

1.1 什么是Maven

1.1.1 Maven概述

Maven是一种构建工具,用于管理Java项目的构建,依赖管理和项目信息管理

Maven的优点

  • 依赖管理:Maven可以自动下载和管理项目依赖项
  • 插件系统:Maven提供了一个强大的插件系统,使开发人员可以轻松地扩展和自定义构建过程
  • 项目信息管理:Maven可以管理项目的元数据,例如项目版本号,作者信息等
  • 统一的构建方式:Maven提供了一种统一的构建方式,使得团队成员能够更轻松地理解和管理项目的构建过程
  • 生态系统:Maven拥有一个庞大的生态系统,包括许多插件和构建工具,使得开发人员可以快速创建高质量的Java应用程序

1.2 Maven的配置和使用

1.2.1 配置Maven 仓库

Maven仓库分为本地仓库和远程仓库两种类型:

  • 本地仓库:位于开发人员的本地计算机上,存储本地构建的项目产物和下载的依赖项
  • 远程仓库:位于服务器上,存储由开发人员上传的构建产物和其他公共的依赖项

Maven

1.2.2 IDEA中使用Maven

在 Maven 中,每个项目的依赖关系都需要使用坐标(Coordinate)来描述,这个坐标包括以下三个信息:

  • groupId:组名,表示依赖项所属的组织或公司的唯一标识符,通常是组织或公司的域名倒序,例如 com.example
  • artifactId:区分不同的项目模块,例如 my-project
  • version:版本号,例如 1.0-SNAPSHOT(开发版)

2 XML

2.1 XML 概述

2.1.1 什么是XML

XML,是一种用于描述数据的标记语言,设计目的是传输数据。

XML 的好处包括:

  • 简单易学:XML 采用标记的形式来描述数据,标记的含义很明确,很容易理解和学习
  • 通用性:XML 不仅仅适用于 Web 应用程序,还可以用于各种不同的应用程序之间的数据传输和存储
  • 可读性:XML 的标记很容易阅读,易于人类理解,可以帮助开发人员更快地理解数据的结构和内容
  • 可扩展性:XML 允许用户自定义标记,因此可以根据具体的需求扩展标记,实现更为灵活的数据描述方式
  • 跨平台性:XML 不依赖于特定的操作系统或开发环境,因此可以在各种平台上使用
  • 数据独立性:XML 与具体的数据存储方式无关,可以存储在不同的数据库中,也可以直接存储在文件中,因此可以实现数据独立性

2.2 XML 核心语法

2.2.1 XML核心语法

1、XML 声明

XML 文件通常以 XML 声明开头,它用于指定 XML 版本和编码方式。例如:

version 属性指定 XML 版本,encoding 属性指定编码方式。

<?xml version="1.0" encoding="UTF-8"?>

2、根元素

XML 文件必须有一个根元素,只能有一个根元素,它是整个 XML 文档的起点。例如: 是根元素。

<catalog>
  ...
</catalog>

3、元素和标签

XML 使用标记来标识数据元素,标记由开始标签、结束标签和内容组成。例如:

<book>是开始标签,</book> 是结束标签

<title>、<author>、<price> 是元素,它们包含了书籍的标题、作者和价格信息。
<book>
  <title>XML Developer's Guide</title>
  <author>Gambardella, Matthew</author>
  <price>44.95</price>
</book>

4、属性

XML 元素可以包含属性,属性只能写在开始标签中,"="前后不要有空格,多个属性中间以空格分开

<book id="bk101">
  ...
</book>

在这个例子中,id 是属性名,bk101 是属性值。

5、注释

XML 支持注释,注释用于在 XML 文件中添加注释性文字,不会被解析器处理。例如:

<!-- 这是一段注释 -->

在这个例子中, 是注释的结束标记。

6、XML 大小写敏感

这个与Java语法类似,大写字符和小写字符不能混用。一个XML的例子:

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
    <!-- 这是一段注释 -->
    <book id="bk101">
        <title>XML Developer's Guide</title>
        <author>Gambardella, Matthew</author>
        <price>44.95</price>
    </book>
    <!-- 这是第二段注释 -->
    <book id="bk102">
        <title>Thinking in Java</title>
        <author>Eckel, Bruce</author>
        <price>39.56</price>
    </book>
</catalog>

2.2.2 XML实例

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 根元素 -->
<catalog>
    <!-- 定义一本书 -->
    <book id="book1001">
        <title>老人与海</title>
        <author>海明威</author>
        <price>50.0</price>
    </book>
    <book id="book1002">
        <title>十八岁给我一个姑娘</title>
        <author>冯唐</author>
        <price>69.9</price>
    </book>
    <book id="book1003">
        <title>全新三国</title>
        <author>变相怪杰</author>
        <price>9.9</price>
    </book>
</catalog>

3 手写WebServer

3.1 Web原理

3.1.1 Web工作原理

Web的工作原理是基于客户端-服务器模型(B/S)的。简单来说,Web由Web服务器、Web客户端和通信协议组成。

web工作原理

1、Web服务器

Web服务器是一个可以接收客户端请求的软件程序。它运行在一个计算机上,一般是指提供Web服务的主机,可以在这个主机上存储Web页面、图像和其他资源。当Web服务器接收到一个客户端请求后,它会发送一个HTTP响应,包括被请求资源的内容和元数据。

2、Web客户端

Web客户端是通过浏览器访问Web的用户设备,如电脑、手机等。当用户在浏览器中输入URL时,浏览器会发送一个HTTP请求到Web服务器。Web服务器接收到请求后,会查找请求的资源并将响应返回给浏览器,浏览器会将响应显示在用户的屏幕上。

3、HTTP协议

Web的通信是基于HTTP协议进行的。它定义了浏览器和Web服务器之间的请求和响应交互方式。当浏览器发送HTTP请求时,请求会包含HTTP方法(GET、POST、PUT等)、请求的URL和HTTP头部信息。Web服务器会解析HTTP请求并生成HTTP响应。HTTP响应会包括状态码、HTTP头部信息和响应正文。状态码表示请求是否成功,HTTP头部信息包含了响应的元数据,响应正文则包含了请求的数据。

4、HTML

写网页的,与xml不一样的是HTML规定了固定的标签,有严格的要求。

3.2 手写WebServer

WebServer的处理步骤

1、创建一个ServerSocket对象:ServerSocket对象用于监听指定的端口,并接受客户端的请求。

2、等待客户端连接:通过调用ServerSocket的accept()方法,等待客户端的连接请求。当有客户端连接时,accept()方法返回一个Socket对象,用于和客户端进行通信。

3、解析HTTP请求:从Socket对象中读取客户端的请求数据,并将其解析为HTTP请求。HTTP请求由请求行、请求头和请求体组成。

4、处理HTTP请求:根据HTTP请求中的方法、路径和参数等信息,处理客户端的请求,并生成HTTP响应。HTTP响应由状态行、响应头和响应体组成。

5、发送HTTP响应:将HTTP响应发送回客户端,并关闭Socket连接。

实现一个WebServer涉及到很多细节,例如解析HTTP请求、处理GET和POST请求、生成HTTP响应等。

3.3 接收HTTP请求

3.3.1 HTTP请求结构

HTTP请求报文由三个部分组成:请求行、请求头和请求体

GET /hello.txt HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

1、请求行

请求行是HTTP请求报文的第一行,包括HTTP方法、请求URI和HTTP版本。请求行的格式如下:

METHOD URI HTTP_VERSION

其中:

METHOD为HTTP方法,通常为GET、POST、PUT、DELETE等;

URI为请求资源的路径,可以包含查询参数;

HTTP_VERSION为HTTP协议版本,通常为HTTP/1.1或HTTP/2。

2、请求头

请求头紧随请求行之后,以一或多个以冒号分隔的键值对的形式提供附加信息。每个键值对为一行,键和值之间用冒号和空格分隔。

上个示例中,请求头包含了Host、User-Agent和Accept三个键值对。Host指定了服务器的域名或IP地址,User-Agent指定了浏览器的类型和版本,Accept指定了浏览器能够接受的响应格式。

3、请求体

请求体是HTTP请求报文的可选部分,通常在使用POST或PUT方法提交表单数据时出现。请求体包含了客户端发送到服务器的实际数据,如表单字段、文件内容等。

上个示例中,GET请求不包含请求消息体,以空行作为结束标识

下面是一个HTTP POST请求报文的示例:

POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
username=john&password=doe

这个示例中,请求行为POST /login HTTP/1.1,表示使用POST方法向/login路径提交请求。请求头包括了Host、Content-Type和Content-Length三个键值对。请求体为username=john&password=doe,表示提交了用户名和密码两个表单字段的值。注意请求头和请求体之间有一个空行

HTTP协议的详细内容可以参考HTTP协议官方文档 RFC2616标准:https://www.rfc-editor.org/rfc/rfc2616

3.3.2 接收HTTP请求

1、创建ServerSocket对象,并指定监听的端口号8088。

ServerSocket serverSocket = new ServerSocket(8088);

2、使用accept()方法等待客户端的连接请求,并获取客户端的Socket对象。

Socket clientSocket = serverSocket.accept();

3、从客户端Socket对象中获取输入流,先尝试一个简单方式读取HTTP请求报文(请求消息),读取代码示意如下:

InputStream in = clientSocket.getInputStream();
int b;
while ((b=in.read())!=-1){
        System.out.print((char) b);
}
in.close();

3.3.3 【案例】编程实现接收HTTP请求

/**
 * @document: 接收客户端的HTTP请求
 * @Author:SmallG
 * @CreateTime:2023/8/16+10:43
 */

public class ServerBootApplication {
    private ServerSocket serverSocket;

    public void start() {
        try {
            //创建ServerSocket对象,监听8088端口
            serverSocket = new ServerSocket(8088);
            System.out.println("等待客户端连接...");
            //等待客户端连接
            Socket socket = serverSocket.accept();
            //获取客户端的输入流,读取请求报文
            InputStream in = socket.getInputStream();
            //字节转字符,由于请求报文的内容全部是字符串,可以直接读取,不需要转换
            int b = 0;
            while ((b = in.read()) != -1) {
                System.out.print((char) b);
            }
            in.close();
            //关闭客户端
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ServerBootApplication application = new ServerBootApplication();
        //启动服务器
        application.start();
    }
}

打开浏览器向 http://localhost:8088 发起请求,在开发工具控制台上输出如下信息:

GET / HTTP/1.1
Host: localhost:8088
Connection: keep-alive
sec-ch-ua: "Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"
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/115.0.0.0 Safari/537.36 Edg/115.0.1901.203
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,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
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: Idea-1c6045da=c0214a16-93b5-4c4f-9c83-4bf53f68cdea

程序并不会停止,没有读取结束的标志。

GET请求消息每个行结束符号为“\r\n”,最后发送了空行“\r\n”为结束,我们改进程行读取到空行就结束读取,让循环结束,代码改进如下:

/**
 * @document: 接收客户端的HTTP请求
 * @Author:SmallG
 * @CreateTime:2023/8/16+10:43
 */

public class ServerBootApplication {
    private ServerSocket serverSocket;

    public void start() {
        try {
            // 创建ServerSocket对象,监听8088端口
            serverSocket = new ServerSocket(8088);
            System.out.println("等待客户端连接...");
            // 等待客户端连接
            Socket socket = serverSocket.accept();
            // 获取客户端的输入流,读取请求报文
            InputStream in = socket.getInputStream();
            // 定义Builder用于存储读取到的一行数据
            StringBuilder builder = new StringBuilder();
            // 定义变量表示读到的当前字符
            char current = 0;
            // 定义变量,表示读到的当前字符的前一个字符
            char previous = 0;

            // 字节转字符,由于请求报文的内容全部是字符串,可以直接读取,不需要转换
            int b = 0;
            while ((b = in.read()) != -1) {
                // 把当前读取到的内容存在当前字符中
                // 因为请求报文采用了 IOS8895-1基于ASCII的编码,可以直接转换为字符
                current = (char) b;
                // 如果当前的字符是\n 前一个字符是\r,说明读到了一行的末尾
                if (current == '\n' && previous == '\r') {
                    if (builder.isEmpty()){
                        break;
                    }
                    // 一行结束,输出当前这一行数据
                    System.out.println(builder);
                    // 清空当前行的数据
                    builder.delete(0, builder.length());
                } else if (current != '\r' && previous != '\n') {
                    // 当前读取到的是正常内容(不是\r\n)把内容拼接到StringBuilder中
                    builder.append(current);
                }
                // 把当前的字符转换为前一个字符
                previous = current;
            }
            in.close();
            // 关闭客户端
            socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ServerBootApplication application = new ServerBootApplication();
        //启动服务器
        application.start();
    }
}

3.4 发送HTTP响应

响应消息结构

HTTP响应消息由三部分组成:状态行、响应头和响应正文

1、状态行

状态行由HTTP协议版本、状态码和状态描述组成,通常格式如下:

HTTP/1.1 200 OK

其中,HTTP/1.1表示HTTP协议的版本,200表示状态码,OK是状态描述。

2、响应头

响应头包含了一些关于响应消息的元数据,如响应日期、内容类型、内容长度等,格式如下:

Content-Type: text/htmlContent-Length: 1234Date: Fri, 25 Feb 2023 10:00:00 GMT

3、响应正文(响应体)

响应正文是服务器返回的实际数据,可以是HTML网页、图片、文本等等。响应正文的格式和内容取决于服务器返回的数据类型和内容。

完整的HTTP响应消息结构如下:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Date: Fri, 25 Feb 2023 10:00:00 GMT
<html>
<head>
<title>Example</title>
</head>
<body>
<p>This is an example.</p>
</body>
</html>

其中响应头Content-Type: text/html; charset=utf-8 用于说明,响应正文中的内容类型,这是text/html表示,响应正文中是一个html网页,charset=utf-8表示响应正文中的网页采用UTF-8编码。

响应头Content-Length: 1234,用于说明响应正文中内容长度,单位是字节数量。

【案例】向浏览器发送HTTP响应

在Java中向浏览器发送HTTP响应需要借助Java中的Socket和OutputStream等类。

/**
 * @document: 接收客户端的HTTP请求
 * @Author:SmallG
 * @CreateTime:2023/8/16+10:43
 */

public class ServerBootApplication {
    private ServerSocket serverSocket;

    public void start() {
        try {
            // 创建ServerSocket对象,监听8088端口
            serverSocket = new ServerSocket(8088);
            System.out.println("等待客户端连接...");
            // 等待客户端连接
            Socket socket = serverSocket.accept();
            // 获取客户端的输入流,读取请求报文
            InputStream in = socket.getInputStream();
            // 定义Builder用于存储读取到的一行数据
            StringBuilder builder = new StringBuilder();
            // 定义变量表示读到的当前字符
            char current = 0;
            // 定义变量,表示读到的当前字符的前一个字符
            char previous = 0;

            // 字节转字符,由于请求报文的内容全部是字符串,可以直接读取,不需要转换
            int b = 0;
            while ((b = in.read()) != -1) {
                // 把当前读取到的内容存在当前字符中
                // 因为请求报文采用了 IOS8895-1基于ASCII的编码,可以直接转换为字符
                current = (char) b;
                // 如果当前的字符是\n 前一个字符是\r,说明读到了一行的末尾
                if (current == '\n' && previous == '\r') {
                    if (builder.toString().isEmpty()) {
                        break;
                    }
                    // 一行结束,输出当前这一行数据
                    System.out.println(builder);
                    // 清空当前行的数据
                    builder.delete(0, builder.length());
                } else if (current != '\r' && previous != '\n') {
                    // 当前读取到的是正常内容(不是\r\n)把内容拼接到StringBuilder中
                    builder.append(current);
                }
                // 把当前的字符转换为前一个字符
                previous = current;
            }
            //往客户端发送响应

            //获取输出流
            OutputStream out = socket.getOutputStream();
            String html = """
                    <html>
                         <head>
                              <title>Hello</title>
                         </head>
                         <body>
                              <p>Hello World</p>
                         </body>
                    </html>
                                       
                     """;
            //定义字节数组 作为响应体
            byte[] body = html.getBytes(StandardCharsets.UTF_8);
            //发送响应
            out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8)); //响应行
            out.write('\r');
            out.write('\n');
            out.write("Content-Type: text/html; charset=utf-8"
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write(("Content-Length: " + body.length)
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write('\r');
            out.write('\n');
            out.write(body); //响应体
            out.close();
            in.close();
            // 关闭客户端
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ServerBootApplication application = new ServerBootApplication();
        //启动服务器
        application.start();
    }
}

HTTP响应的第一行为状态行,这里写入了“HTTP/1.1 200 OK”,表示HTTP版本为1.1,状态码为200,状态码200表示请求成功。

接着写入了响应头信息,包括“Content-Type”表示响应体类型为HTML文本,“charset=utf-8”表示响应体采用的字符集为UTF-8,“Content-Length”表示响应体长度为body的字节数组长度。

之后写入一个空行,表示响应头和响应体的分隔符,最后将响应体内容写入到输出流中。

4 WebServer进阶

4.1 使用多线程处理多用户请求

【案例】使用多线程处理HTTP通信

首先定义ClientHandler,作为线程处理HTTP请求和发生HTTP响应:

/**
 * @document: 用于http请求和响应多线程的处理方式
 * @Author:SmallG
 * @CreateTime:2023/8/16+14:48
 */

public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 获取客户端的输入流,读取请求报文
            InputStream in = socket.getInputStream();
            // 定义Builder用于存储读取到的一行数据
            StringBuilder builder = new StringBuilder();
            // 定义变量表示读到的当前字符
            char current = 0;
            // 定义变量,表示读到的当前字符的前一个字符
            char previous = 0;

            // 字节转字符,由于请求报文的内容全部是字符串,可以直接读取,不需要转换
            int b = 0;
            while ((b = in.read()) != -1) {
                // 把当前读取到的内容存在当前字符中
                // 因为请求报文采用了 IOS8895-1基于ASCII的编码,可以直接转换为字符
                current = (char) b;
                // 如果当前的字符是\n 前一个字符是\r,说明读到了一行的末尾
                if (current == '\n' && previous == '\r') {
                    if (builder.toString().isEmpty()) {
                        break;
                    }
                    // 一行结束,输出当前这一行数据
                    out.println(builder);
                    // 清空当前行的数据
                    builder.delete(0, builder.length());
                } else if (current != '\r' && current != '\n') {
                    // 当前读取到的是正常内容(不是\r\n)把内容拼接到StringBuilder中
                    builder.append(current);
                }
                // 把当前的字符转换为前一个字符
                previous = current;
            }
            //往客户端发送响应
            //获取输出流
            OutputStream out = socket.getOutputStream();
            String html = """
                    <html>
                         <head>
                              <title>Hello</title>
                         </head>
                         <body>
                              <p>Hello World</p>
                         </body>
                    </html>
                                       
                     """;
            //定义字节数组 作为响应体
            byte[] body = html.getBytes(StandardCharsets.UTF_8);
            //发送响应
            out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8)); //响应行
            out.write('\r');
            out.write('\n');
            out.write("Content-Type: text/html; charset=utf-8"
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write(("Content-Length: " + body.length)
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write('\r');
            out.write('\n');
            out.write(body); //响应体
            out.close();
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭客户端
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

重写start方法

/**
 * @document: 接收客户端的HTTP请求
 * @Author:SmallG
 * @CreateTime:2023/8/16+10:43
 */

public class ServerBootApplication {
    private ServerSocket serverSocket;

    public void start() {
        try {
            // 创建ServerSocket对象,监听8088端口
            serverSocket = new ServerSocket(8088);
            while (true) {
                System.out.println("等待客户端连接...");
                // 等待客户端连接
                Socket socket = serverSocket.accept();

                //创建线程对象
                ClientHandler handler = new ClientHandler(socket);
                //启动线程
                Thread t = new Thread(handler);
                t.start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ServerBootApplication application = new ServerBootApplication();
        //启动服务器
        application.start();
    }
}

4.2 解析请求行

解析请求行

HTTP请求行是HTTP请求报文的第一行,包括HTTP方法、请求URL和HTTP协议版本号。例如:

GET /index.html HTTP/1.1

其中,GET表示HTTP请求方法,/index.html表示请求的资源URL,HTTP/1.1表示使用的HTTP协议版本号。

解析HTTP请求行可以获取请求方法、请求URL和协议版本等信息,这些信息对于服务器来说非常重要,可以根据这些信息对请求进行处理和响应。例如,根据请求URL可以确定请求的资源类型和位置,从而进行处理和响应;根据请求方法可以确定请求的类型(如GET、POST、PUT、DELETE等),从而采取相应的处理方式。

显示正确的请求内容

将请求行进行解析,找出客户端发起的请求资源路径,根据请求资源的路径找到相应的资源,发送响应到客户端浏览器。这样就可以在用户请求不同资源时候,响应不同的结果:

显示正确的请求内容

4.2.3 【案例】读取请求行

具体实现逻辑如下:

  1. 创建一个InputStream对象in,并将其设置为socket的输入流。
  2. 创建一个StringBuilder对象builder,用于存储读取的数据。
  3. 定义两个字符变量previous和current,用于记录前一个字符和当前字符。
  4. 定义一个int类型变量b,用于记录从输入流中读取的字节。
  5. 使用while循环从输入流中读取字节,直到读取完一行数据。
  6. 将读取到的字节转换成字符类型,并赋值给变量current。
  7. 判断当前字符是否为行结束符("\r\n"),如果是则退出循环,否则将当前字符添加到builder中。
  8. 将当前字符赋值给previous,以备下次循环使用。
  9. 循环结束后,将builder转换成字符串并返回。
/**
 * @document: 用于http请求和响应多线程的处理方式
 * @Author:SmallG
 * @CreateTime:2023/8/16+14:48
 */

public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 获取客户端的输入流,读取请求报文
            InputStream in = socket.getInputStream();
            //读取请求行
            String requestLine = readLine();
            System.out.println(requestLine);

            //读取请求头
            String requestHeader;
            //请求行不再重复读 读到空行请求头停止
            while (!(requestHeader = readLine()).isEmpty()) {
                out.println(requestHeader);
            }

            //往客户端发送响应
            //获取输出流
            OutputStream out = socket.getOutputStream();
            String html = """
                    <html>
                         <head>
                              <title>Hello</title>
                         </head>
                         <body>
                              <p>Hello World</p>
                         </body>
                    </html>
                                       
                     """;
            //定义字节数组 作为响应体
            byte[] body = html.getBytes(StandardCharsets.UTF_8);
            //发送响应
            out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8)); //响应行
            out.write('\r');
            out.write('\n');
            out.write("Content-Type: text/html; charset=utf-8"
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write(("Content-Length: " + body.length)
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write('\r');
            out.write('\n');
            out.write(body); //响应体
            out.close();
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭客户端
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 读取请求报文请求行信息
     */
    public String readLine() throws IOException {
        InputStream in = socket.getInputStream();
        StringBuilder builder = new StringBuilder();
        char current = 0;
        char previous = 0;
        int b = 0;
        while ((b = in.read()) != -1) {
            current = (char) b;
            if (previous == '\r' && current == '\n') {
                break;
            } else if (current != '\r' && current != '\n') {
                builder.append(current);
            }
            previous = current;
        }

        return builder.toString();
    }
}

【案例】解析请求行

上述代码实现了,读取请求行,读取后需要从请求行中解析其中的每个部分,然后可以根据请求行找到相应的本地文件资源,发送响应给浏览器,显示不同的资源内容。

解析请求行

解析请求行:

/**
 * @document: 解析请求行
 * @Author:SmallG
 * @CreateTime:2023/8/16+15:34
 */

public class Demo02 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8088);
        Socket socket = serverSocket.accept();
        InputStream in = socket.getInputStream();
        StringBuilder builder = new StringBuilder();

        //定义当前字符和前一个字符
        char current = 0; //当前字符
        char previous = 0; //前一个字符
        int b = 0;       //读取的字符
        //定义一个字符串变量 用于接收请求行
        String requestLine = null;
        while ((b = in.read()) != -1) {
            //获取当前字符
            current = (char) b;
            //读取第一行数据
            if (current == '\n' && previous == '\r') {
                requestLine = builder.toString(); //取出第一行数据

                break;
            } else if (current != '\r' && current != '\n') {
                builder.append(current);
            }
            previous = current;
        }
        //解析请求行
        String[] line = requestLine.split("\s"); //\s表示空格,制表符,和换行符
        String method = line[0];
        String uri = line[1];
        String protocol = line[2];
        System.out.println("请求方法:" + method);
        System.out.println("请求路径:" + uri);
        System.out.println("版本号:" + protocol);
    }
}

具体解释如下:

  • 使用String类的split()方法按照空白字符(包括空格、制表符和换行符)对请求行进行分割,得到一个包含请求方法、URI和协议版本三个字段的字符串数组line
  • 将line数组中的第一个元素存储到字符串变量method中,第二个元素存储到字符串变量uri中,第三个元素存储到字符串变量protocol中
  • 最后,使用System.out.println()方法打印出method、uri和protocol的值,方便调试和查看解析结果

4.3 响应静态资源

响应静态资源

重构服务端代码,根据URI在resources/static文件夹中找到静态资源,并且将静态资源响应给客户端,原理为:

响应静态资源

4.3.3 【案例】响应静态资源

重构后的代码:

/**
 * @document: 用于http请求和响应多线程的处理方式
 * @Author:SmallG
 * @CreateTime:2023/8/16+14:48
 */

public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 获取客户端的输入流,读取请求报文
            InputStream in = socket.getInputStream();
            //读取请求行
            String requestLine = readLine();
            System.out.println(requestLine);

            //解析请求行
            String[] line = requestLine.split("\s");
            String method = line[0];
            String uri = line[1];
            String protocol = line[2];

            //读取请求头
            String requestHeader;
            //请求行不再重复读 读到空行请求头停止
            while (!(requestHeader = readLine()).isEmpty()) {
                out.println(requestHeader);
            }

            //往客户端发送响应
            //加载静态资源,通过类的加载器进行加载到target/classes目录
            File root = new File(ClientHandler.class
                    .getClassLoader().getResource(".").toURI());

            //加载target/classes/static目录
            File staticDir = new File(root, "static");
            //加载文件target/classes/static目录中的文件
            File file = new File(staticDir, uri);
            //读取文件的全部内容
            byte[] bytes = new byte[(int) file.length()];
            FileInputStream fis = new FileInputStream(file);
            fis.read(bytes); //读取文件中的全部内容

            //获取输出流
            OutputStream out = socket.getOutputStream();
            //发送响应
            out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8)); //响应行
            out.write('\r');
            out.write('\n');
            out.write("Content-Type: text/html; charset=utf-8"
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write(("Content-Length: " + bytes.length)
                    .getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write('\r');
            out.write('\n');
            out.write(bytes); //响应体
            fis.close();
            out.close();
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭客户端
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 读取请求报文请求行信息
     */
    public String readLine() throws IOException {
        InputStream in = socket.getInputStream();
        StringBuilder builder = new StringBuilder();
        char current = 0;
        char previous = 0;
        int b = 0;
        while ((b = in.read()) != -1) {
            current = (char) b;
            if (previous == '\r' && current == '\n') {
                break;
            } else if (current != '\r' && current != '\n') {
                builder.append(current);
            }
            previous = current;
        }

        return builder.toString();
    }
}

5 练习

1 设计 XML文件存储3部电影信息

1、XML文件包含一个根元素movies,以及三个子元素movie

2、每个movie元素都包含一个title元素、一个director(导演)元素、一个actors(演员)元素和一个release_date(发行日期)元素

3、在actors元素中,有两个actor元素,其内容为电影演员的姓名

<?xml version="1.0" encoding="UTF-8" ?>
<movies>
    <movie>
        <title>时空漩涡</title>
        <director>亚历克斯·卡特</director>
        <actors>
            <actor1>艾玛·华森</actor1>
            <actor2>汤姆·哈迪</actor2>
        </actors>
        <release_date>2023年5月12日</release_date>
    </movie>
    <movie>
        <title>星际探索者</title>
        <director>朱莉娅·安德森</director>
        <actors>
            <actor1>克里斯·埃文斯</actor1>
            <actor2>奥利维娅·玛恩</actor2>
        </actors>
        <release_date>2023年9月8日</release_date>
    </movie>
    <movie>
        <title>未来之影</title>
        <director>尼克·琼斯</director>
        <actors>
            <actor1>艾丽·范宁</actor1>
            <actor2>迈克尔·巴斯滕</actor2>
        </actors>
        <release_date>2023年11月21日</release_date>
    </movie>
</movies>

2 使用IDEA创建Maven多模块项目,包含3个子模块

包含三个子模块的Maven项目的示例:

练习1

在此示例中,my-project是顶级父模块,而sub-module-1、sub-module-2和sub-module-3是三个子模块。每个子模块都有自己的pom.xml文件,用于管理依赖项和构建配置。

可以在每个子模块的src目录中添加源代码和资源文件;在父模块的pom.xml文件中,可以定义每个子模块的依赖关系和构建配置。例如,可以使用元素指定所有子模块的名称,并使用元素指定所有子模块之间的依赖关系。

3 使用Java多线程Socket通讯实现的Web Server

本项目是一个使用Java多线程Socket通讯实现的Web Server,用于处理HTTP请求和响应。Web Server可以处理静态文件请求和动态页面请求。对于静态文件请求,Web Server会直接返回相应的文件内容。

Web Server使用了多线程技术,通过建立线程来管理请求,可以同时处理多个客户端请求。在接收到HTTP请求后,Web Server会分配一个独立的线程来处理该请求。为了避免阻塞其他请求的处理,每个请求都会被分配到一个独立的线程中进行处理。

Web Server的技术栈包括Java多线程技术、Java Socket编程、Java IO流。

服务端:

/**
 * @document: 服务响应
 * @Author:SmallG
 * @CreateTime:2023/8/16+18:42
 */

public class ServerBootApplication {
    //创建响应工具
    private ServerSocket serverSocket;

    /**
     * 创建启动方法
     */
    public void start() {
        //实例化serverSocket
        try {
            serverSocket = new ServerSocket(8088);
            //请求访问
            System.out.println("请求客户端访问...");
            Socket accept = serverSocket.accept();
            ClientHandler handler = new ClientHandler(accept);
            new Thread(handler).start();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建主方法
     */
    public static void main(String[] args) {
        //创建ServerBootApplication对象
        ServerBootApplication application = new ServerBootApplication();
        //启动
        application.start();
    }
}
/**
 * @document: 从客户端Socket对象中获取输入流
 * 读取HTTP请求报文;
 * 读取并解析请求行;
 * 找到静态资源,读取文件的全部内容;
 * 返回响应;关闭客户端连接。
 * @Author:SmallG
 * @CreateTime:2023/8/16+18:48
 */

public class ClientHandler implements Runnable {
    //创建Socket对象
    private Socket socket;

    /**
     * 创建构造方法
     */
    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //获取输入流,读取请求报文
            InputStream in = socket.getInputStream();
            String line = readLine();
            System.out.println(line);

            //解析请求行
            String[] s = line.split("\s");
            String method = s[0];
            String uri = s[1];
            String protocol = s[2];
            System.out.println("请求方法:" + method);
            System.out.println("请求路径:" + uri);
            System.out.println("版本号:" + protocol);

            //读取请求头
            String readHeaders;
            while (!(readHeaders = readLine()).isEmpty()) {
                System.out.println("请求头:" + readHeaders);
            }
            //发送响应
            //找到静态资源
            //类加载路径
            File root = new File(
                    ClientHandler.class.getClassLoader().getResource(".").toURI()
            );
            //定位目录
            File staticDir = new File(root,"static");
            //定位文件
            File file = new File(staticDir,uri);
            //读取文件内容
            byte[] bytes = new byte[(int) file.length()];
            FileInputStream fis = new FileInputStream(file);
            fis.read(bytes);
            fis.close();

            OutputStream out = socket.getOutputStream();
            out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write("Content-Type: text/html; charset=utf-8".getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write(("Content-Length: "+bytes.length).getBytes(StandardCharsets.UTF_8));
            out.write('\r');
            out.write('\n');
            out.write('\r');
            out.write('\n');
            out.write(bytes);
            out.close();
            in.close();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }


    }


    public String readLine() throws IOException {
        InputStream in = socket.getInputStream();
        //用于存储读取的字符
        StringBuilder builder = new StringBuilder();
        char current = 0;
        char previous = 0;
        int b = 0;
        //解析请求行
        while ((b = in.read()) != -1) {
            current = (char) b;
            if (current == '\n' && previous == '\r') {
                break;
            } else if (current != '\r' && current != '\n') {
                builder.append(current);
            }
            previous = current;
        }
        return builder.toString();
    }
}
最后修改:2023 年 08 月 16 日
如果觉得我的文章对你有用,请随意赞赏