1. 程式人生 > >JAVA寫HTTP代理伺服器(一)-socket實現

JAVA寫HTTP代理伺服器(一)-socket實現

HTTP代理伺服器是一種特殊的網路服務,允許一個網路終端(一般為客戶端)通過這個服務與另一個網路終端(一般為伺服器)進行非直接的連線。一些閘道器、路由器等網路裝置具備網路代理功能。一般認為代理服務有利於保障網路終端的隱私或安全,防止攻擊。

HTTP 代理有分兩種:

  1. RFC 7230 - HTTP/1.1: Message Syntax and Routing(即修訂後的 RFC 2616,HTTP/1.1 協議的第一部分)描述的普通代理。這種代理扮演的是「中間人」角色,對於連線到它的客戶端來說,它是服務端;對於要連線的服務端來說,它是客戶端。它就負責在兩端之間來回傳送 HTTP 報文。
  2. Tunneling TCP based protocols through Web proxy servers(通過 Web 代理伺服器用隧道方式傳輸基於 TCP 的協議)描述的隧道代理。它通過 HTTP 協議正文部分(Body)完成通訊,以 HTTP 的方式實現任意基於 TCP 的應用層協議代理。這種代理使用 HTTP 的 CONNECT 方法建立連線,但 CONNECT 最開始並不是 RFC 2616 - HTTP/1.1 的一部分,直到 2014 年釋出的 HTTP/1.1 修訂版中,才增加了對 CONNECT 及隧道代理的描述,詳見 RFC 7231 - HTTP/1.1: Semantics and Content。實際上這種代理早就被廣泛實現。

HTTP代理

http請求經過代理伺服器,代理伺服器只要負責轉發相應的http響應體就可以了。

HTTPS代理

https請求經過代理伺服器,會發送一個CONNECT報文,用於和代理伺服器建立隧道,如果代理伺服器返回HTTP 200,則建立成功,後續代理伺服器只要負責轉發資料就行,實際上SSL/TLS握手還是發生在客戶端和真實伺服器。
QQ截圖20170904111304.jpg

思路

建立SocketServer監聽埠,根據http請求頭方法如果是CONNECT就是HTTPS請求否則都為HTTP請求,接著根據HOST頭建立代理伺服器與目標伺服器的連線,然後轉發資料。HTTPS請求需要特殊處理,因為CONNECT請求並不需要轉發,要返回一個HTTP 200的響應建立隧道,之後才進行轉發。

實現

//監聽埠
ServerSocket serverSocket = new ServerSocket(port);
  for (; ; ) { 
    new SocketHandle(serverSocket.accept()).start(); 
  }
static class SocketHandle extends Thread {

        private Socket socket;

        public SocketHandle(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            OutputStream clientOutput = null;
            InputStream clientInput = null;
            Socket proxySocket = null;
            InputStream proxyInput = null;
            OutputStream proxyOutput = null;
            try {
                clientInput = socket.getInputStream();
                clientOutput = socket.getOutputStream();
                String line;
                String host = "";
                LineBuffer lineBuffer = new LineBuffer(1024);
                StringBuilder headStr = new StringBuilder();
                //讀取HTTP請求頭,並拿到HOST請求頭和method
                while (null != (line = lineBuffer.readLine(clientInput))) {
                    System.out.println(line);
                    headStr.append(line + "\r\n");
                    if (line.length() == 0) {
                        break;
                    } else {
                        String[] temp = line.split(" ");
                        if (temp[0].contains("Host")) {
                            host = temp[1];
                        }
                    }
                }
                String type = headStr.substring(0, headStr.indexOf(" "));
                //根據host頭解析出目標伺服器的host和port
                String[] hostTemp = host.split(":");
                host = hostTemp[0];
                int port = 80;
                if (hostTemp.length > 1) {
                    port = Integer.valueOf(hostTemp[1]);
                }
                //連線到目標伺服器
                proxySocket = new Socket(host, port);
                proxyInput = proxySocket.getInputStream();
                proxyOutput = proxySocket.getOutputStream();
                //根據HTTP method來判斷是https還是http請求
                if ("CONNECT".equalsIgnoreCase(type)) {//https先建立隧道
                    clientOutput.write("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
                    clientOutput.flush();
                } else {//http直接將請求頭轉發
                    proxyOutput.write(headStr.toString().getBytes());
                }
                //新開執行緒轉發客戶端請求至目標伺服器
                new ProxyHandleThread(clientInput, proxyOutput).start();
                //轉發目標伺服器響應至客戶端
                while (true) {
                    clientOutput.write(proxyInput.read());
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (proxyInput != null) {
                    try {
                        proxyOutput.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (proxyOutput != null) {
                    try {
                        proxyOutput.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (proxySocket != null) {
                    try {
                        proxySocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (clientInput != null) {
                    try {
                        clientInput.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (clientOutput != null) {
                    try {
                        clientOutput.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }
static class ProxyHandleThread extends Thread {

        private InputStream input;
        private OutputStream output;

        public ProxyHandleThread(InputStream input, OutputStream output, CountDownLatch cdl) {
            this.input = input;
            this.output = output;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    output.write(input.read());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

後記

以上一個簡單的HTTP代理伺服器就實現了,不過其中問題也有很多,如BIO模型的缺陷,異常處理機制。
下一篇會用netty來實現一個高效能的HTTP代理伺服器。
程式碼託管在github上,歡迎start