http代理服務器實現(web cache)

關鍵詞: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;
    }

}

程序中都有十分詳細的中文註釋,如果有不明白的地方,歡迎在下面評論交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章