下面部分来自实验指导书
实验要求
实验目的
熟悉并掌握 Socket 网络编程的过程与技术;
深入理解 HTTP 协议,掌握HTTP代理服务器的基本工作原理;
掌握 HTTP 代理服务器设计与编程实现的基本技能。
实验内容
- 设计并实现一个基本 HTTP 代理服务器。要求在指定端口(例如8080)接收来自客户的HTTP 请求并且根据其中的 URL 地址访问该地址所指向的 HTTP 服务器(原服务器),接收 HTTP 服务器的响应报文,并将响应报文转发给对应的客户进行浏览。
- 设计并实现一个支持 Cache 功能的 HTTP 代理服务器。要求能缓 存原服务器响应的对象,并能够通过修改请求报文(添加 if-modified-since头行),向原服务器确认缓存对象是否是最新版本。(选作内容,加分项目,可以当堂完成或课下完成)
- 扩展 HTTP 代理服务器,支持如下功能:(选作内容,加分项目,
可以当堂完成或课下完成)- 网站过滤:允许/不允许访问某些网站;
- 用户过滤:支持/不支持某些用户访问外部网站;
- 网站引导:将用户对某个网站的访问引导至一个模拟网站(钓鱼)。
下面使用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:
- 本地服务器socket,即
ServerSocket
的对象 - 用于连接这个程序和浏览器的socket,记作socket1
- 程序用来连接远程服务器的socket,记作socket2,和2一样都是
Socket
的对象。
- 本地服务器socket,即
-
也就是说,浏览器本来应该直接跟远程服务器通信的,但是被我们拦截了下来,创建了这些socket来衔接。期间的输入输出流都是些什么鬼呢?
- 第一个InputStream是用于接收浏览器发送的报文,对于程序来说是一段输入,所以打开了第一个(socket1)的输入流来接收。
- 随后打开了连接远程服务器的socket1的输出流,因为我们要发送报文,对于程序来说是一段输出。
- 马上我们要打开socket2输入流用于接收服务器的应答,而且还要打开socket1的输出流,把服务器发送的报文再向浏览器输出。
-
回到程序,刚才向服务器发送了一段报文,服务器也会应答我们,所以下面打开socket2的输入流和socket1的输出流完成这段输入输出,就算是完成了一次代理访问:
2. 加入缓存
这个思路很简单,代码见最后。
第一,要在向浏览器输出来自远程服务器的应答的同时,还要向一个文件里面输出相同的内容,这就是写缓存文件的过程;
第二,在重构报文的时候加入If-Modified-Since
字段,后面跟的是GMT时间,我也用了粗暴的方式解决(不要学我,最好改进下)。这里使用缓存文件的最后修改时间作为这个时间。这个字段有什么用呢?发送这段报文给服务器之后,服务器会判断在你发送的时间之后,请求的资源是否被修改了,如果被修改,会返回状态码200 OK,并且包括最新的资源,如果返回 304 Not Modified,则意味着没有修改,后面不会跟随多余的内容,这时就要从我们创建好的缓存文件中读取内容了。
因此第三步,在服务器应答的时候多加一步,判断返回状态码:判断是200还是304来确定是否使用缓存。
3. 屏蔽访问和钓鱼请求
我使用几个Map和Set标记钓鱼网站和禁止访问的人或者资源,这个实现起来思路比缓存更简单。
遇到的问题
传输图片资源的时候会花掉,但是如果把buffer大小改成1个字节,就不会花了,原因还不太清楚……
评论区