計算機網路--http代理伺服器的設計與實現
一、Socket程式設計的客戶端和服務端的主要步驟:
Java Socket程式設計:對於http傳輸協議
客戶端:
1、建立新的socket,繫結伺服器host和埠號
2、Socket建立成功後獲得相應的輸出流
3、將請求報文通過輸出流傳到伺服器,記得flush()重新整理快取
4、建立該socket所對應的輸入流,獲取伺服器的相應報文
服務端:
1、通過建立相應埠的socket實現監聽某埠的socket請求
2、當有別的socket請求連線就開始監聽socket的資訊,接收到請求報文
3、根據對請求報文的解析,得到請求者的url、埠還有請求資訊
4、將響應資訊還有必要的頭部連線形成響應報文,通過
二、HTTP代理伺服器的基本原理:
代理伺服器,即作為真實伺服器的一個代理端,客戶端的請求資訊不是傳送的真實請求的伺服器而是傳送的代理伺服器,此時代理伺服器是作為一個伺服器,之後代理伺服器通過解析客戶端的請求資訊,再向真實伺服器傳送請求報文,獲得請求的資訊,此時代理伺服器是作為一個客戶端。
使用代理伺服器的好處是:
1、在請求客戶端和真實伺服器之間添加了一層,這樣就可控的對於請求的響應報文做一些限制或者是改變,例如網站過濾、釣魚網站等使得響應到客戶端的資訊是代理伺服器處理過的;
2、還有就是請求報文先發送到代理伺服器,這樣代理伺服器可以設立快取,通過對請求報文解析後代理伺服器可以通過查詢本地快取,如果有快取好的,並且通過向伺服器傳送是否更新的資訊後得到沒有修改後就可以直接從代理伺服器將響應報文返回給客戶端,這樣減少了服務端的負載,減少了流量
三、HTTP代理伺服器的程式流程圖:
中間代理伺服器可以設定對請求報文和響應報文做一些修改
四、實現HTTP代理伺服器的關鍵技術及解決方案
1、關鍵技術:socket程式設計傳送和接受報文
由於http的請求和響應報文都有特定的格式,所以一旦對於報文的格式理解錯誤就不能獲得正確的響應,例如:對於請求報文每一行需要換行符,但是在程式設計的時候需要清楚理解換行符和回車符,如果在寫請求報文時單單以\n作為換行組成的報文將得不到伺服器的響應會產生400 bad request錯誤。
解決方案:每一行換行需要以回車符和換行符即\r \n 兩個一起,這樣才能得到正確的報文。在讀取響應報文時也要注意會有兩個符號作為一行的換行,所以在讀取到
2、關鍵技術:對於客戶端、代理伺服器、真正伺服器之間的響應執行緒之間的正確順序的組織;
解決方案:使用執行緒組織各部分之間的排程關係,代理伺服器處於一直監聽狀態,當和客戶端互動時處於伺服器的角色,當和伺服器互動時處於客戶端的角色。
3、關鍵技術:對於請求報文資訊的解析,包括正式請求的伺服器的url、埠號、host等資訊的正確獲取
解決方案:按行提取資訊,使用字串處理函式提取有用的資訊
4、關鍵技術:使用快取的代理伺服器,需要做到儲存請求報文相應的響應報文,順序不能有差錯而且資訊不能有缺漏
解決方案:使用日誌,在每接受一次請求的時候,將請求的完整url儲存到日誌中,之後一旦得到相應資訊直接儲存在url下方,每次通過匹配url得知其下方的響應是否是所需的,這樣方便查詢和修改
五、HTTP代理伺服器的實驗驗證過程以及實驗結果
1、基本功能:代理上網
2、擴充套件功能:遮蔽網站
3、擴充套件功能:釣魚網站
選擇搜狗但是進入的是淘寶網
4、擴充套件功能:帶有快取處理
每一次都會在日誌中找是否已有相應的快取,已有資訊則向伺服器傳送時間確認報文,後決定是否使用快取中資訊
六、HTTP代理伺服器原始碼(帶有詳細註釋)
package test;
import java.io.*;
import java.net.*;
import java.util.*;
public class MyHttpProxy extends Thread {
public static int CONNECT_RETRIES = 5; // 嘗試與目標主機連線次數
public static int CONNECT_PAUSE = 5; // 每次建立連線的間隔時間
public static int TIMEOUT = 8000; // 每次嘗試連線的最大時間
public static int BUFSIZ = 1024; // 緩衝區最大位元組數
public static boolean logging = false; // 是否記錄日誌
public static OutputStream log_S = null; // 日誌輸出流
public static OutputStream log_C = null; // 日誌輸出流
public static OutputStream log_D = null; // 響應報文日誌
public static int count = -1;
public static List<String> requestInfo = new ArrayList<String>();
public static List<String> cacheInfo;
Socket ssocket = null;
// cis為客戶端輸入流,sis為目標主機輸入流
InputStream cis = null, sis = null;
BufferedReader cbr = null, sbr = null; // 轉化為字元流讀取便於比較
// cos為客戶端輸出流,sos為目標主機輸出流
OutputStream cos = null, sos = null;
PrintWriter cpw = null, spw = null;// 轉化為字元流
String buffer = ""; // 讀取請求頭
String URL = ""; // 讀取請求URL
String host = ""; // 讀取目標主機host
int port = 80; // 預設埠80
String findUrl = "";//在快取中查詢的url
// 與客戶端相連的Socket
protected Socket csocket;
public MyHttpProxy(Socket cs) {
try {
csocket = cs;
cis = csocket.getInputStream(); // 代理伺服器作為伺服器接受客戶端的請求
cbr = new BufferedReader(new InputStreamReader(cis));
cos = csocket.getOutputStream(); // 代理伺服器作為伺服器向客戶端發出響應
cpw = new PrintWriter(cos);
start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void writeLog(int c, int browser) throws IOException {
if (browser == 1)
log_C.write((char) c);
else if (browser == 2)
log_S.write((char) c);
else
log_D.write((char) c);
}
public void writeLog(byte[] bytes, int offset, int len, int browser)
throws IOException {
for (int i = 0; i < len; i++)
writeLog((int) bytes[offset + i], browser);
}
public void run() {
try {
csocket.setSoTimeout(TIMEOUT);
System.out.println("到了讀取第一行");
buffer = cbr.readLine(); // 獲取首部行
System.out.println("buffer:" + buffer);
URL = getRequestURL(buffer);
System.out.println(URL);
if(URL.equals("http://www.sogou.com/")){
URL = "http://www.taobao.com/";
buffer = "GET "+URL+" HTTP/1.1";
requestInfo.add("Accept: text/html, application/xhtml+xml, */*");
requestInfo.add("Accept-Language: zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3");
requestInfo.add("User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.2; WOW64; Trident/6.0)");
requestInfo.add("Accept-Encoding: gzip, deflate");
requestInfo.add("Proxy-Connection: Keep-Alive");
requestInfo.add("DNT: 1");
requestInfo.add("Host: www.taobao.com");
requestInfo.add("Cookie: thw=cn; isg=0BC4B5EFD7C7FCFEB73317770EA7F3F5; l=AeVoHE44ZTsle7DjpW8fBSV7pbSl-2U7; cna=GCHeDZQAVwkCAdvZ9Apwg8rH; t=1a1386bec550ab78d1aaf5ad5b90e044; mt=ci%3D-1_0; _med=dw:1366&dh:768&pw:1366&ph:768&ist:0");
}
else if(URL.equals("http://www.qq.com/")) {
URL = "";
}
int n;
// 抽取host
n = URL.indexOf("//");
if (n != -1)
host = URL.substring(n + 2); // www.baidu.com/
n = host.indexOf('/');
if (n != -1)
host = host.substring(0, n);// www.baidu.com
n = URL.indexOf('?');
if(n != -1)
findUrl = URL.substring(0,n);
else findUrl = URL;
// 分析可能存在的埠號
n = host.indexOf(':');
if (n != -1) {
port = Integer.parseInt(host.substring(n + 1));
host = host.substring(0, n);
}
int retry = CONNECT_RETRIES;
while (retry-- != 0 && !host.equals("")) {
try {
System.out.println("埠號:" + port + "主機:" + host);
System.out.println("第一行是 " + retry + ":" + buffer);
ssocket = new Socket(host, port); // 嘗試建立與目標主機的連線
break;
} catch (Exception e) {
e.printStackTrace();
}
// 等待
Thread.sleep(CONNECT_PAUSE);
}
if (ssocket != null) {
ssocket.setSoTimeout(TIMEOUT);
sis = ssocket.getInputStream(); // 代理伺服器作為客戶端接受響應
sbr = new BufferedReader(new InputStreamReader(sis));
sos = ssocket.getOutputStream(); // 代理伺服器作為客戶端發出請求
spw = new PrintWriter(sos);
String modifTime = findCache(findUrl);// 在快取中尋找是否之前已經快取過這個url的資訊
System.out.println("上一次修改的時間為:" + modifTime);//
writeLog(buffer.getBytes(), 0, buffer.length(), 1);
writeLog(buffer.getBytes(), 0, buffer.length(), 3);
writeLog("\r\n".getBytes(), 0, 2, 3);
// 之前沒有快取
if (modifTime == null) {
while (!buffer.equals("")) {
buffer += "\r\n";
if(buffer.contains("www.taobao.com")) { //遮蔽人人網,如果是淘寶就傳送淘寶的報文
int k = 0;
while(requestInfo.size() - k > 0) {
spw.write(buffer);
buffer = requestInfo.get(k++);
buffer += "\r\n";
}
break;
}
else{
spw.write(buffer);
writeLog(buffer.getBytes(), 0, buffer.length(), 1);
System.out.print("向伺服器傳送請求:"+buffer);
buffer = cbr.readLine();
}
}
spw.write("\r\n");
writeLog("\r\n".getBytes(), 0, 2, 1);
spw.flush();
// 讀取伺服器的響應資訊
int length;
byte bytes[] = new byte[BUFSIZ];
while (true) {
try {
if ((length = sis.read(bytes)) > 0) { // 讀取客戶端的請求轉給伺服器
cos.write(bytes, 0, length);
if (logging) {
writeLog(bytes, 0, length, 1);
writeLog(bytes,0,length,3);
}
} else if (length < 0)
break;
} catch (SocketTimeoutException e) {
} catch (InterruptedIOException e) {
System.out.println("\nRequest Exception:");
e.printStackTrace();
}
}
if(count == 0) {
System.out.println(cbr.readLine());
}
cpw.write("\r\n");
writeLog("\r\n".getBytes(), 0, 2, 3);
writeLog("\r\n".getBytes(), 0, 2, 2);
cpw.flush();
} else {
buffer += "\r\n";
spw.write(buffer);
System.out.print("向伺服器傳送確認修改時間請求:"+buffer);
String str1 = "Host: " + host + "\r\n";
spw.write(str1);
String str = "If-modified-since: " + modifTime
+ "\r\n";
spw.write(str);
spw.write("\r\n");
spw.flush();
System.out.print(str1);
System.out.print(str);
String info = sbr.readLine();
System.out.println("伺服器發回的資訊是:"+info);
if (info.contains("Not Modified")) {
int j = 0;
System.out.println("使用快取中的資料");
while (j < cacheInfo.size()) {
info = cacheInfo.get(j++);
info += "\r\n";
System.out.print(info);
cpw.write(info);
}
cpw.write("\r\n");
cpw.flush();
} else {
System.out.println("有更新,使用新的資料");
while (!info.equals("")) {
info += "\r\n";
System.out.print("新的資料是:" + info);
cpw.write(info);
info = sbr.readLine();
}
cpw.write("\r\n");
cpw.flush();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String getRequestURL(String buffer) {
String[] tokens = buffer.split(" ");
String URL = "";
if (tokens[0].equals("GET"))
for (int index = 0; index < tokens.length; index++) {
if (tokens[index].startsWith("http://")) {
URL = tokens[index];
break;
}
}
return URL;
}
public void pipe(InputStream cis, InputStream sis, OutputStream sos,
OutputStream cos) {
try {
int length;
byte bytes[] = new byte[BUFSIZ];
while (true) {
try {
if ((length = cis.read(bytes)) > 0) { // 讀取客戶端的請求轉給伺服器
sos.write(bytes, 0, length);
if (logging)
writeLog(bytes, 0, length, 1);
} else if (length < 0)
break;
} catch (SocketTimeoutException e) {
} catch (InterruptedIOException e) {
System.out.println("\nRequest Exception:");
e.printStackTrace();
}
try {
if ((length = sis.read(bytes)) > 0) {// 接受伺服器的響應回傳給請求的客戶端
cos.write(bytes, 0, length); // 因為是按位元組讀取,所以將回車和換行符也傳遞過去了
if (logging) {
writeLog(bytes, 0, length, 1);
writeLog(bytes, 0, length, 3);
}
}
} catch (SocketTimeoutException e) {
} catch (InterruptedIOException e) {
System.out.println("\nResponse Exception:");
e.printStackTrace();
}
}
} catch (Exception e0) {
System.out.println("Pipe異常: " + e0);
}
}
public static void startProxy(int port, Class clobj) {
try {
ServerSocket ssock = new ServerSocket(port);
while (true) {
Class[] sarg = new Class[1];
Object[] arg = new Object[1];
sarg[0] = Socket.class;
try {
java.lang.reflect.Constructor cons = clobj
.getDeclaredConstructor(sarg);
arg[0] = ssock.accept();
System.out.println("啟動執行緒:"+count++);
cons.newInstance(arg); // 建立HttpProxy或其派生類的例項
} catch (Exception e) {
Socket esock = (Socket) arg[0];
try {
esock.close();
} catch (Exception ec) {
}
}
}
} catch (IOException e) {
System.out.println("\nStartProxy Exception:");
e.printStackTrace();
}
}
// 測試用的簡單main方法
static public void main(String args[]) throws FileNotFoundException {
System.out.println("在埠8888啟動代理伺服器\n");
OutputStream file_S = new FileOutputStream(new File("log_s.txt"));
OutputStream file_C = new FileOutputStream(new File("log_c.txt"));
OutputStream file_D = new FileOutputStream("log_d.txt",true);
MyHttpProxy.log_S = file_S;
MyHttpProxy.log_C = file_C;
MyHttpProxy.log_D = file_D; // 直接儲存相關URl對應的響應報文
MyHttpProxy.logging = true;
MyHttpProxy.startProxy(8888, MyHttpProxy.class);
}
public String findCache(String head) {
cacheInfo = new ArrayList<String>();
String resul = null;
int count = 0;
try {
// 直接在存有url和相應資訊的檔案中查詢
InputStream file_D = new FileInputStream("log_d.txt");
String info = "";
while (true) {
int c = file_D.read();
if (c == -1)
break; // -1為結尾標誌
if (c == '\r') {
file_D.read();
break;// 讀入每一行資料
}
if (c == '\n')
break;
info = info + (char) c;
}
System.out.println("第一次得到:" + info);
System.out.println("要找的是:" + head);
int m = 0;
while ((m = file_D.read()) != -1 && info!=null) {
//System.out.println("在尋找:"+info);
// 找到相同的,那麼它下面的就是響應資訊,找上次修改的時間
if (info.contains(head)) {
String info1;
do {
System.out.println("找到相同的了:" + info);
info1 = "";
if(m!='\r' && m != '\n')
info1 += (char) m;
while (true) {
m = file_D.read();
if (m == -1)
break;
if (m == '\r') {
file_D.read();
break;
}
if (m == '\n') {
break;
}
info1 += (char) m;
}
System.out.println("info1是:"+info1);
if (info1.contains("Last-Modified:")) {
resul = info1.substring(16);
}
cacheInfo.add(info1);
if(info1.equals("")){
System.out.print("我是空");
return resul;
}
} while (!info1.equals("") && info1 != null && m != -1);
}
info = "";
while (true) {
if (m == -1)
break;
if (m == '\r') {
file_D.read();
break;
}
if (m == '\n')
break;
info += (char) m;
m = file_D.read();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return resul;
}
}