计算机网络实验3——基于TCP的多线程Web Server服务器的实现

一、实验目的

  1. 了解掌握网络编程的基本概念和原理,包括套接字编程、传输控制协议(TCP)、IP地址、端口等。
  2. 了解掌握多线程编程,学习如何使用多线程来处理并发连接。
  3. 熟悉HTTP协议,理解Web应用程序的基本工作原理。

二、实验原理

1)TCP/IP协议

  1. 可靠性:TCP 是一种可靠的协议,它确保数据在通信中可靠传输。其使用序列号和确认机制来跟踪数据包的传输和接收,以确保数据不会丢失、损坏或乱序。
  2. 面向连接:TCP 是一种面向连接的协议,在通信之前,通信双方需要建立连接,连接的建立包括"三次握手"等。当成功建立连接后,数据就可以进行双向传输。
  3. 流量控制:TCP 使用流量控制来确保发送方不会向接收方发送太多数据,以防止接收方无法处理。
  4. 数据分段:TCP 将应用程序传输的数据分成小块(称为数据段)以便传输。每个数据段都带有序列号,以便接收方可以重组它们,并确保数据按正确的顺序传输。
  5. 数据包重传:如果接收方没有确认数据段的接收,发送方会重新发送它们。这确保了数据的可靠传输。
  6. 端口:TCP 使用端口来标识通信的应用程序。例如:在服务器上,HTTP通常使用端口80,在客户端和服务器之间建立连接时,双方将使用此端口进行通信。
  7. 双向通信:TCP 允许双向通信,即客户端和服务器都可以发送和接收数据。

2)HTTP协议

HTTP 是一种客户端-服务器架构的协议,其中客户端发送请求,而服务器响应请求.HTTP通信遵循请求-响应模型。客户端发送HTTP请求,服务器接收并解析请求,然后返回HTTP响应。

HTTP请求包括一个请求方法,例如:

  1. GET:用于请求获取资源。
  2. POST:用于提交数据给服务器。
  3. PUT:用于上传文件或数据。
  4. DELETE:用于删除资源。

HTTP请求包括请求头,用于传递额外的信息,例如:

  1. Host:指定主机名。
  2. User-Agent:标识客户端的用户代理(通常是浏览器)。
  3. Accept:指定客户端能够接受的媒体类型(MIME类型)。
  4. 其他自定义头部。

HTTP响应包括一个状态码,指示请求的结果,例如:

  1. 200 OK:请求成功,服务器返回所请求的资源。
  2. 404 Not Found:未找到请求的资源。
  3. 500 Internal Server Error:服务器内部错误。

HTTP响应包括响应头,提供关于响应的额外信息,例如:

  1. Content-Type:指定响应体的媒体类型。
  2. Content-Length:指定响应体的长度。
  3. Location:用于重定向。

3) 多线程编程

在多线程编程中,每个线程可以独立执行不同的任务,但它们共享相同的进程资源,如内存空间。多线程编程旨在实现并发,允许多个任务并发执行,以提高性能和资源利用率。

使用多线程目的为了提高程序的执行效率,防止用户等待时间过长,一般把一些耗时较长且无需知道返回结果的程序做成多线程异步处理

常见实现多线程的有以下实现方式 :

  1. 继承Thread类并实现run()方法
  1. 定义一个类、继承Thread类
  2. 在这个类里面重写run()方法
  3. 创建这个类的对象
  4. 启动线程

优点:

 优点继承Thread类方式编写简单,如果需要访问当前线程无需用 Thread.currentThread()方法 直接使用this,即可获得当前线程

缺点:

 因为线程类已经继承了Thread类,所以不能再继承其他的父类,线程类已经继承了Thread类,不能再继承其他类(java的单继承性),因此该方式不够灵活。

  1. 实现Runnable接口实现run()方法
  1. 定义一个类MyRunnable类实现Runnable接口
  2. 在这个自己定义类里面重写run()方法
  3. 创建Thread类的对象,把MyRunnable类作为构造方法的参数
  4. 启动线程

优点:

线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

缺点:

编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

4)文件操作

        在多线程 Web 服务器中,文件操作用于读取服务器上的静态资源文件,如 HTML、CSS、JavaScript 或图像文件,并将其发送给客户端作为 HTTP 响应。服务器需要打开文件、读取文件内容,并将文件内容写入到响应中,以便客户端能够下载或浏览这些资源。文件操作涉及访问和管理操作系统中的文件系统,通常包括读取(读取文件内容)和写入(将数据写入文件)。

        文件描述符是操作系统分配给文件的唯一标识符。在文件操作中,可以使用文件描述符来引用文件。要访问文件,首先需要打开文件。打开文件操作允许应用程序获取文件描述符,以便后续的文件操作。在文件操作完成后,需要关闭文件以释放资源。未关闭的文件可能会导致资源泄漏。读取文件操作涉及从文件中读取数据,可以通过文件描述符或文件路径来完成。读取文件时,数据被读入内存供应用程序使用。写入文件操作涉及将数据写入文件,可以通过文件描述符或文件路径来完成。写入文件时,数据被写入文件的末尾或指定的位置。文件操作可能会引发错误,如文件不存在、权限不足、磁盘空间不足等。应用程序需要处理这些错误并采取适当的措施。

三、实验分析 

此实验主要完成以下三个需求:

  1. 实现服务器:实现一个基于TCP的多线程Web服务器,能够接受HTTP请求、解析请求、处理请求,并发送HTTP响应。
  2. 实现静态资源处理:服务器需要能够读取并发送静态资源文件,如HTML、CSS、图像等。
  3. 实现并发连接:服务器要能够处理多个并发连接,每个连接由一个独立的线程来处理。

四、实验设计 

程序运行流程图:

  1. 创建一个名为 WebServer 的主类,并定义了一个常量 WEB_ROOT,它表示 Web 服务器的根目录路径。在 main 方法中,服务器创建一个 ServerSocket 并监听端口 8080。然后,可以无限循环来接受客户端连接。每当有客户端连接到服务器时,会创建一个新的线程来处理请求,实现多线程
  2. RequestHandler 类,用于处理每个客户端的请求。当一个客户端连接到服务器时,会创建一个 RequestHandler 线程来处理该客户端的请求。在此类中进行获取客户端的输入流和输出流、创建一个 Request 对象并解析客户端请求、创建一个 Response 对象并将其关联到请求、使用 Response 对象发送静态资源作为响应给客户端等操作
  3. Response 类用于处理响应操作。包含setRequest 方法用于将请求与响应相关联和sendStaticResource 方法用于发送静态资源作为响应。它首先检查请求的文件是否存在,如果存在则读取文件的内容并发送给客户端。如果文件不存在,它会发送一个包含 "404  Not Found" 错误信息的响应。
  4. Request 类用于解析客户端的 HTTP 请求。包含parse 方法用于解析客户端请求。它读取请求的数据,并从中提取请求的 URI。parseUri 方法用于从请求字符串中提取 URI。

五、实验过程 

在WebServer主类中

WEB_ROOT常量:定义了服务器的根目录,通常是服务器文件系统中的一个目录,用于存放静态资源文件,如HTML文件、图像等。

main方法:创建一个ServerSocket 对象,用于监听端口 8080 上的连接请求。然后进入一个无限循环,等待客户端连接。while循环:服务器使用一个循环来接受客户端连接。每当有客户端连接请求到达,server.accept() 方法会返回一个 Socket 对象,表示与客户端的通信通道。 

RequestHandler 类:每当有新的客户端连接时,服务器会创建一个新的线程来处理这个连接。RequestHandler 是一个实现了 Runnable 接口的内部类,用于处理单个客户端的请求。RequestHandler 构造函数:接收一个 Socket 对象,表示与客户端的通信通道。

run 方法(在 RequestHandler 内部类中):在新线程中执行,处理客户端的请求。它执行以下主要步骤:

  1. 打印客户端的IP地址和端口信息。
  2. 创建输入流 in 和输出流 out,用于与客户端进行数据交换。
  3. 创建 Request 对象,用于解析客户端的HTTP请求。
  4. 创建 Response 对象,用于生成HTTP响应。
  5. 调用 sendStaticResource() 方法,将静态资源(如HTML文件)发送给客户端。
  6. 最后关闭输入流、输出流和与客户端的连接。

 

Request类,用于解析HTTP请求的内容和提取请求中的URI

private InputStream input:类中的私有成员变量,用于接收HTTP请求的输入流,这个输入流通常来自于与客户端的Socket连接。构造函数 public Request(InputStream input):构造函数接受一个 InputStream 对象,用于初始化 Request 实例。

parseUri(String requestString) 方法:这个私有方法用于从HTTP请求信息中提取URI(请求的资源路径)。具体步骤包括:

  1. 查找请求字符串中的第一个空格的位置(index1)。
  2. 如果找到了第一个空格,继续查找下一个空格的位置(index2),以找到URI的结束位置。
  3. 使用 substring 方法截取中间部分,即请求的URI,然后返回。

 

 构造函数 public Response(OutputStream output):构造函数接受一个 OutputStream 对象,用于初始化 Response 实例,以便后续向客户端发送响应。

 

sendStaticResource() 方法:这是生成并发送HTTP静态资源的主要方法。主要步骤:

  1. 创建一个字节数组 bytes,用于存储静态资源的内容。数组大小为 1024*1024 字节,即 1 MB。这个大小适用于假定响应内容不会超过 1 MB 的情况
  2. 获取请求对象的URI(request.getUri()),并构建出完整的文件路径,以便查找静态资源文件
  3. 检查文件是否存在(file.exists())。如果文件存在,打开文件输入流 FileInputStream(fis),并尝试读取文件内容到字节数组 bytes。
  4. 构建HTTP响应头,包括响应状态行(200 OK)和响应头字段(Content-Type 和 Content-Length)。使用输出流 output 将使用输出流 output 将响应头发送到客户端。、响应头发送到客户端。
  5. 如果文件不存在,生成一个HTTP 404 错误响应,包括错误消息和状态行,并将其发送到客户端。

 

进行测试:

访问存在的静态资源文件 

访问不存在的静态资源文件  

 六、结论与分析

遇到的问题1:最初版的服务器是单线程的,采用了阻塞式的方式来处理客户端请求,也就是在接受一个请求并处理完它之前,不会接受其他请求。

  1. 解决方法:定义一个RequestHandler类实现Runnable接口。每次接受客户端连接请求时,都会创建一个新的线程来处理该请求。这样,不同的客户端请求可以并行处理,提高了服务器的并发性能。

 

遇到的问题2:通过服务器访问已存在的静态资源文件时,显示找不到此localhost页面 

可能出现的原因:静态资源文件的路径设置不正确,构建文件路径时没有使用正确的文件路径分隔符,HTTP的响应状态行和头字段设置错误等原因

解决方法:对于可能出现的原因,发现是静态资源文件的路径设置不正确的原因

遇到的问题3:对该过程的HTTP请求过程进行分析

运行程序,在浏览器输入localhost:8080/index.html,进行静态资源文件访问,并用wireshark对该操作进行抓包,分析http请求过程

1)浏览器构建请求行信息,构建好后,浏览器准备发起网络请求

GET /index.html  HTTP/1.1

2)浏览器通过TCP与服务器建立连接

浏览器发送连接请求,带上SYN = 1 seq= 0的字段信息,此时发送方状态为同步已发送

 其次,接收方接受请求后,回复连接请求,带上SYN = 1 ACK = 1 seq = 0 ack =  1的字段信息,此时接收方的状态为同步已接收

  最后,发送方接受响应后,回复带上ACK = 1 seq = 1 ack =  1的字段信息,此时,发送方的状态为建立连接状态,而接收方的状态得等发送方这时的回复收到后再转为建立连接。

 3)浏览器发送HTTP请求

4) 服务器响应请求

 在分析过程中还发现

        通过查找相关资料,了解到HTTP/2引入"CONTINUATION"帧的主要目的是提高HTTP通信的性能和效率。这些帧的使用可以帮助减少网络延迟和降低冗余传输的头信息,以使Web页面加载更快。 

附实验代码:

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

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

    public static void main(String[] args) {
        try {
            ServerSocket server = new ServerSocket(8080);

            while (true) {
                Socket socket = server.accept();
                // 在每次连接时创建一个新线程来处理请求
                Thread thread = new Thread(new RequestHandler(socket));
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class RequestHandler implements Runnable {
        private Socket socket;

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

        @Override
        public void run() {
            try {
                System.out.println(socket.getInetAddress() + ":" + socket.getPort());
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();

                Request request = new Request(in);
                request.parse();

                Response response = new Response(out);
                response.setRequest(request);
                response.sendStaticResource();

                in.close();
                out.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Request

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

public class Request {
    private InputStream input;
    private String uri;

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

    public void parse() {
        int i;
        byte[] buf = new byte[1048];
        try {
            i = input.read(buf);
        }
        catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        String str=new String(buf);
        //在控制台输出请求信息
        System.out.print(str);
        //截取请求Url
        uri = parseUri(str);
        //在控制台输出请求Url,看是否正确
        // System.out.println(uri);
    }

    //从请求信息中截取请求Url,即首行的中间部分
    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 + 2, index2);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }
}

 Response

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

public class Response {
    Request request;
    OutputStream output;

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

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

    //1.加响应头
    //2.读取文件
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[1024*1024];//响应的内容假如不超过1024*1024个字节
        FileInputStream fis = null;
        try {
            System.out.println(WebServer.WEB_ROOT+ File.separator+request.getUri());
            File file = new File(WebServer.WEB_ROOT+ File.separator+request.getUri());
            if (file.exists()) {
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, 1024*1024);
                String str="http/1.1 200 ok\r\n\r\n";
                output.write(str.getBytes());
                output.write(bytes, 0, ch);
            }
            else {
                //如果没有发现文件,返回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>404 Not Found</h1>";
                output.write(errorMessage.getBytes());
            }
        }
        catch (Exception e) {
            System.out.println(e.toString() );
        }
        finally {
            if (fis!=null)
                fis.close();
        }
    }
}

<html>
<head>
    <title>Welcome</title>
</head>
<body>
<br>
<h1>Hello!</h1>
</audio>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值