NormanZyq
发布于 2019-11-11 / 687 阅读
0
0

2019秋 计算机网络实验1 实现HTTP代理

下面部分来自实验指导书

实验要求

实验目的

熟悉并掌握 Socket 网络编程的过程与技术;
深入理解 HTTP 协议,掌握HTTP代理服务器的基本工作原理;
掌握 HTTP 代理服务器设计与编程实现的基本技能。

实验内容

  1. 设计并实现一个基本 HTTP 代理服务器。要求在指定端口(例如8080)接收来自客户的HTTP 请求并且根据其中的 URL 地址访问该地址所指向的 HTTP 服务器(原服务器),接收 HTTP 服务器的响应报文,并将响应报文转发给对应的客户进行浏览。
  2. 设计并实现一个支持 Cache 功能的 HTTP 代理服务器。要求能缓 存原服务器响应的对象,并能够通过修改请求报文(添加 if-modified-since头行),向原服务器确认缓存对象是否是最新版本。(选作内容,加分项目,可以当堂完成或课下完成)
  3. 扩展 HTTP 代理服务器,支持如下功能:(选作内容,加分项目,
    可以当堂完成或课下完成)
    1. 网站过滤:允许/不允许访问某些网站;
    2. 用户过滤:支持/不支持某些用户访问外部网站;
    3. 网站引导:将用户对某个网站的访问引导至一个模拟网站(钓鱼)。

下面使用Java实现上述要求

1. 能够转发报文的简单HTTP代理服务器

  • 创建SocketServer.java文件,直接在main方法中执行后续操作。

  • 因为是本地代理,所以代理服务器的IP地址就是127.0.0.1,只需要另外指定监听的端口即可,然后创建一个ServerSocket的实例server等待连接:

    // 监听指定的端口
    int port = 808;
    ServerSocket server = new ServerSocket(port);
    
  • while(true)中等待连接,使用server.accept()获取这个连接并实例化一个新的Socket对象:

    Socket socket = server.accept();
    System.out.println("获取到一个连接!来自 " + socket.getInetAddress().getHostAddress());
    
  • 创建新线程,处理来自浏览器的报文。首先接收:

    // 解析header
    InputStreamReader r = new InputStreamReader(socket.getInputStream());
    BufferedReader br = new BufferedReader(r);
    String readLine = br.readLine();
    String host;
    
    StringBuilder header = new StringBuilder();
    
    while (readLine != null && !readLine.equals("")) {
        header.append(readLine).append("\n");
        readLine = br.readLine();
    }
    
  • 接下来有两个选择,第一个是把接收到的报文直接再发出去,第二是做一些处理,如果直接转发可以跳过下面两步(解析和构建)

  • 需要解析这个报文获取必要的东西就行,我只用了效率非常低的暴力办法,而且正确性不一定足够。代码稍微长点就不多贴一次了,具体见最后源码中的parse()方法。

  • 根据刚才解析到的报文头部重新构建一个发送报文:

    requestBuffer.append(method).append(" ").append(visitAddr)
        .append(" HTTP/1.1").append("\r\n")
        .append("HOST: ").append(host).append("\n")
        .append("Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\n")
        .append("Accept-Encoding:gzip, deflate, sdch\n")
        .append("Accept-Language:zh-CN,zh;q=0.8\n")
        .append("If-Modified-Since: ").append(lastModified).append("\n")
        .append("User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15\n")
        .append("Encoding:UTF-8\n")
        .append("Connection:keep-alive" + "\n")
        .append("\n");
    
  • 现在有了报文,创建一个新的Socket作为连接远程服务器的socket:

    // 创建新的socket连接远程服务器
    Socket connectRemoteSocket = new Socket(host, visitPort);
    // 这个是连接远程服务器的socket的stream
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connectRemoteSocket.getOutputStream()));
    // 发送报文
    writer.write(requestBuffer.toString()); 
    writer.flush();
    
  • 现在理一理思路,我们目前到底创建了多少个socket:

    1. 本地服务器socket,即ServerSocket的对象
    2. 用于连接这个程序和浏览器的socket,记作socket1
    3. 程序用来连接远程服务器的socket,记作socket2,和2一样都是Socket的对象。
  • 也就是说,浏览器本来应该直接跟远程服务器通信的,但是被我们拦截了下来,创建了这些socket来衔接。期间的输入输出流都是些什么鬼呢?

    1. 第一个InputStream是用于接收浏览器发送的报文,对于程序来说是一段输入,所以打开了第一个(socket1)的输入流来接收。
    2. 随后打开了连接远程服务器的socket1的输出流,因为我们要发送报文,对于程序来说是一段输出。
    3. 马上我们要打开socket2输入流用于接收服务器的应答,而且还要打开socket1的输出流,把服务器发送的报文再向浏览器输出。
  • 回到程序,刚才向服务器发送了一段报文,服务器也会应答我们,所以下面打开socket2的输入流和socket1的输出流完成这段输入输出,就算是完成了一次代理访问:

2. 加入缓存

这个思路很简单,代码见最后。

第一,要在向浏览器输出来自远程服务器的应答的同时,还要向一个文件里面输出相同的内容,这就是写缓存文件的过程;

第二,在重构报文的时候加入If-Modified-Since字段,后面跟的是GMT时间,我也用了粗暴的方式解决(不要学我,最好改进下)。这里使用缓存文件的最后修改时间作为这个时间。这个字段有什么用呢?发送这段报文给服务器之后,服务器会判断在你发送的时间之后,请求的资源是否被修改了,如果被修改,会返回状态码200 OK,并且包括最新的资源,如果返回 304 Not Modified,则意味着没有修改,后面不会跟随多余的内容,这时就要从我们创建好的缓存文件中读取内容了。

因此第三步,在服务器应答的时候多加一步,判断返回状态码:判断是200还是304来确定是否使用缓存。

3. 屏蔽访问和钓鱼请求

我使用几个Map和Set标记钓鱼网站和禁止访问的人或者资源,这个实现起来思路比缓存更简单。

遇到的问题

传输图片资源的时候会花掉,但是如果把buffer大小改成1个字节,就不会花了,原因还不太清楚……

源码

源码SocketServer.java


评论