国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

18143453325 在線咨詢 在線咨詢
18143453325 在線咨詢
所在位置: 首頁 > 營銷資訊 > 建站知識 > Netty搭建簡單的HTTP代理服務器

Netty搭建簡單的HTTP代理服務器

時間:2023-02-10 20:24:01 | 來源:建站知識

時間:2023-02-10 20:24:01 來源:建站知識

開篇

思路

假設我們訪問http://www.baidu.com,瀏覽器會自動幫我們創(chuàng)建出一個http請求報文,大致如下 GET / HTTP/1.1 # 請求行: 請求方法 請求路徑 請求協(xié)議版本, 下一行開始是請求頭 Host: www.baidu.com User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 Connection: keep-alive content-length: 0 通過請求頭的host得知訪問的域名是www.baidu.com,通過DNS解析出ip,向該IP的80端口發(fā)送這段報文. 此時,假設該百度ip對應的服務器上部署的是Tomcat容器,那么,Tomcat會自動解析該字符串,將其組裝為HttpServletRequest.然后響應Response 瀏覽器收到響應后,解析響應內容,然后進行后續(xù)操作(將html解析為頁面,繼續(xù)請求其中的css/js等,然后運行js等操作) 1. 代理服務器:創(chuàng)建Socket服務端,監(jiān)聽消息. 2. 本地配置Internet選項的Http代理,將自己電腦(瀏覽器)的所有請求轉發(fā)到代理服務器. 3. 代理服務器獲取到本機發(fā)送的請求報文后,轉發(fā)給目標主機(也就請求行中的主機信息,例如百度) 4. 目標主機響應信息給代理服務器后,地理服務器將響應發(fā)送回自己的電腦(瀏覽器)即可. 1.瀏覽器發(fā)送Http Connect連接請求CONNECT baidu.com:443 HTTP/1.12.代理服務器收到請求后,同樣解析出 目標主機 和 端口(443),然后與目標主機建立TCP連接,并先響應給瀏覽器如下報文HTTP/1.1 200 Connection Established3.建立完連接后,瀏覽器繼續(xù)發(fā)送后續(xù)的請求內容,我們需要將其轉發(fā)給目標主機,然后目標主機也會發(fā)送回響應,我們同樣將其發(fā)送回瀏覽器.4.如上發(fā)送/響應可能會進行多次,并且內容都是經(jīng)過加密的.對于普通Http請求,我們要做的只是轉發(fā)請求到目標主機,并且中間可以任意獲取/篡改請求或響應內容. 而對于Https請求,我們需要事先建立和目標主機的連接,然后告訴瀏覽器連接建立成功,然后讓雙方任意發(fā)送消息. 如此,也可以得出,Http想比于Https,安全性實在太弱.

大致流程

GET http://csdnimg.cn/public/favicon.ico HTTP/1.1
Host: http://csdnimg.cn
Proxy-Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://blog.csdn.net/zuoxiaolong8810/article/details/65441709
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
content-length: 0

第一行為請求行,包括了http的請求方式,請求的主機和請求的http協(xié)議版本.
后面都是請求頭,包括了Cookies等信息(如果有的話).最后的content-length: 0,是因為GET請求沒有請求體.
CONNECT webim.tim.qq.com:443 HTTP/1.1
Host: http://webim.tim.qq.com:443
Proxy-Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36

對于HTTPS來說,第一次的請求都是CONNECT(連接請求).請求頭的信息通常也就這么3個主要的.
可以發(fā)現(xiàn),在使用ip代理后,請求報文有如下變化: 1. 瀏覽器會自動在請求行上添加要請求的完整路徑.(例如, GET /public/favicon.ico HTTP/1.1 變?yōu)榱?GET http://csdnimg.cn/public/favicon.ico HTTP/1.1) 這個設計是因為在早期Http設計中,沒有http代理時,目標服務器收到請求后,假設請求行中的uri為/a/b. 那么目標服務器可以很清楚的知道它要訪問的是自己的/a/b路徑. 而使用代理后,并不知道 目標服務器的完整地址,所以需要攜帶目標服務器的完整路徑. 后來,為了解決虛擬主機的問題,幾乎所有的瀏覽器都會在請求頭中攜帶host屬性,也就解決了這個問題. 2. 請求頭中的Connection屬性變?yōu)镻roxy-Connection (Http1.1中,默認keep-alive,除非顯式指定Connection: close) 因為老舊代理(Http1.0)不認識Connection屬性,會將其作為無關屬性直接轉發(fā)給目標服務器. 但目標服務器會根據(jù)其要求(Connection: keep-alive),保持長連接,而代理則不會保持這個連接. 客戶端收到代理轉發(fā)回去的響應后(瀏覽器也會根據(jù)其要求),保持長連接,但此時代理已經(jīng)關閉了這個連接. 為了解決這個問題,就出現(xiàn)了Proxy-Connection, 如果代理是Http1.1,那么,可將其自動重寫為Connection.再發(fā)送給目標服務器. 如果代理是1.0,那么,服務器會收到Proxy-Connection,就發(fā)現(xiàn)它是代理,因為它沒有自動將其轉為Connection.就會在響應中添加Connection:close即可. 那么,對于匿名代理服務器的實現(xiàn),應該就是類似的原理.我們將瀏覽器給代理服務器專門添加的信息去除,然后篡改請求頭中的ip等信息. 此外,除了設置internet選項,還可以通過設置dns來使用代理服務器,例如自己搭建個dns后,將所有域名都映射到代理服務器上即可.


//拼接出自定義響應報文
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK/r/n")
.append("Content-Type: text/html; charset=UTF-8/r/n/r/n")
.append("<html>" +
"<head></head>" +
"<body>" +
"<h1>測試響應</h1>" +
"</body>" +
"</html>");
//將自定義響應報文發(fā)送回瀏覽器
sendResponse(sb.toString().getBytes("UTF-8"));
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

<html><head></head><body><h1>測試響應</h1></body></html>


第一行為響應行,然后是響應頭,然后是響應主體



開始編碼

/** * 啟動Netty server,監(jiān)聽指定端口的TCP連接. * 此處監(jiān)聽客戶端向我們發(fā)送的http報文 */ @SneakyThrows public void start() { //1 用于接收Client的連接 的線程組 EventLoopGroup bossGroup = new NioEventLoopGroup(proxyConfig.getSocket().getClientThreadNum()); //2 用于實際業(yè)務操作的線程組 EventLoopGroup workerGroup = new NioEventLoopGroup(proxyConfig.getSocket().getEventThreadNum()); //3 創(chuàng)建一個輔助類Bootstrap(引導程序),對server進行配置 ServerBootstrap serverBootStrap = new ServerBootstrap(); //4 將兩個線程組加入 bootstrap serverBootStrap.group(bossGroup, workerGroup) //指定使用這種類型的通道 .channel(NioServerSocketChannel.class) //使用 childHandler 綁定具體的事件處理器 .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { //設置字符串形式的解碼 這樣serverHandler中獲取到的msg可以直接(String)msg轉為string socketChannel.pipeline() //組合了http請求解碼器和http響應編碼器的一個類,可自定義各種最大長度 .addLast(NAME_HTTP_CODE_HANDLER, new HttpServerCodec()) //消息聚合器,注意,需要添加在http編解碼器(HttpServerCodec)之后 .addLast(NAME_HTTP_AGGREGATOR_HANDLER, new HttpObjectAggregator(65536)) //自定義 輸入事件 處理器 .addLast(proxyServerOutboundHandler) //自定義 客戶端輸入事件 處理器 .addLast(NAME_PROXY_SERVER_HANDLER, proxyServerHandler); } }) //服務端接受連接的隊列長度 .option(ChannelOption.SO_BACKLOG, 2048) //接收緩沖區(qū)大小 .option(ChannelOption.SO_RCVBUF, 128 * 1024); log.info("代理服務器啟動,在{}端口",proxyConfig.getSocket().getProxyPort()); //5 綁定端口,進行監(jiān)聽 異步的 可以開啟多個端口監(jiān)聽 ChannelFuture future = serverBootStrap.bind(proxyConfig.getSocket().getProxyPort()).sync(); //6 關閉前阻塞 future.channel().closeFuture().sync(); //7 關閉線程組 bossGroup.shutdownGracefully().sync(); workerGroup.shutdownGracefully().sync(); }


代理服務器 輸入事件處理類

連接成功后,在該監(jiān)聽器中自動發(fā)送客戶端發(fā)送的報文 給 目標服務器,目標服務器響應后,
處理器讀取事件被觸發(fā), 讀取該響應,發(fā)送回客戶端即可.
此時,客戶端會繼續(xù)發(fā)送后續(xù)的加密信息,這時,我們另起一個連接,連接到目標服務器,并給這個連接設置監(jiān)聽器/處理器.
后續(xù)我們直接將客戶端報文轉發(fā)給目標服務器,將目標服務器的響應轉發(fā)回客戶端即可.
這些后續(xù)操作,在channelRead()方法中,是判斷收到的消息是不是FullHttpRequest的子類,也就是不是符合http報文格式來判斷的.
如果不符合,即表示其為https請求的后續(xù)操作.






/** * 通道讀取到消息 事件 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //通道id String channelId = ProxyUtil.getChannelId(ctx); //HTTP/HTTPS : 如果是 http報文格式的,此時已經(jīng)被編碼解碼器轉為了該類, if (msg instanceof FullHttpRequest) { final FullHttpRequest request = (FullHttpRequest) msg; //獲取ip和端口 InetSocketAddress address = ProxyUtil.getAddressByRequest(request); //HTTPS : if (HttpMethod.CONNECT.equals(request.method())) { log.info(LOG_PRE + ",https請求.目標:{}", channelId, request.uri()); //給客戶端響應成功信息 HTTP/1.1 200 Connection Established .如果失敗時關閉客戶端通道 - 該方法是自己封裝的 //此處沒有添加Connection Established,似乎也沒問題 if (!ProxyUtil.writeAndFlush(ctx, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), true)) return; //此處將 該通道 的用于報文編碼解碼的處理器去除,因為后續(xù)發(fā)送的https報文都是加密過的,不符合一般報文格式,我們直接轉發(fā)即可 ctx.pipeline().remove(ProxyServer.NAME_HTTP_CODE_HANDLER); ctx.pipeline().remove(ProxyServer.NAME_HTTP_AGGREGATOR_HANDLER); //用通道id作為key,將 目標服務器地址存入, 此時 第二個參數(shù)(ChannelFuture)為null,因為 我們還未和目標服務器建立連接 ChannelCacheUtil.put(channelId, new ChannelCache(address, null)); //直接退出等待下一次雙方連接即可. return; } //HTTP: 運行到這里表示是http請求 log.info(LOG_PRE + ",http請求.目標:{}", channelId, request.uri()); //連接到目標服務器.并將當前的通道上下文(ctx)/請求報文(msg) 傳入 connect(true, address, ctx, msg, proxyConfig); return; } //其他格式數(shù)據(jù)(建立https connect后的客戶端再次發(fā)送的加密數(shù)據(jù)): //從緩存獲取之前處理https請求時緩存的 目標服務器地址 和 與目標服務器的連接通道 ChannelCache cache = ChannelCacheUtil.get(ProxyUtil.getChannelId(ctx)); //如果緩存為空,應該是緩存已經(jīng)過期,直接返回客戶端請求超時,并關閉連接 if (Objects.isNull(cache)) { log.info(LOG_PRE + ",緩存過期", channelId); ProxyUtil.writeAndFlush(ctx, new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_TIMEOUT), false); ctx.close(); return; } //HTTPS: 如果此時 與目標服務器建立的連接通道 為空,則表示這個Https協(xié)議,是客戶端第二次傳輸數(shù)據(jù)過來,因為第一次我們只是返回客戶端 200信息,并沒有真的去連接目標服務器 if (Objects.isNull(cache.getChannelFuture())) { log.info(LOG_PRE + ",https,正在與目標建立連接"); //連接到目標服務器,獲取到 連接通道,并將該通道更新到緩存中 ChannelCacheUtil.put(channelId, cache.setChannelFuture( connect(false, cache.getAddress(), ctx, msg, proxyConfig))); } else { //此處,表示https協(xié)議的請求第x次訪問(x > 2; 第一次我們響應200,第二次同目標主機建立連接, 此處直接發(fā)送消息即可) //如果此時通道是可寫的,寫入消息 if (cache.getChannelFuture().channel().isWritable()) { log.info(LOG_PRE + ",https,正在向目標發(fā)送后續(xù)消息"); cache.getChannelFuture().channel().writeAndFlush(msg); } else { log.info(LOG_PRE + ",https,與目標通道不可寫,關閉與客戶端連接"); //返回 表示失敗的 408狀態(tài)碼響應 ProxyUtil.responseFailedToClient(ctx); } } } /** * 和 目標主機 建立連接 */ private ChannelFuture connect(boolean isHttp, InetSocketAddress address, ChannelHandlerContext ctx, Object msg, ProxyConfig proxyConfig) { ChannelFuture channelFuture; //用工廠類構建出一個bootstrap,用來建立socket連接 Bootstrap bootstrap = bootstrapFactory.build(); //如果是http請求 if (isHttp) { //與目標主機建立連接 channelFuture = bootstrap //設置上http連接的通道初始化器 .handler(new HttpConnectChannelInitializer(ctx, proxyConfig)) //連接 .connect(address); //添加監(jiān)聽器,當連接建立成功后.進行相應操作 return channelFuture.addListener(new HttpChannelFutureListener(msg, ctx)); } //如果是Https請求 channelFuture = bootstrap .handler(new HttpsConnectChannelInitializer(ctx)) .connect(address); return channelFuture.addListener(new HttpsChannelFutureListener(msg, ctx)); }


@Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() //作為客戶端時的請求編碼解碼 .addLast(new HttpClientCodec()) //數(shù)據(jù)聚合類,將http報文轉為 FullHttpRequest和FullHttpResponse .addLast(new HttpObjectAggregator(proxyConfig.getSocket().getMaxContentLength())) //自定義處理器 .addLast(new HttpConnectHandler(ctx)); }@Override public void operationComplete(ChannelFuture future) throws Exception { String channelId = ProxyUtil.getChannelId(ctx); //連接成功操作 if(future.isSuccess()){ log.info(LOG_PRE + ",與目標主機建立連接成功."); //將客戶端請求報文發(fā)送給服務端 if(future.channel().isWritable()){ future.channel().writeAndFlush(msg); }else{ future.channel().close(); } return; } log.info(LOG_PRE + ",與目標主機建立連接失敗.",channelId); //給客戶端響應連接超時信息, 關閉 與客戶端的連接 ProxyUtil.responseFailedToClient(ctx); //清除緩存 ChannelCacheUtil.remove(channelId); //日志記錄 Throwable cause = future.cause(); if(cause instanceof ConnectTimeoutException) log.error(LOG_PRE + ",連接超時:{}",channelId , cause.getMessage()); else if (cause instanceof UnknownHostException) log.error(LOG_PRE + ",未知主機:{}", channelId, cause.getMessage()); else log.error(LOG_PRE + ",異常:{}", channelId,cause.getMessage(),cause); log.info(LOG_PRE + ",給客戶端響應失敗信息成功.",channelId); } /** * 讀取到消息 * * 注意,從邏輯上來說,進行到這一步,客戶端已經(jīng)發(fā)送了它的請求報文,并且我們也收到目標服務器的響應. * 那么似乎可以直接使用如下語句,在將消息發(fā)回給客戶端后,關閉與客戶端的連接通道. * ctx.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE); * 但據(jù)我理解,瀏覽器會復用一些通道,所以最好不要關閉. * (ps: 我關閉后,看直播時,無法加載出視頻.... 不將它關閉,就一切正常. 并且,我之前測試過,客戶端多次連接會使用相同id的channel. * 也就是同一個TCP連接.) * */ @Override public void channelRead(ChannelHandlerContext ctx0, Object msg) throws Exception { //目標主機的響應數(shù)據(jù) // FullHttpResponse response = (FullHttpResponse) msg; log.info(LOG_PRE + ",讀取到響應.",ProxyUtil.getChannelId(ctx)); //發(fā)回給客戶端 ProxyUtil.writeAndFlush(ctx, msg, true); }


總結

bug




[2018-01-24 12:44:36.841] [nioEventLoopGroup-4-7 ] [INFO ] [c.zx.jump.handler.ProxyServerHandler:68 ] - [代理服務器處理類]通道id:5cfcc6ca,https請求.目標:account.youku.com:443 [2018-01-24 12:44:36.842] [nioEventLoopGroup-4-7 ] [INFO ] [com.zx.jump.util.ProxyUtil :33 ] - 通道id:5cfcc6ca,正在向客戶端寫入數(shù)據(jù). [2018-01-24 12:44:36.843] [nioEventLoopGroup-4-7 ] [INFO ] [com.zx.jump.util.ProxyUtil :36 ] - 通道id:5cfcc6ca,向客戶端寫入數(shù)據(jù)成功. [2018-01-24 12:44:37.077] [nioEventLoopGroup-4-7 ] [ERROR] [c.zx.jump.handler.ProxyServerHandler:156 ] - [代理服務器處理類]通道id:5cfcc6ca,發(fā)生異常:Connection reset by peer[2018-01-24 12:44:46.849] [nioEventLoopGroup-4-3 ] [INFO ] [c.zx.jump.handler.ProxyServerHandler:87 ] - [代理服務器處理類]通道id:b00b2836,http請求.目標:http://iku.youku.com/onlineconfig/iku-caller-js [2018-01-24 12:44:46.861] [nioEventLoopGroup-2-3 ] [INFO ] [c.z.j.l.HttpChannelFutureListener :44 ] - [http連接建立監(jiān)聽器]通道id:b00b2836,與目標主機建立連接成功. [2018-01-24 12:44:47.183] [nioEventLoopGroup-2-3 ] [INFO ] [c.zx.jump.handler.HttpConnectHandler:61 ] - [Http連接處理類]通道id:b00b2836,讀取到響應. [2018-01-24 12:44:47.184] [nioEventLoopGroup-2-3 ] [INFO ] [com.zx.jump.util.ProxyUtil :33 ] - 通道id:b00b2836,正在向客戶端寫入數(shù)據(jù). [2018-01-24 12:44:47.185] [nioEventLoopGroup-4-3 ] [INFO ] [com.zx.jump.util.ProxyUtil :36 ] - 通道id:b00b2836,向客戶端寫入數(shù)據(jù)成功. [2018-01-24 12:44:47.432] [nioEventLoopGroup-4-3 ] [ERROR] [c.zx.jump.handler.ProxyServerHandler:156 ] - [代理服務器處理類]通道id:b00b2836,發(fā)生異常:Connection reset by peer

關鍵詞:代理,服務,簡單

74
73
25
news

版權所有? 億企邦 1997-2025 保留一切法律許可權利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點擊下載Chrome瀏覽器
關閉