關鍵詞:web cache 代理服務器 計網 計算機網絡 socket http
這個項目是計算機網絡的課程項目之一,要求使用socket編程實現http代理服務器,能夠同時服務兩個以上客戶端同時訪問,允許用戶自行設置工作區。而我在這裏使用的是比較熟悉的Java實現。
如果同學們有幸搜到了這篇文章,請有限制的借鑑,畢竟課程項目的初衷就是爲了讓同學們在時間中鞏固知識,而不是交差拿高分。
本項目的架構如下,這是項目的類圖:
服務器與客戶端的交互過程圖解如下:
代理服務器的機制是這樣子的:它既可以作爲服務器,響應來自瀏覽器客戶端的請求,發送網頁文件給瀏覽器客戶端,同時,它也可以作爲客戶端,向網絡中的web server發送請求來獲取最新的信息。當它作爲一個局域網的代理服務器時,如果它的所有緩存都爲空,則局域網中所有連接它的主機的DNS請求,HTTP請求都要通過它發往外網中的DNS服務器和web服務器,它獲取響應之後再進行緩存併發送回局域網中的客戶端。
它的具體實現思路如下:
當監聽到客戶端發送數據報的socket後,代理服務器將提取數據報的首行,獲取請求,讀取緩存判斷之前是否已經緩存該請求,若無,將此請求寫入緩存文件。
從請求中提取主機名和端口,與服務器新建一個socket進行會話。檢查本地緩存中是否有之前的響應數據,若有,尋找其中的Last-Modified:字段,並生成一個condition GET 請求發送給服務器,如果服務器返回304 Not Modified,就將相應的本地緩存發送至瀏覽器,如果沒有緩存或者有更新,則將新的請求轉發給瀏覽器,並將最新的緩存寫入本地緩存中。
本地緩存的記錄格式是:請求首行+響應內容。
注意:本程序會過濾掉無法訪問的google網站和CONNECT請求
本程序一共由兩個.java文件實現
HttpProxy.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class HttpProxy {
public static String cachePath="";
public static OutputStream writeCache;
public static int TIMEOUT=5000;//response time out upper bound
public static int RETRIEVE=5;//retry connection 5 times
public static int CONNECT_PAUSE=5000;//waiting for connection
public static void main(String[] args) throws IOException {
ServerSocket serverSocket;
Socket currsoket=null;
/** users need to setup work space */
System.out.println("==============請輸入緩存的存儲目錄,輸入 d 則設置爲默認目錄(程序同一目錄下)=================");
Scanner scanner=new Scanner(System.in);
cachePath=scanner.nextLine();
if(cachePath.equals("d")){
cachePath="defaul_cache.txt";
}
/** 初始化緩存寫對象 */
writeCache=new FileOutputStream(cachePath,true);
System.out.println("=================================== 工作目錄設置完畢====================================");
try {
//設置serversocket,綁定端口8888
serverSocket=new ServerSocket(8888);
int i=0;
//循環,持續監聽從這個端口的所有請求
while(true){
currsoket=serverSocket.accept();
//啓動一個新的線程來處理這個請求
i++;
System.out.println("啓動第"+i+"個線程");
new MyProxy(currsoket);
}
} catch (IOException e) {
if (currsoket != null) {
currsoket.close();//及時關閉這個socket
}
e.printStackTrace();
}
writeCache.close();//關閉文件輸出流
}
}
MyProxy.java
import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.Vector;
public class MyProxy extends Thread {
Socket socket ;//這個socket是這個線程與瀏覽器的socket
String targetHost=null;
String targetPort;
InputStream inputStream_client;//這個輸入流用來讀取瀏覽器發過來的請求
OutputStream outputStream_client;//這個輸出流用來將數據發送到瀏覽器
PrintWriter outPrintWriter_client;//這個writer用來向瀏覽器寫入數據
BufferedReader bufferedReader_client;//這個緩衝用來緩存瀏覽器的請求
Socket accessSocket;//這個socket用來向網站連接
InputStream inputStream_Web;//這個輸入流用來讀取從網站發回的響應
OutputStream outputStream_Web;//這個輸出流用來向網站發送請求
PrintWriter outPrintWriter_Web;//這個writer用來向網站發送請求
BufferedReader bufferedReader_web;//這個緩衝用來緩存想網站發送的請求
String cacheFilePath;
File file=null;
FileInputStream fileInputStream;
String url="";
ArrayList<String>cache;
int cache_url_index=-1;
boolean has_cache_no_timestamp=false;
public MyProxy(Socket inputSocket) throws IOException {
socket=inputSocket;
/** 創建一個文件對象 */
file=new File(HttpProxy.cachePath);
if (!file.exists()){//文件不存在則新建一個文件
file.createNewFile();
}
fileInputStream=new FileInputStream(HttpProxy.cachePath);
System.out.print("代理服務器啓動\n");
System.out.print("獲取的socket來自"+inputSocket.getInetAddress()+":"+inputSocket.getPort()+"\n");
inputStream_client=socket.getInputStream();//創建從瀏覽器獲取請求的輸入流
bufferedReader_client=new BufferedReader(new InputStreamReader(inputStream_client));
outputStream_client=socket.getOutputStream();//創建向瀏覽器發送響應的流
outPrintWriter_client=new PrintWriter(outputStream_client);
/** 讀取緩存 */
cache=readCache(fileInputStream);
System.out.println("讀到的緩存有"+cache.size()+"行");
start();//啓動本線程
}
public void run() {
try {
socket.setSoTimeout(HttpProxy.TIMEOUT);//設置最大等待時間,超過則自動斷開連接
String buffer;
//debug
System.out.println("從瀏覽器讀取第一行....");
buffer = bufferedReader_client.readLine();//從瀏覽器讀取第一行請求
System.out.println(buffer);
/** 提取 url */
url=getURL(buffer);
/** 過濾一些雜亂的請求,比如Google的和一些後臺的CONNECT請求還有QQ管家的監聽 */
if(buffer.contains("CONNECT")||buffer.contains("google")||buffer.contains("c.gj.qq.com")){
System.out.println("請求"+buffer+"已被過濾");
return ;//退出run()方法,該線程就自動結束
}
/** 將請求寫入緩存文件,如果緩存中已經有相同的請求,就不再寫入了 */
boolean has_in_cache_already=false;
for(String iter:cache){
if (iter.equals(buffer)) {
has_in_cache_already = true;
break;
}
}
if (has_in_cache_already==false){
String temp = buffer + "\r\n";
write_cache(temp.getBytes(), 0, temp.length());
}
/** 提取主機和端口 */
String[] HostandPort=new String[2];
if (buffer!=null)
HostandPort= findHostandPort(buffer);
targetHost = HostandPort[0];
targetPort = HostandPort[1];
System.out.println("提取的主機名:" + targetHost + " 提取的端口號: " + targetPort);
/** 嘗試與目標主機連接 */
int retry = HttpProxy.RETRIEVE;
while (retry-- != 0 && (targetHost != null)) {
try {
accessSocket = new Socket(targetHost, Integer.parseInt(targetPort));
break;
} catch (Exception e) {
e.printStackTrace();
}
Thread.sleep(HttpProxy.CONNECT_PAUSE);//等待
}
if (accessSocket != null) {//成功建立連接
//debug
System.out.println("請求將發送至:" + targetHost);
accessSocket.setSoTimeout(HttpProxy.TIMEOUT);
inputStream_Web = accessSocket.getInputStream();//獲取網站返回的響應
bufferedReader_web = new BufferedReader(new InputStreamReader(inputStream_Web));
outPrintWriter_Web = new PrintWriter(accessSocket.getOutputStream());//準備好向網站發送請求
/** 如果緩存文件爲空 */
if (cache.size()==0) {
/** 將請求直接發往網站,並獲取響應,記錄響應至緩存 */
sendRequestToInternet(buffer);
transmitResponseToClient();
} else {//緩存文件不爲空,尋找之前有沒有緩存過該請求
String modifyTime;
String info="";
modifyTime=findModifyTime(cache,buffer);//提取modifytime
System.out.println("提取到的modifytime:"+modifyTime);
if (modifyTime!=null||has_cache_no_timestamp){
/** 如果緩存的內容裏面該請求是沒有Last-Modify屬性的,就不用向服務器查詢If-Modify了,否則向服務器查詢If-Modify */
if (!has_cache_no_timestamp){
buffer += "\r\n";
outPrintWriter_Web.write(buffer);
System.out.print("向服務器發送確認修改時間請求:\n" + buffer);
String str1 = "Host: " + targetHost + "\r\n";
outPrintWriter_Web.write(str1);
String str = "If-modified-since: " + modifyTime
+ "\r\n";
outPrintWriter_Web.write(str);
outPrintWriter_Web.write("\r\n");
outPrintWriter_Web.flush();
System.out.print(str1);
System.out.print(str);
info= bufferedReader_web.readLine();
System.out.println("服務器發回的信息是:" + info);
}
if (info.contains("Not Modified")||has_cache_no_timestamp) {//如果服務器給回的響應是304 Not Modified,就將緩存的數據直接發送給瀏覽器
int contentindex = 0;
String temp_response="";
System.out.println("使用緩存數據");
if (cache_url_index!=-1)
for (int i=cache_url_index+1;i<cache.size();i++){
if (cache.get(i).contains("http://"))
break;
temp_response+=cache.get(i);
temp_response+="\r\n";
}
System.out.println("使用緩存:\n"+temp_response);
outputStream_client.write(temp_response.getBytes(),0,temp_response.getBytes().length);
outputStream_client.write("\r\n".getBytes(),0,"\r\n".getBytes().length);
outputStream_client.flush();
} else {
/** 服務器返回的不是304 Not Modified的話,就將服務器的響應直接轉發到瀏覽器並記錄緩存就好了 */
System.out.println("有更新,使用新的數據");
transmitResponseToClient();
}
}else{
/**緩存中沒有找到之前的記錄,直接將請求發送給網站,並接收響應,將響應寫入緩存 */
sendRequestToInternet(buffer);
transmitResponseToClient();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
*將請求發送給網站
* @param buffer 請求的第一行報文
* @throws IOException
*/
private void sendRequestToInternet(String buffer) throws IOException {
while(!buffer.equals("")){
buffer+="\r\n";
outPrintWriter_Web.write(buffer);
System.out.print("發送請求:"+buffer+"\n");
buffer=bufferedReader_client.readLine();
}
outPrintWriter_Web.write("\r\n");
outPrintWriter_Web.flush();
}
/**
* 提取主機名和端口
* @param content 待提取的報文,這是請求的第一行
* @return
*/
private String[] findHostandPort(String content){
String host=null;
String port=null;
String[] result=new String[2];
int index;
int portIndex;
String temp;
StringTokenizer stringTokenizer=new StringTokenizer(content);
stringTokenizer.nextToken();//丟棄第一個字串 這是請求類型 比如GET POST
temp=stringTokenizer.nextToken();//這個字串裏面有主機名和端口
host=temp.substring(temp.indexOf("//")+2);//比如 http://news.sina.com.cn/gov/2017-12-13/doc-ifypsqiz3904275.shtml -> news.sina.com.cn/gov/2017-12-13/doc-ifypsqiz3904275.shtml
index=host.indexOf("/");
if (index!=-1){
host=host.substring(0,index);//比如 news.sina.com.cn/gov/2017-12-13/doc-ifypsqiz3904275.shtml -> news.sina.com.cn
portIndex=host.indexOf(":");
if (portIndex!=-1){
port=host.substring(portIndex+1);//比如 www.ghostlwb.com:8080 -> 8080
host=host.substring(0,portIndex);
}else{//沒有找到端口號,則加上默認端口號80
port="80";
}
}
result[0]=host;
result[1]=port;
return result;
}
/**
* 提取URL
* @param firstline 請求報文的第一行
* @return
*/
private String getURL(String firstline){
StringTokenizer stringTokenizer=new StringTokenizer(firstline);
stringTokenizer.nextToken();
return stringTokenizer.nextToken();
}
/**
* 這個函數做三件事:從網站接收響應,發送給瀏覽器,並將響應寫入緩存
* @throws IOException
*/
private void transmitResponseToClient() throws IOException {
byte[] bytes=new byte[2048];
int length=0;
while(true){
if((length=inputStream_Web.read(bytes))>0){
outputStream_client.write(bytes,0,length);
String show_response=new String(bytes,0,bytes.length);
System.out.println("服務器發回的消息是:\n---\n"+show_response+"\n---");
write_cache(bytes,0,length);
write_cache("\r\n".getBytes(),0,2);
continue;
}
break;
}
outPrintWriter_client.write("\r\n");
outPrintWriter_client.flush();
}
/**
* 從文件中讀取緩存內容,按行讀取
* @param fileInputStream
* @return
*/
private ArrayList<String> readCache(FileInputStream fileInputStream){
ArrayList<String> result=new ArrayList<>();
String temp;
BufferedReader br=new BufferedReader(new InputStreamReader(fileInputStream));
try {
while((temp=br.readLine())!=null){
result.add(temp);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 將內容寫入緩存,這兩段代碼參考網上的
* @param c
* @throws IOException
*/
private void write_cache(int c) throws IOException {
HttpProxy.writeCache.write((char) c);
}
private void write_cache(byte[] bytes, int offset, int len)
throws IOException {
for (int i = 0; i < len; i++)
write_cache((int) bytes[offset + i]);
}
/**
* 提取modifytime
* @param cache_temp
* @param request
* @return
*/
private String findModifyTime(ArrayList<String> cache_temp,String request){
String LastModifiTime=null;
int startSearching=0;
has_cache_no_timestamp=false;
System.out.println("將要比對的URL是"+request);
for(int i=0;i<cache_temp.size();i++){
if (cache_temp.get(i).equals(request)){
startSearching=i;
cache_url_index=i;
for(int j=startSearching+1;j<cache_temp.size();j++){
if(cache_temp.get(j).contains("http://"))
break;
if (cache_temp.get(j).contains("Last-Modified:")){
LastModifiTime=cacheFilePath.substring(cache_temp.get(j).indexOf("Last-Modified:"));
return LastModifiTime;
}
if (cache_temp.get(j).contains("<html>")){
has_cache_no_timestamp=true;
return LastModifiTime;
}
}
}
}
return LastModifiTime;
}
}
程序中都有十分詳細的中文註釋,如果有不明白的地方,歡迎在下面評論交流