计网实验A3:简单的web服务器

计网实验A3:简单的web服务器

实验介绍

首先学习面向TCP连接的套接字编程基础知识:如何创建套接字,将其绑定到特定的地址和端口,以及发送和接收数据包。其次还将学习 HTTP 协议格式的相关知识。在此基础上,本实验开发一个简单的 Web 服务器,它仅能处理一个HTTP连接请求。

相关背景介绍

Socket编程接口

要实现 Web 服务器,需使用套接字 Socket编程接口来使用操作系统提供的网络通信功能。 Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,是一组编程接口。它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。使用 Socket 后,无需深入理解 TCP/UDP 协议细节(因为Socket 已经为我们封装好了),只需要遵循 Socket 的规定去编程,写出的程序自然就是遵循 TCP/UDP 标准的。Socket 的地位如下图所示:

从某种意义上说,Socket 由地址IP和端口Port构成。IP 是用来标识互联网中的一台主机的位置,而 Port 是用来标识这台机器上的一个应用程序,IP 地址是配置到网卡上的,而 Port 是应用程序开启的,IP 与 Port 的绑定就标识了互联网中独一无二的一个应用程序。

套接字类型 流式套接字(SOCK_STREAM):用于提供面向连接、可靠的数据传输服务。 数据报套接字(SOCK_DGRAM):提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。 原始套接字(SOCK_RAW):主要用于实现自定义协议或底层网络协议。

在本 WEB 服务器程序实验中,采用流式套接字进行通信。其基本模型如下图所示:

其工作过程如下:服务器首先启动,通过调用 socket() 建立一个套接字,然后调用绑定方法 bind() 将该套接字和本地网络地址联系在一起,再调用 listen() 使套接字做好侦听连接的准备,并设定的连接队列的长度。客户端在建立套接字后,就可调用连接方法 connect() 向服务器端提出连接请求。服务器端在监听到连接请求后,建立和该客户端的连接,并放入连接队列中,并通过调用 accept() 来返回该连接,以便后面通信使用。客户端和服务器连接一旦建立,就可以通过调用接收方法 recv()/recvfrom() 和发送 方法send()/sendto() 来发送和接收数据。最后,待数据传送结束后,双方调用 close() 关闭套接字。

HTTP传输协议

超文本传输协议(HTTP)是用于Web上进行通信的协议:它定义Web浏览器如何从Web服务器请求资源以及服务器如何响应。为简单起见,在该实验中将处理HTTP协议的1.0版。HTTP通信以事务形式进行,其中事务由客户端向服务器发送请求,然后读取响应组成。 请求和响应消息共享一个通用的基本格式:

  • 初始行(请求或响应行)
  • 零个或多个头部行
  • 空行(CRLF)
  • 可选消息正文。

对于大多数常见的HTTP事务,协议归结为一系列相对简单的步骤:

首先,客户端创建到服务器的连接;然后客户端通过向服务器发送一行文本来发出请求。这请求行包HTTP方法(比如GET,POST、PUT等),请求URI(类似于URL),以及客户机希望使用的协议版本(比如HTTP/1.0);接着,服务器发送响应消息,其初始行由状态线(指示请求是否成功),响应状态码(指示请求是否成功完成的数值),以及推理短语(一种提供状态代码描述的英文消息组成);最后一旦服务器将响应返回给客户端,它就会关闭连接。

实验功能要求

Web 服务器的基本功能是接受并解析客户端的 HTTP 请求,然后从服务器的文件系统获取所请求的文件,生成一个由头部和响应文件内容所构成成的 HTTP 响应消息,并将该响应消息发送给客户端。如果请求的文件不存在于服务器中,则服务器应该向客户端发送“404 Not Found”差错报文。 具体的过程和步骤分为:

  1. 当一个客户(浏览器)连接时,创建一个连接套接字;
  2. 从这个连接套接字接收 HTTP 请求;
  3. 解释该请求以确定所请求的特定文件;
  4. 从服务器的文件系统获得请求的文件;
  5. 创建一个由请求的文件组成的 HTTP 响应报文,报文前面有首部行;
  6. 经 TCP 连接向请求浏览器发送响应;
  7. 如果浏览器请求一个在该服务器中不存在的文件,服务器应当返回一个“404 Not Found”差错报文。

总体设计

首先开启服务器,并等待连接请求;浏览器请求访问服务器及其文件,服务器检查请求报文是否包含了关闭指令,若是,则程序停止运行;若否,则检查服务器内是否包含该文件;如果包含,则将请求文件放入输出流返回给浏览器,否则返回“404 NOT FOUND”。

详细设计

数据结构设计

这里不做复杂的数据结构处理,主要分为两个类对象:HttpServer 以及WebServerWebServer主要是做的监听的内容,HttpServer做的是处理结果,返回目标的内容。

HttpServer继承自线程类,会生成一个线程,独立于主线程进行处理,可以实现一定的并发的能力。

函数分析

监听,并且建立线程处理,这里我们的HttpServer就是进行处理的线程,我们的主进程一直监听内容,但监听到事件时候,生产子线程,进行内容的处理。

    public void startServer(int port){
    
    
        try {
    
    
            @SuppressWarnings("resource")
            ServerSocket serverSocket = new ServerSocket(port); // 绑定端口
            while(true){
    
    
                //服务器套接字
                Socket socket = serverSocket.accept();
                // serverSocket.accept():服务器接受客户端的连接请求,并返回一个套接字,客户机通过此套接字与服务器通信。
                // 如果未连接到客户端,线程处于阻塞状态,程序无法执行下去。
                new HttpServer(socket).start(); {
    
    
                // 先新建一个HttpServer的对象,然后此对象调用HttpServer类的start()方法,把套接字放进去当作参数
                };
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();  // 出现异常的应对措施
        }
    }

我们对于请求的处理,会生成一个线程来进行处理,在这个新的线程中,我们通过套接字,获取其中的输入输出流对象,然后进行数据的传输。

    //     多线程方法调用
    @Override
    public void run() {
    
    
        // read方法和response方法定义在下面,此处直接调用封装好的方法。
        String filePath = read(); //  返回文件名,提供给response方法作参数
        response(filePath);
    }

我们通过read函数,解析我们的报文的主要内容,首先是了解到我们的了解到我们文件的所在位置,从报文首部解析出url,也就是之后通过url获得资源路径。

    //解析web发出的请求的内容
    private String read() {
    
    
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        // BufferedReader类从字符输入流中读取文本并缓冲字符,以便有效地读取字符,数组和行
        try {
    
    
            // 读取请求头, 如:GET /index.html HTTP/1.1
            String readLine = reader.readLine();
            // 读HTTP请求报文(请求行):第一行,第一个是方法,第二个是URL,第三个是http协议版本
            String[] split = readLine.split(" ");
            if (split.length != 3) {
    
      // split 字符串数组必须要有三部分
                return null;
            }
            System.out.println(readLine);  // 打印HTTP请求报文的第一行(请求行)  后面是首部行和实用主体
            String path= split[1];  // path = /index.html 文件名

            //如果要读取并打印所有请求内容
//            while(readLine!=null)
//            {
    
    
//                System.out.println(readLine);
//                readLine=reader.readLine();
//            }
            return path; // 方法将返回文件名(/index.html)
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

然后通过这个资源路径,访问我们的文件内容,以字符串的形式进行读取,返回给我们的客户端。如果我们没有进行资源的定位,那么我们就进行基础页面的返回给我们的客户端。否则由项目根目录向下查找到我们的资源,将资源内部的内容以字符的形式读入,构建返回头部返回给目标客户端。

    private void response(String filePath) {
    
    
        // 获得项目下的路径
        String path = System.getProperty("user.dir");
        // 默认定位到的当前用户目录("user.dir")(即工程根目录)
        // JVM就可以据"user.dir" + "自己设置的目录" 得到完整的路径(即绝对路径)
        String x;
        x=filePath.replaceAll("/","");
        StringBuffer str=new StringBuffer(x);
        str.insert(0,"\\");
        System.out.println(str);
        File file = new File(path + str);
        // filePath是我们用输入流读取到的文件名;所以file就是申请的文件路径

        // 在当前根目录下查找浏览器申请的文件是否存在
        // 三个情况:1.默认返回,2.404 NOT FOUND,3.index.html
        if (!filePath.equals("/")) {
    
      // 如果只有主机号和端口,没有文件路径,就返回默认文件
            if (file.exists()) {
    
    
                // 1、资源存在,读取资源
                try {
    
    
                    // 将文件里的响应正文放入字符串变量sb内
                    BufferedReader reader = new BufferedReader(new FileReader(file));
                    System.out.println("reader" + reader);
                    // 输出文件输入流的信息,只有浏览器申请到的文件存在才有这条信息(readerjava.io.BufferedReader)
                    StringBuffer sb = new StringBuffer();
                    // 字符变量
                    String line = null;
                    while ((line = reader.readLine()) != null) {
    
     // 逐行去读取文件
                        sb.append(line).append("\r\n");
                        // 文件里逐行加入sb字符变量里(尤其注意要加回车符和换行符)
                    }
                    // 将文件里的响应正文放入字符串变量sb内
                    // 填写HTTP响应报文到字符串变量result内
                    StringBuffer result = new StringBuffer();
                    // result是HTTP响应,其格式为:状态行、响应头部,(回车符,换行符),响应正文
                    // 其中第一行==状态行,状态行分别为: 协议版本,空格,状态码,空格,状态码描述,回车符,换行符
                    result.append("HTTP /1.1 200 ok \r\n"); // HTTP /1.1是协议版本,状态码是200(正常),状态码描述是ok
                    // 下面是响应头部的各个字段名和值
                    result.append("Content-Type:text/html \r\n");  // 内容类型
                    result.append("Content-Length:" + file.length() + "\r\n");  // 文件内容字节数
                    // sb是响应消息正文
                    result.append("\r\n" + sb.toString());
                    //System.out.println("result=="+result);
                    out.write(result.toString().getBytes());  // 输出result的信息回复给浏览器
                    out.flush();
                    out.close();
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }

            } else {
    
    
                // 2、资源不存在,提示 file not found;也要反馈HTTP响应
                StringBuffer error = new StringBuffer();
                // HTTP响应:状态行
                error.append("HTTP /1.1 400 file not found \r\n");
                // 响应头部 + (回车符,状态行)
                error.append("Content-Type:text/html \r\n");
                error.append("Content-Length:20 \r\n").append("\r\n");
                // 响应正文
                error.append("<h1 >404 Not Found  </h1>");
                try {
    
    
                    out.write(error.toString().getBytes());  // 输出流回复浏览器
                    out.flush();
                    out.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }else {
    
    
            //默认界面
            StringBuffer error = new StringBuffer();
            error.append("HTTP /1.1 200 hello world \r\n");
            error.append("Content-Type:text/html \r\n");
            error.append("Content-Length:20 \r\n").append("\r\n");
            error.append("<h1 >hello world</h1>");
            try {
    
    
                out.write(error.toString().getBytes());  // 回复浏览器
                out.flush();
                out.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

调试设计

这里测试访问在项目目录下的主页文件,同时访问该端口的默认文件,之后再测试一下为未存在的页面的nei’r

运行结果

默认访问:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DAUFRgb-1678758291546)(…/实验截图/A3/2022-12-12 13_29_08-.png)]

访问未存在文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bxnBfsXj-1678758291546)(…/实验截图/A3/2022-12-12 13_29_45-.png)]

访问存在文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oj4T7uja-1678758291547)(…/实验截图/A3/2022-12-12 13_30_06-.png)]

后台日志结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZfBvg3Kz-1678758291547)(…/实验截图/A3/2022-12-12 13_30_20-.png)]

实验总结

困难与解决

异常返回输出值:

Web服务端本来应该只接收到一次请求报文,但是却收到两条报文,第二条报文请求的是/favicon.ico 文件,可以看到页签上标题栏前面是个地球,这个是默认的。原因就是后台给的响应里没有指定这个图标,浏览器的第二次请求,就是请求这个资源。

这里没有对此进行的二次处理。

心得与思考

本次计算机网络课程的实验令我受益匪浅。首先,面对纲领性的课程的学习,不仅仅要把书本上,课堂上的内容掌握,理解许多抽象的知识,还应该努力去了解其实际如何发挥作用的,在实践中去学习,印象非常深刻,而且对以后的学习会很有意义。其次,了解和尝试计算机网络相关编程的工具是非常有必要的,例如相关的软件,相关的库,相关的类,这样的学习可以帮助我们拓展知识面,虽然无法全面地掌握,但是有了粗略的了解之后,这一块的知识就在实际需要的时候被调动出来。本次实验就加强了我们的编程能力,让我拓展学习了许多课程相关的内容。最后,我通过本次实验发现计算机网络是一个有很多细节的研究方向,既需要全局的了解,又需要局部的精通,其中不乏很多可以继续提升的地方,也许我们能凭借自己的努力,达到更高的层次水平,为将来的计算机网络发展做出一些微薄的贡献,这会对整个人类社会产生很积极的影响。

猜你喜欢

转载自blog.csdn.net/interval_package/article/details/129517488