nio實現http及https代理

一,回望BIO

           上篇博文用了java 阻塞模型socket實現了http及https代理,也簡單的說了下其主要缺點是比較耗費資源或者更好的說法是資源利用率不高,爲什麼呢?一旦客戶端和代理服務器每建立一個連接(基本上每請求一個url就會建立一個新的連接)而我們實現的代理服務器爲了去監聽客戶端發給服務器端的消息並轉發就建立一個線程去專門的監聽相應的流並處理轉發這些數據到目標服務器;同樣,對於每一個目標服務器到我們實現的代理服務器的連接,上篇博文也是都新建一個線程去監聽維護流發來到代理服務器的消息並轉發到客戶端。這樣的設計很簡單很直觀——客戶端和目標服務器每建立一個連接直接就新建一個線程去監聽就行了,你不用去管http和https的中間溝通的細節,只需要知道https建立的握手環節就行。但是針對http和https都是一問一答式的交互,顯然,我們可以把監聽客戶端連接的線程和監聽服務器連接的線程合併到一個線程中,這樣就又減少了線程的數量,減少了線程上下文切換的消耗。不過這樣還是不得不用一個線程去維持客戶端和目標服務器到我們的代理服務器的連接,即使他們中間不傳數據或者說數據已經傳完了(我們可以對socket設置so_timeout來讓socket 過期,並在線程中拋出我們將捕獲的異常來結束線程來回收一些線程資源,但是不可以設置過短,不然有些網站可能網速波動後,就會導致代理服務器斷開連接了)。

二,NIO特點分析

          那麼nio到底好在哪呢?簡單來說,nio可以用一個線程來處理代理服務器與客戶端以及目的服務器的所有連接。它爲什麼能做到,主要是因爲nio中的selector,channel以及buffer。你要做的就是想辦法把連接都附着在一個channel上然後註冊到一個selector,讓這個selector去管理這些channel,buffer則是內存緩衝的東西負責接收channel發來的信息或者承載你將要發送到channel的信息,我自己的實踐中主要用了ByteBuffer,其他幾個使用方式基本相同,之所以使用nio包下的這幾個buffer是因爲封裝的操作很適合nio開發,因爲nio不想bio每次讀寫內容都讀寫至結束,nio則需要記錄這些讀寫過程點,而nio包下的幾種buffer就提供這些很方便的api。這麼說可能有點抽象,讀者可以自己去動手實踐便可體會。

三,NIO實現http(s)代理服務器分析

          把個人在實現http代理服務器過程中認爲比較重要踩坑也較深的地方着重提一下,再給出實現代碼,讀者可以在看完我的這段話後自己試着實現,然後再看我的代碼。

          1,理解http和https請求的請求響應過程,都是一問一答模式。2,https有一個握手過程是明文傳遞消息的,通過這個可以建立最初連接,之後都是在這個建立的通道上代理傳遞客戶端傳到服務器的消息的。3,要有清晰地認識,nio在傳遞消息的過程中一定要對buffer中存入的消息有明確的記錄(這個記錄意思是,讀知道讀到的第一個字節(字符)在哪,能讀到的最後一個字節(字符)在哪;寫:第一個應該寫在的位置應該在哪,最後一個能寫的位置在哪。)4,有一個意識,我們這裏針對buffer的使用都是單線程的,所以不用擔心會不會一邊channel在讀,另一邊通道在寫的情況的。建議的是時刻將channel的readable事件註冊給selector,而其writable要在滿足某種情況下你手動設定(比如你有內容讀到buffer中時),不要在一開始在生成一個channel時就把channel的writable事件註冊給selector,因爲正常情況下,channel都是writable的(原諒我這麼中英結合,因爲我覺得翻譯成中真的有點別),這就會導致你的cpu會被大量佔用。5,打日誌。日誌要特別重要,特別體現你的思路,不然會很亂,不好分析(debug就別想了)6,異常捕獲,因爲我們這裏使用單線程實現代理服務器的所以,一旦某個地方出錯可能就會導致程序停掉,你肯定不希望某一個url訪問錯誤就導致你的代理服務器停掉吧。

好了,有了上面的的一些經驗,下面給出自己的實現。

四,NIO的https實現

package server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.swing.text.Position;

public class ServerMain {

	private static Map<SocketChannel,SocketChannel> proxyChannelMap = new HashMap<>();              //client socket 對應的代理到目的服務器的socket
	private static Map<SocketChannel,ByteBuffer> backBufferToClient = new HashMap<SocketChannel,ByteBuffer>();  //要發給客戶端的消息
	private static Map<SocketChannel,ByteBuffer> forwardBufferToServer = new HashMap<>();        //要發給目的服務器的消息
	public static void main(String[] s) {
		buildServerSocketChannel();
//		ServerSocketChannel ssc = new ServerSocket(11111);
	}
	public static ServerSocketChannel buildServerSocketChannel() {
		ServerSocketChannel ssc  = null;
		try {
			ssc= ServerSocketChannel.open();
			ssc.configureBlocking(false);
			ssc.socket().setReuseAddress(true);
			ssc.bind(new InetSocketAddress(11111));
			System.out.println("server channel built");
			Selector selector = Selector.open();
			ssc.register(selector, SelectionKey.OP_ACCEPT);
			while(true) {
				System.out.println("==========================");
					if(selector.select()>0) {
						Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
						while(iterator.hasNext()) {
							SelectionKey selectionKey = iterator.next();
							iterator.remove();
							try {
								if(selectionKey.isAcceptable()) {
									afterAccept(selectionKey);
								}
								
								if(selectionKey.isConnectable()) {
									afterConnect(selectionKey);
								}
								if(selectionKey.isReadable()) {
									SocketChannel channel = (SocketChannel) selectionKey.channel(); 
									String side = (String) selectionKey.attachment();
									if(side.equals("0")) {         //此channel是客戶端與代理服務器的channel
										ByteBuffer buffer = null;
										SocketChannel proxyChannel = proxyChannelMap.get(channel);
										if(proxyChannel!=null) {
											if(forwardBufferToServer!=null && forwardBufferToServer.get(proxyChannel) !=null) {
												buffer = forwardBufferToServer.get(proxyChannel);
											}else {
												buffer = ByteBuffer.allocate(1024*2);
											}
											try {
												channel.read(buffer);  //debug一下,看看在channel準備好的情況下,能不能一次性把所有內容讀到buffer
												buffer.flip();
												if(buffer.limit()>0) {
													forwardBufferToServer.put(proxyChannel, buffer);
													selectionKey.interestOps(0);
													proxyChannel.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
												}
											} catch (IOException e) {
												proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
												selectionKey.cancel();
												channel.close();
												proxyChannelMap.get(channel).close();
											}
										}else {        //第一次連接
											buffer = ByteBuffer.allocate(1024*2);
											channel.read(buffer);
											selectionKey.interestOps(0);
											buffer.flip();
											String headerStr = new String(buffer.array(),buffer.position(),buffer.limit(),"US-ASCII");
											String[] lineStrs = headerStr.split("\\n");
											String host="";
											int port = 80;
											int type=0;                   //默認是http方式
											String hostTemp="";
												for(int i=0 ; i<lineStrs.length ; i++) {         //解析請求頭
													System.out.println(lineStrs[i]);
													if(i==0) {
														type = (lineStrs[i].split(" ")[0].equalsIgnoreCase("CONNECT") ? 1 : 0);
													}else {
														String[] hostLine = lineStrs[i].split(": ");
														if(hostLine[0].equalsIgnoreCase("host")) {
															hostTemp = hostLine[1];
														}
													}
												}
												if(hostTemp.split(":").length>1) {
													host = hostTemp.split(":")[0];
													port = Integer.valueOf(hostTemp.split(":")[1].split("\\r")[0]);
												}else {
													host = hostTemp.split(":")[0].split("\\r")[0];
												}
												try {
													proxyChannel = SocketChannel.open(new InetSocketAddress(host, port));
												} catch (Exception e) {
													continue;
												}
												proxyChannel.configureBlocking(false);
												proxyChannel.register(selector, SelectionKey.OP_CONNECT, "1");//將代理到目的服務器的連接註冊到selector
												proxyChannelMap.put(channel, proxyChannel);
												proxyChannelMap.put(proxyChannel,channel);
												if(type==1) {    //https直接返回告訴客戶端連接已建立
													ByteBuffer writeBuffer = ByteBuffer.wrap("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
													backBufferToClient.put(channel, writeBuffer);
													selectionKey.interestOps(SelectionKey.OP_WRITE);
												}else {
													forwardBufferToServer.put(proxyChannel, buffer);
													proxyChannel.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
												}
										}
									}else {    //此channel是代理服務器與目標服務器的channel
										ByteBuffer buffer = null;
										SocketChannel proxyChannel = proxyChannelMap.get(channel); //獲取代理服務器到client的連接通道
										if(proxyChannel==null) {
											System.out.println("----------------channel broken!!");
										}else {
											if(backBufferToClient!=null && backBufferToClient.get(proxyChannel) !=null) {
												buffer = backBufferToClient.get(proxyChannel);
											}else {
												buffer = ByteBuffer.allocate(1024*2);
											}
											try {
												channel.read(buffer);  
												buffer.flip();
												if(buffer.limit()>0) {
													selectionKey.interestOps(0);
													backBufferToClient.put(proxyChannel, buffer);
													proxyChannel.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
												}
											} catch (IOException e) {
												if(proxyChannelMap.get(selectionKey.channel())!=null) {
													proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
													proxyChannelMap.get(channel).close();
												}
												selectionKey.cancel();
												channel.close();
												System.out.println("++++++++++++++++++++++++++++++close a client channel");
											}
										}
									}
								}
								if(selectionKey.isWritable()) {
									System.out.println("writeable++++++++++++++"+selectionKey.channel());
									SocketChannel channel = (SocketChannel) selectionKey.channel();
									String side = (String) selectionKey.attachment();
									try {
										if(side.equals("0")) {         //此channel是客戶端與代理服務器的channel
											int a=0;
											if(backBufferToClient != null && backBufferToClient.get(channel)!=null 
													&&(backBufferToClient.get(channel).position()<backBufferToClient.get(channel).limit())
													&& (a=channel.write(backBufferToClient.get(channel)))>0) {
												if(!backBufferToClient.get(channel).hasRemaining()) {
													backBufferToClient.get(channel).clear();
													if(proxyChannelMap.get(channel) !=null) {
														proxyChannelMap.get(channel).keyFor(selector).interestOps(SelectionKey.OP_READ);
													}
													selectionKey.interestOps(SelectionKey.OP_READ);
												}else {
													if(proxyChannelMap.get(channel) !=null) {
														proxyChannelMap.get(channel).keyFor(selector).interestOps(0);
													}
													selectionKey.interestOps(SelectionKey.OP_WRITE);
												}
											}
										}else {
											int a=0;
											if(forwardBufferToServer != null && forwardBufferToServer.get(channel) !=null
													&&(forwardBufferToServer.get(channel).position()<forwardBufferToServer.get(channel).limit())
													&& (a=channel.write(forwardBufferToServer.get(channel))) >0) {
												if(!forwardBufferToServer.get(channel).hasRemaining()) {
													forwardBufferToServer.get(channel).clear();
													if(proxyChannelMap.get(channel) !=null) {
														proxyChannelMap.get(channel).keyFor(selector).interestOps(SelectionKey.OP_READ);
													}
													selectionKey.interestOps(SelectionKey.OP_READ);
												}else {
													if(proxyChannelMap.get(channel) !=null) {
														proxyChannelMap.get(channel).keyFor(selector).interestOps(0);
													}
													selectionKey.interestOps(SelectionKey.OP_WRITE);
												}
											}
										}
									} catch (IOException e) {
										if(proxyChannelMap.get(selectionKey.channel())!=null) {
											proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
											proxyChannelMap.get(channel).close();
										}
										selectionKey.cancel();
										channel.close();
									}
								}
							} catch (CancelledKeyException e) {
								System.out.println("cancle channel:"+selectionKey.channel());
								System.out.println("cancle proxy channel:"+proxyChannelMap.get(selectionKey.channel()));
								if(proxyChannelMap.get(selectionKey.channel())!=null){
									proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
									proxyChannelMap.remove(proxyChannelMap.get(selectionKey.channel()));
								}
								selectionKey.cancel();
								System.out.println("cancle the selectionkey+++++++++++++++++++++++++++");
								proxyChannelMap.remove(selectionKey.channel());
							} 
						}
					}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return ssc;
	}
	
	public static void afterAccept(SelectionKey selectionKey) {
		try {
			ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
			SocketChannel socketChannel = ssc.accept();
			socketChannel.configureBlocking(false);
			Selector selector = selectionKey.selector();
			socketChannel.register(selector, SelectionKey.OP_READ,"0");
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	public static void afterConnect(SelectionKey selectKey) {
		try {
			SocketChannel socketChannel = (SocketChannel) selectKey.channel();
			Selector selector = selectKey.selector();
			socketChannel.keyFor(selector).interestOps(SelectionKey.OP_READ);
		} catch (Exception e) {
			System.out.println("建立到目的服務器socket後出錯:"+e);
		}
	}
}

          必須承認,上面的程序我認爲沒有必要每次在讀結束後執行selectionKey.interestOps(0);這句話,這句話作用是暫時讓selector不對剛讀完的這個通道的任何事件感興趣,也就是希望當把剛讀出來的buffer內容全部寫到對應的到目的服務器的通道後在觸發selector再次對這個通道的讀事件感興趣,設計這句話的初衷是因爲我擔心因爲下一步要讀和要寫必須有一個確定的步驟(因爲nio中的buffer對於讀和寫用的是同一個position和limit,多說一句netty中就把buffer中的讀和寫的position和limit就區分開了,所以就不需要有這樣的擔心),但是後來想一想,我這個是單線程的啊,一次操作必定是寫或者讀啊,所以我原來的擔心完全是沒必要的,這裏之所以把代碼留下來,也正好加深一下印象。這不會有問題,而且向我們上面所說的channel大部分時候都是writable的,所以等將剛讀出到buffer寫完到通道纔再次觸發原來通道可讀是效率不大受影響的。但確實在理論上,這個地方確實是不應該設置的。當你要試圖去掉每次讀後的這行代碼時,記得在每次寫完後,執行buffer.compact(),這個方法什麼作用,你自己查詢吧。

五,個人總結

        nio真的就那麼完美無缺嗎?我看不一定,經過個人實踐寫出的單線程nio代理服務器,首先一個體驗——很容易出錯,難調試分析錯誤比較難。其次,其實說實話這個單線程版本nio實現的代理服務器展示出來的效果並沒有bio的效果好。分析起來也很正常,雖然nio在我們開發http代理服務器時在訪問資源時會很節省資源,但是人家bio是多線程啊,當你實現這個代理服務器只是爲你自己用時根本不在乎多那麼幾個線程時,肯定是多線程處理更快啊,而nio中對所有channel的io操作都是在一個線程中完成的,肯定會出現頁面刷出比較慢,尤其是頁面元素(圖片較多)時會發現刷出慢。但是,nio省資源那是沒得說的,在實現聊天那樣小流量功能時,這當然非常棒啊,而且我們可以搞多個selector,去監聽不同的ServerSocket從而去另開一個線程去監聽連接,把客戶端的連接負載均衡到這些selector上,這就可以利用你的服務器多核的優勢了,而且很大程度的節省內存資源cpu線程切換耗損。

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