Contents

WEB网关系列02-基于http协议实现流量的代理

基于http协议实现流量的代理

模式之间差异

  • 直连模式

/img/source/gateway/proxy/direct_mode.png

  • 代理模式

/img/source/gateway/proxy/proxy_mode.png

直连模式

直连模式,也就是服务与服务之间直接请求调用,比如说我们正常在浏览器上面输入baidu,然后浏览器把百度相应的响应反馈回来的过程。 这个过程种,用户充当了service-A的角色,百度的服务器充当了service-B的角色。

  • 代码还原以上过程,利用java的Java.net.HttpURLConnection类实现网络访问
    • 除了HttpURLConnection还有很多java封装的包也能实现网络访问
      • 通过common封装好HttpClient;
      • 通过 Apache 封装好CloseableHttpClient;
      • 通过SpringBoot-RestTemplate;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class DirectMode {

    public static void main(String[] args) {

        String api = "https://www.baidu.com";
        HttpURLConnection connection = null;
        InputStream in = null;
        BufferedReader reader = null;
        try {
            //构造一个URL对象
            URL url = new URL(api);
            //获取URLConnection对象
            connection = (HttpURLConnection) url.openConnection();
            //getOutputStream会隐含的进行connect(即:如同调用上面的connect()方法,所以在开发中不调用connect()也可以)
            in = connection.getInputStream();
            //通过InputStreamReader将字节流转换成字符串,在通过BufferedReader将字符流转换成自带缓冲流
            reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            String line = null;
            //按行读取
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            String response = sb.toString();
            System.out.println(response);
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

上面的代码会输出百度的响应信息,跟我们正常在浏览器上面访问百度是同样的效果,只是,浏览器上面有更美观的样式展现出来了。

代理模式

代理模式,也就是服务与服务中间经过了代理服务的转发,分别是请求和响应的转发。代理服务除了处理转发外还需要相应的路由寻址, 也就是把请求对应转发到对应的目标服务上,比如http://xxx/request1对应的地址为service-B上的地址。http://xxx/request2对应的地址为service-C上的地址。 那么就需要代理服务能正确的进行路由匹配和转发了。

代理模式01 不带路由转发的代理功能

/img/source/gateway/proxy/socket_proxy_mode.png

  • 代码如下

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Base64;
import java.util.Date;
public class ProxyHttpsServer {

    private final int bufferSize = 8092;
    private int defaultPort = 1080; //默认端口
    private int localPort ;
    private ServerSocket localServerSocket;

    private boolean socksNeekLogin = false;//是否需要登录
    private String username = "admin";
    private String password = "admin123";

    public static void main(String[] args) {
        Integer port = args.length == 1 ? Integer.parseInt(args[0]) : null;
        new ProxyHttpsServer(port).startService();
    }

    public ProxyHttpsServer(Integer port){
        this.localPort = port == null ? defaultPort : port;
    }

    public void startService() {

        try {

            //开启一个ServerSocket服务器,监听请求的到来.
            localServerSocket = new ServerSocket(localPort);

            log("httpproxy server started , listen on " +localServerSocket.getInetAddress().getHostAddress()+":"+ localPort );

            // 一直监听,接收到新连接,则开启新线程去处理
            while (true) {
                Socket localSocket = localServerSocket.accept();
                new SocketThread(localSocket).start();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //格式化打印方法,用来打印信息
    private final void log(Object message, Object... args) {
        Date dat = new Date();
        String msg = String.format("%1$tF %1$tT %2$-5s %3$s%n", dat, Thread.currentThread().getId(), String.format(message.toString(),args));
        System.out.print(msg);
    }

    /**
     * IO操作中共同的关闭方法
     * @param socket
     */
    protected final void closeIo(Socket closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * IO操作中共同的关闭方法
     * @param socket
     */
    protected final void closeIo(Closeable closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (IOException e) {
            }
        }
    }

    private class SocketThread extends Thread {
        private Socket localSocket;
        private Socket remoteSocket;
        private InputStream lin;
        private InputStream rin;
        private OutputStream lout;
        private OutputStream rout;

        public SocketThread(Socket socket) {
            this.localSocket = socket;
        }

        public void run() {

            //获取远程socket的地址,然后进行打印
            String addr = localSocket.getRemoteSocketAddress().toString();
            log("process one socket : %s", addr);

            try {
                lin = localSocket.getInputStream();
                lout = localSocket.getOutputStream();

                StringBuilder headStr = new StringBuilder();
                BufferedReader br = new BufferedReader(new InputStreamReader(lin));
                //读取HTTP请求头,并拿到HOST请求头和method

                String line;
                String host = "";
                String proxy_Authorization = "";
                while ((line = br.readLine()) != null) {
                    //打印http协议头
                    log(line);
                    headStr.append(line + "\r\n");
                    if (line.length() == 0) {
                        break;
                    } else {
                        String[] temp = line.split(" ");
                        if (temp[0].contains("Host")) {
                            host = temp[1];
                        }

                        //如果配置了需要登陆,就解析消息头里面的Proxy-Authorization字段,认证的账号和密码信息是通过base64加密传过来的。
                        if(socksNeekLogin && (temp[0].contains("Proxy-Authorization"))){//获取认证信息
                            proxy_Authorization = temp[2];
                        }

                    }
                }

                String type = headStr.substring(0, headStr.indexOf(" "));
                //根据host头解析出目标服务器的host和port
                String[] hostTemp = host.split(":");
                host = hostTemp[0];
                int port = 80; //先设置成默认的Http端口80
                //hostTemp的长度大于1表示用户指定了非80或443端口,解析出对应的端口出来
                if (hostTemp.length > 1) {
                    port = Integer.valueOf(hostTemp[1]);
                }else{
                    //端口如果没有指定,有可能是443,也有可能是80,因此尝试根据HTTP method来判断是https还是http请求,有CONNECT的是HTTPS请求,采用443端口
                    if ("CONNECT".equalsIgnoreCase(type)){
                        port = 443;
                    }
                }

                //

                boolean isLogin = false;
                //如果需要登录,校验登录是否通过
                if(socksNeekLogin){

                    //通过username和password进行base64加密得到一个串,然后和请求里面传过来的Proxy-Authorization比较,一致的话就认证成功。
                    String authenticationEncoding = Base64.getEncoder().encodeToString(new String(username + ":" + password).getBytes());
                    if(proxy_Authorization.equals(authenticationEncoding)){
                        isLogin = true;//登录通过
                        //log("login success, basic: %s", proxy_Authorization);
                    }else {
                        log("httpproxy server need login,but login failed .");
                    }
                }

                //不需要登录或已被校验登录成功,才进入代理,否则直接程序结束,关闭连接
                if(!socksNeekLogin || isLogin){

                    //连接到目标服务器
                    remoteSocket = new Socket(host, port);//进行远程连接
                    rin = remoteSocket.getInputStream();
                    rout = remoteSocket.getOutputStream();

                    //根据HTTP method来判断是https还是http请求
                    if ("CONNECT".equalsIgnoreCase(type)) {//https先建立隧道
                        lout.write("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
                        lout.flush();
                    }else {//http直接将请求头转发
                        rout.write(headStr.toString().getBytes());
                        rout.flush();
                    }

                    new ReadThread().start();

                    //设置超时,超过时间未收到客户端请求,关闭资源
                    //remoteSocket.setSoTimeout(10000);
                    //写数据,负责读取客户端发送过来的数据,转发给远程
                    byte[] data = new byte[bufferSize];
                    int len = 0;
                    while((len = lin.read(data)) > 0){
                        if(len == bufferSize) {//读到了缓存大小一致的数据,不需要拷贝,直接使用
                            rout.write(data);
                            rout.flush();
                        }else {//读到了比缓存大小的数据,需要拷贝到新数组然后再使用
                            byte[] dest = new byte[len];
                            System.arraycopy(data, 0, dest, 0, len);
                            rout.write(dest);
                            rout.flush();
                        }

                    }

                }

            } catch (Exception e) {
                log("exception : %s %s", e.getClass(), e.getLocalizedMessage());
                //e.printStackTrace();
            } finally {
                log("close socket, system cleanning ...  %s ", addr);
                closeIo(lin);
                closeIo(rin);
                closeIo(lout);
                closeIo(rout);
                closeIo(localSocket);
                closeIo(remoteSocket);
            }
        }

        //读数据线程负责读取远程数据后回写到客户端
        class ReadThread extends Thread {
            @Override
            public void run() {
                try {
                    byte[] data = new byte[bufferSize];
                    int len = 0;
                    while((len = rin.read(data)) > 0){
                        if(len == bufferSize) {//读到了缓存大小一致的数据,不需要拷贝,直接使用
                            lout.write(data);
                            lout.flush();
                        }else {//读到了比缓存大小的数据,需要拷贝到新数组然后再使用
                            byte[] dest = new byte[len];
                            System.arraycopy(data, 0, dest, 0, len);
                            lout.write(dest);
                            lout.flush();
                        }
                    }
                } catch (IOException e) {
                    //log(remoteSocket.getLocalAddress() + ":"+ remoteSocket.getPort() + " remoteSocket InputStream disconnected.");
                } finally {
                }
            }

        }

    }
}
  • 通过命令来代理访问百度地址

-- 代理访问百度
curl -i -x 127.0.0.1:1080  http://www.baidu.com

-- 对应响应如下

HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 2381
Content-Type: text/html
Date: Wed, 08 Nov 2023 03:41:02 GMT
Etag: "588604dc-94d"
Last-Modified: Mon, 23 Jan 2017 13:27:56 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/

<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <ocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.come=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script>  href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www/gs.gif> </p> </div> </div> </div> </body> </html>
  • 通过代码来实现代理访问百度

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class ProxyHttpsServerTest {
    public static void main(String[] args) {

        //设置代理服务器的ip和端口
        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 1080));
        String api = "https://www.baidu.com";
        HttpURLConnection connection = null;
        InputStream in = null;
        BufferedReader reader = null;
        try {
            //构造一个URL对象
            URL url = new URL(api);
            //获取URLConnection对象
            connection = (HttpURLConnection) url.openConnection(proxy);
            //getOutputStream会隐含的进行connect(即:如同调用上面的connect()方法,所以在开发中不调用connect()也可以)
            in = connection.getInputStream();
            //通过InputStreamReader将字节流转换成字符串,在通过BufferedReader将字符流转换成自带缓冲流
            reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            String line = null;
            //按行读取
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            String response = sb.toString();
            System.out.println(response);
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

代理模式02 带路由转发的代理功能hello-world版

/img/source/gateway/proxy/socket_match_proxy_mode.png

参考

https://blog.csdn.net/jxlhljh/article/details/119963668

https://github.com/monkeyWie/proxyee

https://www.runoob.com/java/net-serversocket-socket.html