实现一个HTTP代理服务器(哈工大计网实验一Java版)
本实验的完整代码详见:https://github.com/Zhang-Qing-Yun/network-lab
目录
- 目的和内容
- 原理
- HTTP网络应用通信原理
- HTTP代理服务器原理
- 代码实验
- 代理服务器启动并监听客户端连接
- 线程池
- 基于LRU的缓存设计
- 加载配置文件
- 提交给线程池的任务(主要的业务逻辑)
目的和内容
- 设计并实现一个基本HTTP 代理服务器。要求在指定端口(例如8080)接收来自客户的HTTP 请求并且根据其中的URL 地址访问该地址所指向的HTTP 服务器(原服务器),接收HTTP 服务器的响应报文,并将响应报文转发给对应的客户进行浏览。
- 设计并实现一个支持Cache 功能的HTTP 代理服务器。要求能缓存原服务器响应的对象,并能够通过修改请求报文(添加if-modified-since头行),向原服务器确认缓存对象是否是最新版本。
- 扩展HTTP 代理服务器,支持如下功能:
a) 网站过滤:允许/不允许访问某些网站;
b) 用户过滤:支持/不支持某些用户访问外部网站;
c) 网站引导:将用户对某个网站的访问引导至一个模拟网站(钓鱼)。
原理
HTTP网络应用通信原理
在HTTP网络应用中,通信的两个进程主要采用客户端/服务器模式(或浏览器/服务器模式),客户端向服务器发送请求,服务器接收到客户端请求后,向客户端提供相应的服务。通信过程如下:
服务器端:
- 服务器端需要首先启动,并绑定一个本地主机端口,在端口上提供服务
- 等待客户端请求
- 接收到客户端请求时,建立起与客户端通信的套接字,开启新线程,将与客户端通信的套接字放入新线程处理
- 返回第二步,主线程继续等待客户端请求。
- 关闭服务器
客户端:
- 根据服务器IP与端口,建立起与服务器通信的socket
- 向服务器发送请求报文,并等待服务器应答
- 请求结束后关闭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版)相关推荐
- java 柱状图jar_GitHub - mafulong/NetworkExper: 计网实验,抓包,java,jigloo界面开发,柱状图,文件自定义保存...
jiWangShiYanByJava 计网实验,抓包,java,jigloo界面开发,柱状图,文件自定义保存 基于Winpcap的网络流量统计分析系统的设计与实现 一.实验内容描述 本实验是用java ...
- 2020计网实验报告
title: 计网实验报告 date: 2020-12-13 16:31:07 tags: *实验名称* 实验1 WireShark的使用 *实验时间* 2020年10月7日 10:00-11:40时 ...
- BUAA 计网实验笔记 1
BUAA 计网实验笔记 1 - 第一周网络实验入门 尽管笔者事先预习了相关实验内容,但是,还是做了2小时的实验. (流下憨憨的眼泪,特此记录一下,后来者可留心注意下) 问题1:设备认知 在线实验平 ...
- 云南大学软件学院java实验九_云南大学 软件学院 计网实验
<云南大学 软件学院 计网实验>由会员分享,可在线阅读,更多相关<云南大学 软件学院 计网实验(6页珍藏版)>请在人人文库网上搜索. 1.云南大学软件学院实 验 报 告课程: ...
- 【计网实验——prj4】广播网络实验
[计网实验--prj4]广播网络实验 实验要求 1. 实现节点广播的broadcast_packet函数 2. 验证广播网络能够正常运行 • 从一个端节点ping另一个端节点 3. 验证广播网络的效率 ...
- 【计网实验——prj9】路由器转发实验
[计网实验--prj9]路由器转发实验 实验要求 实验内容一 运行给定网络拓扑(router_topo.py) 在r1上执行路由器程序./router,进行数据包的处理 在h1上进行ping实验 Pi ...
- 3服务器是否明确返回了文件内容,云南大学软件学院计网实验2.doc
云南大学软件学院计网实验2 云南大学软件学院 实 验 报 告 课程: 计算机网络原理实验 任课教师: 姓名: 学号: 专业: 成绩: 实验二.应用层协议分析实验报告 启动Ethereal分组俘获器.开 ...
- 【计网实验——prj6】生成树机制实验
[计网实验--prj6]生成树机制实验 实验要求 1. 基于已有代码,实现生成树运行机制,对于给定拓扑(four_node_ring.py),计算输出相应状态下的最小生成树拓扑; 2. 自己构造一个不 ...
- BUAA 计网实验笔记 3
BUAA 计网实验笔记 3 -第三周网络层实验 这周实验还是挺繁琐的,尤其是VLAN间通信,重点理解一下.要不然你可能也像我一样,实验4小时 实验(1) ARP分析 ARP协议是用来建立mac地址和i ...
最新文章
- swift支持多线程操作数据库类库-CoreDataManager
- 洛谷——P3807 【模板】卢卡斯定理
- C语言 NULL与0 对应的地址
- 数据库分库分表(持续更新中)
- webpack配置--传统多页面项目
- netty系列之:netty中的懒人编码解码器
- Java并发编程之AbstractQueuedSychronizer(抽象队列同步器,简称AQS)
- p1292监狱(动态规划)
- Spring中xml文件配置也可以配置容器list、set、map
- 高分影像批处理第三回——RPC文件与几何校正
- jquery读取xml比较js读取xml 比比就知道
- python turtle 画蜡笔小新_蜡笔小新有几集?作者到底怎么死的啊?
- html 怎么获取焦点的位置,jQuery怎么获取焦点?
- 分析少年派2中的Crypto
- 云原生之Kubernetes:24、污点和容忍度详解
- 小小签到获取签到列表和发送签到数据可弄自动签到
- JS之——解决IE6、7、8使用JSON.stringify报JSON未定义错误的问题
- 穿过黑暗的夜,才懂黎明的晨
- 瑞吉外卖(1)环境搭建
- 函数式编程中的副作用概念