WEB网关系列02-基于http协议实现流量的代理
Contents
基于http协议实现流量的代理
模式之间差异
- 直连模式
- 代理模式
直连模式
直连模式,也就是服务与服务之间直接请求调用,比如说我们正常在浏览器上面输入baidu,然后浏览器把百度相应的响应反馈回来的过程。 这个过程种,用户充当了service-A的角色,百度的服务器充当了service-B的角色。
- 代码还原以上过程,利用java的
Java.net.HttpURLConnection
类实现网络访问- 除了HttpURLConnection还有很多java封装的包也能实现网络访问
- 通过common封装好HttpClient;
- 通过 Apache 封装好CloseableHttpClient;
- 通过SpringBoot-RestTemplate;
- 除了HttpURLConnection还有很多java封装的包也能实现网络访问
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 不带路由转发的代理功能
- 代码如下
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&tpl=mn&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>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <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版
参考
https://blog.csdn.net/jxlhljh/article/details/119963668