本实验的完整代码详见:https://github.com/Zhang-Qing-Yun/network-lab

目录

  • 目的和内容
  • 原理
    • HTTP网络应用通信原理
    • HTTP代理服务器原理
  • 代码实验
    • 代理服务器启动并监听客户端连接
    • 线程池
    • 基于LRU的缓存设计
    • 加载配置文件
    • 提交给线程池的任务(主要的业务逻辑)

目的和内容

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

原理

HTTP网络应用通信原理

在HTTP网络应用中,通信的两个进程主要采用客户端/服务器模式(或浏览器/服务器模式),客户端向服务器发送请求,服务器接收到客户端请求后,向客户端提供相应的服务。通信过程如下:

服务器端:

  1. 服务器端需要首先启动,并绑定一个本地主机端口,在端口上提供服务
  2. 等待客户端请求
  3. 接收到客户端请求时,建立起与客户端通信的套接字,开启新线程,将与客户端通信的套接字放入新线程处理
  4. 返回第二步,主线程继续等待客户端请求。
  5. 关闭服务器

客户端:

  1. 根据服务器IP与端口,建立起与服务器通信的socket
  2. 向服务器发送请求报文,并等待服务器应答
  3. 请求结束后关闭socket

HTTP代理服务器原理

RFC 7230规定,代理在HTTP通信中扮演一个中间人的角色,对于连接来的客户端来说,它扮演一个服务器的角色;对于要连接的远程服务器,它扮演一个客户端的角色。代理服务器就负责在客户端和服务器之间转发报文。如下图所示:

代理服务器在指定端口监听浏览器的请求,在接收到浏览器的请求时,首先查看浏览器的IP地址,如果来自被限制的IP地址,就向客户端返回错误信息。否则,从请求头中解析出请求的host主机,如果属于不允许访问的主机,则向客户端返回错误信息,如果属于需要引导的host,则修改请求头内所有的地址字段为被引导的地址。之后,根据URL查找是否缓存中是否有该URL的缓存,如果存在,则从中取出Last-modified头部内容,并构造包含If-modified-since的请求头,向服务器发送确认最新版本的报文,并在返回的请求头第一行里确认是否有“Not Modified”,如果存在该字段,则说明本地缓存未过期,直接将本地缓存内容发送给客户端,否则缓存过期,将服务器的报文直接写回客户端。如果缓存中不存在,就直接将客户端请求转发到服务器,并将服务器返回内容缓存后,再返回给客户端。

代理服务器的拦截用户、拦截主机和钓鱼信息都预先配置在配置文件里,并在程序运行后读入程序中,以按照规则执行。

程序运行流程图如下:

代码实验

代理服务器启动并监听客户端连接

    public void start() throws IOException {//  启动服务端ServerSocket serverSocket = new ServerSocket(port);startLog();//  遇到客户端连接就创建一个任务,然后提交到线程池当中,接下来由该线程与客户端保持通信while (true) {//  监听客户端连接Socket socket = serverSocket.accept();System.out.println("接收到了"+ socket.getInetAddress() + " " + socket.getPort() + "的连接");//  创建一个任务并提交给线程池处理threadPool.execute(new ProxyTask(socket));}}

线程池

static final class MixedTargetThreadPool {//  首先从环境变量 mixed.thread.amount 中获取预先配置的线程数//  如果没有对 mixed.thread.amount 做配置,则使用常量 MIXED_MAX 作为线程数private static final int max = (null != System.getProperty(MIXED_THREAD_AMOUNT)) ?Integer.parseInt(System.getProperty(MIXED_THREAD_AMOUNT)) : MIXED_MAX;//  自定义线程池private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(MIXED_CORE,max,KEEP_ALIVE_SECONDS,TimeUnit.SECONDS,new LinkedBlockingQueue(QUEUE_SIZE),new ThreadPoolExecutor.CallerRunsPolicy());static {EXECUTOR.allowCoreThreadTimeOut(true);//  钩子函数,用来关闭线程池Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {@Overridepublic void run() {shutdownThreadPoolGracefully(EXECUTOR);}}));}
}

基于LRU的缓存设计

public class LRUCache implements Cache {/*** 默认的代理服务器要缓存的请求的最大容量*/private static final int MAX_CAPACITY = 1024;//  读写锁private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();//  读锁private final WriteLock writeLock = lock.writeLock();//  写锁private final ReadLock readLock = lock.readLock();private final LRU<String, byte[]> lru;public LRUCache() {lru = new LRU<>(MAX_CAPACITY);}public LRUCache(int maxCapacity) {lru = new LRU<>(maxCapacity);}@Overridepublic void addCache(String url, byte[] content) {writeLock.lock();try {lru.put(url, content);} finally {writeLock.unlock();}}@Overridepublic byte[] getContent(String url) {readLock.lock();try {return lru.get(url);} finally {readLock.unlock();}}/*** 具体的LRU数据结构 <br/>* 不是线程安全的,需要自行解决线程安全问题*/static class LRU<K, V> extends LinkedHashMap<K, V> {//  最大的容量private final int maxCapacity;public LRU(int maxCapacity) {//  accessOrder参数为true时,当调用get和put方法时会将访问到的元素放到双向链表的尾部super(16, 0.75f, true);this.maxCapacity = maxCapacity;}//  实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则返回true,然后会删除链表的顶端元素eldest@Overridepublic boolean removeEldestEntry(Map.Entry<K, V> eldest){return size() > maxCapacity;}}
}

加载配置文件

protected void initConfig() {InputStream inputStream = null;BufferedReader urlReader = null;BufferedReader userReader = null;BufferedReader fishingReader = null;try {//  读取主配置文件proxy.propertiesinputStream = this.getClass().getClassLoader().getResourceAsStream("proxy.properties");Properties properties = new Properties();properties.load(inputStream);//  加载主配置SingletonFactory factory = SingletonFactory.getInstance();ProxyConfig config = factory.getObject(ProxyConfig.class);config.setUrlRule(Integer.parseInt(properties.getProperty("urlRule")));config.setUserRule(Integer.parseInt(properties.getProperty("userRule")));//  设置配置的url,文件里一行就是一个urlInputStream urlStream = this.getClass().getClassLoader().getResourceAsStream("url.txt");if (urlStream != null) {urlReader = new BufferedReader(new InputStreamReader(urlStream));List<String> urls = new ArrayList<>();String line;while ((line = urlReader.readLine()) != null) {urls.add(line);}config.setUrls(urls);}//  设置配置的User即主机地址,一行就是一个地址InputStream userStream = this.getClass().getClassLoader().getResourceAsStream("user.txt");if (userStream != null) {userReader = new BufferedReader(new InputStreamReader(userStream));List<String> users = new ArrayList<>();String line;while ((line = userReader.readLine()) != null) {users.add(line);}config.setUsers(users);}//  设置要被钓鱼的用户,一行就是就是一个用户即主机地址InputStream fishingStream = this.getClass().getClassLoader().getResourceAsStream("fishing.txt");if (fishingStream != null) {fishingReader = new BufferedReader(new InputStreamReader(fishingStream));List<String> fishingUsers = new ArrayList<>();String line;while ((line = fishingReader.readLine()) != null) {fishingUsers.add(line);}config.setFishingUsers(fishingUsers);}} catch (IOException e) {e.printStackTrace();System.out.println("配置文件不存在或格式不正确");} finally {//  关闭资源if (inputStream != null) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}}if (urlReader != null) {try {urlReader.close();} catch (IOException e) {e.printStackTrace();}}if (userReader != null) {try {userReader.close();} catch (IOException e) {e.printStackTrace();}}if (fishingReader != null) {try {fishingReader.close();} catch (IOException e) {e.printStackTrace();}}}
}

提交给线程池的任务(主要的业务逻辑)

package com.qingyun.network.task;import com.google.common.primitives.Bytes;
import com.qingyun.network.cache.LRUCache;
import com.qingyun.network.config.ProxyConfig;
import com.qingyun.network.constants.ProxyConstants;
import com.qingyun.network.factory.SingletonFactory;
import com.qingyun.network.util.IOUtil;
import org.apache.commons.lang3.ArrayUtils;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;/*** @description: 具体执行代理业务的任务,目前只能做到一次请求一个TCP连接即HTTP1.0的情景* @author: 張青云* @create: 2021-10-27 20:07**/
public class ProxyTask implements Runnable {//  用于和客户端通信的TCP套接字private final Socket socket;//  用户和目的服务器建立连接的TCP套接字private Socket targetSocket;//  缓存private final LRUCache cache;//  配置信息private final ProxyConfig config;public ProxyTask(Socket socket) {this.socket = socket;cache = SingletonFactory.getInstance().getObject(LRUCache.class);config = SingletonFactory.getInstance().getObject(ProxyConfig.class);}@Overridepublic void run() {InputStream clientInputStream;OutputStream clientOutputStream;String url = null;String host = null;int port = 80;StringBuffer buffer = new StringBuffer();  // HTTP请求头的字符形式try {clientInputStream = socket.getInputStream();clientOutputStream = socket.getOutputStream();//  解析HTTP请求头String line;while ((line = IOUtil.readHttpLine(clientInputStream)) != null) {if (line.startsWith("GET")) {//  GET /index.html HTTP1.1url = line.split(" ")[1];} else if (line.startsWith("Host")) {//  Host: 127.0.0.1:80host = line.split(" ")[1];}buffer.append(line).append("\r\n");}buffer.append("\r\n");if (host == null) {//  TODO:处理没有带host字段的请求return;}//  解析地址和端口号,如果没有端口号则使用默认的80String[] split = host.split(":");host = split[0];if (split.length != 1) {port = Integer.parseInt(split[1]);}//  网站过滤if (config.getUrlRule() == ProxyConstants.ALLOW_URL) {//  如果当前访问的网站没在配置文件中则拦截if (!config.getUrls().contains(host)) {clientOutputStream.write(refuseProxy().getBytes());clientOutputStream.flush();return;}} else if (config.getUrlRule() == ProxyConstants.REFUSE_URL) {//  如果要访问的网站存在于配置文件中则拦截if (config.getUrls().contains(host)) {clientOutputStream.write(refuseProxy().getBytes());clientOutputStream.flush();return;}} else {  // 配置文件写错了clientOutputStream.write(refuseProxy().getBytes());clientOutputStream.flush();return;}//  用户过滤String clientHost = socket.getInetAddress().getHostAddress();if (config.getUserRule() == ProxyConstants.ALLOW_USER) {//  如果客户端的Host不在配置文件里拦截if (config.getUsers().contains(clientHost)) {clientOutputStream.write(refuseProxy().getBytes());clientOutputStream.flush();return;}} else if (config.getUserRule() == ProxyConstants.REFUSE_USER) {//  如果客户端的Host在配置文件里拦截if (config.getUsers().contains(clientHost)) {clientOutputStream.write(refuseProxy().getBytes());clientOutputStream.flush();return;}} else {  // 配置文件写错了clientOutputStream.write(refuseProxy().getBytes());clientOutputStream.flush();return;}//  钓鱼if (config.getFishingUsers().contains(clientHost)) {//  构造发送给钓鱼网站的HTTP报文StringBuffer fishingHTTP = new StringBuffer();fishingHTTP.append("GET " + ProxyConstants.fishingUrl + " HTTP/1.1" + "\r\n");fishingHTTP.append("Host: " + ProxyConstants.fishingHost + "\r\n");fishingHTTP.append("\r\n");String fishingHTTPStr = fishingHTTP.toString();//  建立连接然后发送数据String[] hostAndPort = ProxyConstants.fishingHost.split(":");targetSocket = new Socket(hostAndPort[0], Integer.parseInt(hostAndPort[1]));OutputStream outputStream = targetSocket.getOutputStream();outputStream.write(fishingHTTPStr.getBytes());waitTargetServerAndTransfer(clientOutputStream, targetSocket.getInputStream());return;}//  对于非GET请求的方法,直接转发给目的服务器if (url == null) {transfer(host, port, buffer, clientInputStream, clientOutputStream);return;}String uri = url;byte[] content = cache.getContent(uri);//  对于GET方法,如果缓存中存在则向目的服务器发送条件GETif (content != null) {//  从缓存中提取Last-Modified值String lastModified = parseLastModified(content);//  构造条件GET请求StringBuffer ifGetReqBuffer = new StringBuffer();ifGetReqBuffer.append("GET " + url + " HTTP/1.1\r\n");ifGetReqBuffer.append("Host: " + host + ":" + port + "\r\n");ifGetReqBuffer.append("If-modified-since: " + lastModified + "\r\n");ifGetReqBuffer.append("\r\n");String ifGetReq = ifGetReqBuffer.toString();//  向目的服务器发送targetSocket = new Socket(host, port);OutputStream outputStream = targetSocket.getOutputStream();InputStream inputStream = targetSocket.getInputStream();outputStream.write(ifGetReq.getBytes());outputStream.flush();//  阻塞式监听目的服务器的返回值String respFirstLine = IOUtil.readHttpLine(inputStream);int code = Integer.parseInt(respFirstLine.split(" ")[1]);//  缓存过期if (code != 304) {System.out.println("代理服务器对" + uri + "的缓存过期");//  将报文转发至客户端byte[] firstLine = (respFirstLine + "\r\n").getBytes();clientOutputStream.write(firstLine);byte[] resp = waitTargetServerAndTransfer(clientOutputStream, inputStream);//  将响应结果进行缓存,只缓存具有Last-Modified首部行的响应结果if (new String(resp).contains("Last-Modified")) {cache.addCache(uri, ArrayUtils.addAll(firstLine, resp));System.out.println("对" + uri + "的响应结果进行了缓存");}} else {  // 缓存命中System.out.println("对" + uri + "的访问命中缓存");//  直接返回缓存中的值clientOutputStream.write(content);clientOutputStream.flush();}} else { //  缓存不存在,则直接请求目的服务器,然后转发给客户端,并在代理服务器进行缓存System.out.println("代理服务器没有对" + uri + "请求的缓存");byte[] resp = transfer(host, port, buffer, null, clientOutputStream);//  将响应结果进行缓存,只缓存具有Last-Modified首部行的响应结果if (new String(resp).contains("Last-Modified")) {cache.addCache(uri, resp);System.out.println("对" + uri + "的响应结果进行了缓存");}}} catch (Exception e) {e.printStackTrace();} finally {try {if (socket != null) {socket.close();}if (targetSocket != null) {targetSocket.close();}} catch (IOException e) {e.printStackTrace();}}}/*** 在客户端和目标服务器之间进行转发,也就是将客户端的内容直接发送到目的服务器,然后再将目的服务器返回的内容直接转发给客户端* @param host 目标服务器主机地址* @param port 目标服务器端口号* @param head HTTP的请求头* @param body HTTP除去head后的内容的输入流* @param clientOutputStream 客户端socket的输出流* @return 目标服务器返回的相应内容*/private byte[] transfer(String host, int port, StringBuffer head, InputStream body, OutputStream clientOutputStream) throws IOException {//  和远程服务器建立连接//  TODO:有BUG,可能连不上目标服务器targetSocket = new Socket(host, port);InputStream targetServerInputStream = targetSocket.getInputStream();OutputStream targetServerOutputStream = targetSocket.getOutputStream();//  先写入请求头targetServerOutputStream.write(head.toString().getBytes());//  请求体不为null时写入请求体if (body != null) {byte[] bytes = new byte[256 * 1024];int size;// TODO:有BUG,可能读不到完整数据;但是如果while循环读的话,如果目标服务器不关闭TCP连接,则会阻塞在这里if ((size = body.read(bytes)) >= 0) {targetServerOutputStream.write(bytes, 0, size);}}targetServerOutputStream.flush();//  同步阻塞式等待目标服务器返回响应return waitTargetServerAndTransfer(clientOutputStream, targetServerInputStream);}/*** 同步阻塞式等待目标服务器返回响应,并且将响应结果直接返回给客户端* @param clientOutputStream 到客户端的输出流* @param targetServerInputStream 到目的服务器的输入流* @return 客户端的响应结果*/private byte[] waitTargetServerAndTransfer(OutputStream clientOutputStream,InputStream targetServerInputStream) throws IOException {List<byte[]> response = new ArrayList<>();byte[] bytes = new byte[256 * 1024];int length;// TODO:有BUG,可能读不到完整数据;但是如果while循环读的话,如果目标服务器不关闭TCP连接,则会阻塞在这里if ((length = targetServerInputStream.read(bytes)) >= 0) {//  写回给客户端clientOutputStream.write(bytes, 0, length);//  收集响应结果byte[] part = new byte[length];System.arraycopy(bytes, 0, part, 0, length);response.add(part);}//  将响应结果返回List<Byte> list = new LinkedList<>();for (byte[] one: response) {list.addAll(Bytes.asList(one));}return Bytes.toArray(list);}/*** 从缓存中提取Last-Modified值* @param context HTTP报文* @return Last-Modified值,如果没有则返回null*/private String parseLastModified(byte[] context) {StringBuffer headLine = new StringBuffer();for (int i = 0; i < context.length; i++) {if (context[i] == '\r') {//  请求头解析结束时都没有找到Last-Modified请求行if (headLine.length() == 0) {return null;}String s = headLine.toString();if (s.startsWith("Last-Modified")) {return s.substring(15);}i++;headLine = new StringBuffer();continue;}headLine.append((char) context[i]);}return null;}/*** 拒绝代理时向客户端返回的HTTP报文*/private String refuseProxy() {String resp = "HTTP/1.1 500 Internal Server Error\r\n";resp += "\r\n";return resp;}
}

这里只是列出了重要代码,如需查看完整代码,或者想要参考我的编程风格,请移步到该实验的代码仓库去查看,相信你一定会有收获的。(代码仓库:https://github.com/Zhang-Qing-Yun/network-lab)

实现一个HTTP代理服务器(哈工大计网实验一Java版)相关推荐

  1. java 柱状图jar_GitHub - mafulong/NetworkExper: 计网实验,抓包,java,jigloo界面开发,柱状图,文件自定义保存...

    jiWangShiYanByJava 计网实验,抓包,java,jigloo界面开发,柱状图,文件自定义保存 基于Winpcap的网络流量统计分析系统的设计与实现 一.实验内容描述 本实验是用java ...

  2. 2020计网实验报告

    title: 计网实验报告 date: 2020-12-13 16:31:07 tags: *实验名称* 实验1 WireShark的使用 *实验时间* 2020年10月7日 10:00-11:40时 ...

  3. BUAA 计网实验笔记 1

    BUAA 计网实验笔记 1 ​ - 第一周网络实验入门 尽管笔者事先预习了相关实验内容,但是,还是做了2小时的实验. (流下憨憨的眼泪,特此记录一下,后来者可留心注意下) 问题1:设备认知 在线实验平 ...

  4. 云南大学软件学院java实验九_云南大学 软件学院 计网实验

    <云南大学 软件学院 计网实验>由会员分享,可在线阅读,更多相关<云南大学 软件学院 计网实验(6页珍藏版)>请在人人文库网上搜索. 1.云南大学软件学院实 验 报 告课程: ...

  5. 【计网实验——prj4】广播网络实验

    [计网实验--prj4]广播网络实验 实验要求 1. 实现节点广播的broadcast_packet函数 2. 验证广播网络能够正常运行 • 从一个端节点ping另一个端节点 3. 验证广播网络的效率 ...

  6. 【计网实验——prj9】路由器转发实验

    [计网实验--prj9]路由器转发实验 实验要求 实验内容一 运行给定网络拓扑(router_topo.py) 在r1上执行路由器程序./router,进行数据包的处理 在h1上进行ping实验 Pi ...

  7. 3服务器是否明确返回了文件内容,云南大学软件学院计网实验2.doc

    云南大学软件学院计网实验2 云南大学软件学院 实 验 报 告 课程: 计算机网络原理实验 任课教师: 姓名: 学号: 专业: 成绩: 实验二.应用层协议分析实验报告 启动Ethereal分组俘获器.开 ...

  8. 【计网实验——prj6】生成树机制实验

    [计网实验--prj6]生成树机制实验 实验要求 1. 基于已有代码,实现生成树运行机制,对于给定拓扑(four_node_ring.py),计算输出相应状态下的最小生成树拓扑; 2. 自己构造一个不 ...

  9. BUAA 计网实验笔记 3

    BUAA 计网实验笔记 3 -第三周网络层实验 这周实验还是挺繁琐的,尤其是VLAN间通信,重点理解一下.要不然你可能也像我一样,实验4小时 实验(1) ARP分析 ARP协议是用来建立mac地址和i ...

最新文章

  1. swift支持多线程操作数据库类库-CoreDataManager
  2. 洛谷——P3807 【模板】卢卡斯定理
  3. C语言 NULL与0 对应的地址
  4. 数据库分库分表(持续更新中)
  5. webpack配置--传统多页面项目
  6. netty系列之:netty中的懒人编码解码器
  7. Java并发编程之AbstractQueuedSychronizer(抽象队列同步器,简称AQS)
  8. p1292监狱(动态规划)
  9. Spring中xml文件配置也可以配置容器list、set、map
  10. 高分影像批处理第三回——RPC文件与几何校正
  11. jquery读取xml比较js读取xml 比比就知道
  12. python turtle 画蜡笔小新_蜡笔小新有几集?作者到底怎么死的啊?
  13. html 怎么获取焦点的位置,jQuery怎么获取焦点?
  14. 分析少年派2中的Crypto
  15. 云原生之Kubernetes:24、污点和容忍度详解
  16. 小小签到获取签到列表和发送签到数据可弄自动签到
  17. JS之——解决IE6、7、8使用JSON.stringify报JSON未定义错误的问题
  18. 穿过黑暗的夜,才懂黎明的晨
  19. 瑞吉外卖(1)环境搭建
  20. 函数式编程中的副作用概念

热门文章

  1. git 申请合并冲突:rebase 解决合成一条再合并
  2. Chrome、Edge等浏览器多线程下载功能开启
  3. 自动化操作桌面之根据图片移动鼠标
  4. jQuery获取(设置)自定义属性值
  5. 顶级域名后缀列表(转)
  6. win10可用空间变成未分配_有关如何在win10系统中对未分配的磁盘空间进行分区的详细教程...
  7. 像中文的罗马音字体复制_罗马音大全可复制app中文下载
  8. ERP编制物料清单 华夏
  9. 视频编码:H.264编码
  10. selenium切换窗口句柄