網絡編程之Selector & SelectionKey詳解(二)

這篇博文我希望能總結出一個實用的簡單客戶端服務器通信示例,我在前面幾篇博文中說過,socket通信中存在數據無邊界的問題,這樣造成不知道read數據是否完成;在前面幾篇博文中我們看到在對客戶端SocketChannel進行註冊感興趣的事件時,並沒有註冊SelectionKey.OP_WRITE寫事件,是因爲寫事件註冊之後客戶端通道大多數情況下都會立即滿足可以寫數據的條件,即使這個時候服務器邏輯還沒有生成要寫回去的數據,所以對於客戶端SocketChannel的寫事件,我們一般不是在註冊通道時註冊,而是在服務器準備好要寫回的數據之後,通過選擇鍵的interestOps(int paramInt)這個方法進行設置。

這篇博文代碼示例期望能結界,(1)數據無邊界傳輸問題,(2)服務器端讀寫事件的轉換問題。如果讀者曾經看過Thrit通信組件的源碼的話,你會發現筆者所總結的這個示例就是按照Thrit的服務器端通信模型進行編寫的。我們下面直接上代碼。

下面是客戶端的代碼,這個與前幾篇博文中所使用的客戶端通信代碼差不多,只不過我這裏使用的是阻塞模式的客戶端通信模式。

package com.yujie.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
 * @author yujie.wang
 *	SocketChannel 客戶端代碼測試
 */
public class SocketChannel_Client {
	
	private final static String DEFAULT_HOST = "127.0.0.1";
	
	private final static int DEFAULT_PORT = 3456;
	
	private SocketChannel channel;
	
	private Socket socket;
	
	//分配一個大小爲50字節的緩衝區 用於客戶端通道的讀寫
	private ByteBuffer buffer = ByteBuffer.allocate(50);
	
	public SocketChannel_Client(){
		this(DEFAULT_HOST, DEFAULT_PORT);
	}
	
	public SocketChannel_Client(String host, int port){
		init(host,port);
	}
	
	/**
	 * 打開通道並設置對等的客戶端socket對象
	 * 建立與服務端通道的連接
	 * @param host
	 * @param port
	 */
	public void init(String host, int port){
		try {
			//打開一個客戶端通道,同時當前通道並沒有與服務端通道建立連接
			channel = SocketChannel.open();
			//獲得對等的客戶端socket
			socket = channel.socket();
			//配置客戶端socket
			setSocket();
			//將通道設置爲非阻塞工作方式,如果通道設置我阻塞模式,那麼這與直接使用傳統Socket通信是一樣的
			//channel.configureBlocking(false);
			//異步連接,發起連接之後就立即返回
			//返回true,連接已經建立
			//返回false,後續繼續建立連接
			channel.connect(new InetSocketAddress(host,port));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/**
	 * 驗證連接是否建立
	 */
	public void finishConnect(){
		try {
			while(!channel.finishConnect()){
				// nothing to do,wait connect
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 驗證當前連接是否可用
	 */
	public void isConnected(){
		try {
			if(channel == null || !channel.isConnected())
				throw new IOException("channel is broken");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 配置客戶端通道對等的Socket
	 */
	public void setSocket(){
		try {
			if(socket != null ){
				//設置socket 讀取的超時時間5秒
				//socket.setSoTimeout(5000);
				//設置小數據包不再組合成大包發送,也不再等待前一個數據包返回確認消息
				socket.setTcpNoDelay(true);
				//設置如果客戶端Socket關閉了,未發送的包直接丟棄
				socket.setSoLinger(true, 0);
			}
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	//java.lang.IllegalArgumentException
	public void write(String data) {
		byte [] datas = data.getBytes();
		buffer.clear();
		buffer.putInt(datas.length);
		buffer.put(data.getBytes());
		buffer.flip();
		try {
			// write並不一定能一次將buffer中的數據都寫入 所以這裏要多次寫入
			// 當多個線程同時調用同一個通道的寫方法時,只有一個線程能工作,其他現在則會阻塞
			while(buffer.hasRemaining()){
				channel.write(buffer);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void read(){
		try {
			buffer.clear();
			// read方法並不阻塞,如果有數據讀入返回讀入的字節數,沒有數據讀入返回0 ,遇到流的末尾返回-1
			// 當然這裏和Socket和ServerSocket通信一樣 也會存在消息無邊界的問題 我們這裏就採取簡單的讀取一次作爲示例
			System.out.println("read begin");
			channel.read(buffer);
		/*	while(buffer.hasRemaining() && channel.read(buffer) != -1){
				printBuffer(buffer);
			}*/
			buffer.flip();
			printBuffer(buffer);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 輸出buffer中的數據
	 * @param buffer
	 */
	public void printBuffer(ByteBuffer buffer){
		while(buffer.hasRemaining()){
			System.out.print((char)buffer.get());
		}
		System.out.println("");
		System.out.println("****** Read end ******");
	}
	
	/**
	 * 判斷通道是否打開
	 * @return
	 */
	public boolean isChannelOpen(){
		try {
			return channel.finishConnect() ? true : false;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return false;
	}
	
	/**
	 * 關閉通道
	 */
	public void closeChannel(){
		if(channel != null){
			try {
				channel.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
	//	client(DEFAULT_HOST,DEFAULT_PORT);
		SocketChannel_Client client = new SocketChannel_Client();
		client.finishConnect();
		System.out.println("connect success");
		//這裏連續進行三次讀寫,希望模擬的是連續三次客戶端方法調用
		for(int i = 0;i<3;i++){
			client.write("Hello World");
			client.read();
		}
		//客戶端等待一段時間之後 直接退出
		sleep(15000);
		System.out.println("client exit");
 
	}
	
	public static void sleep(long time){
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

服務器端代碼我新起了一個線程用於處理SocketChannel的通信和業務處理邏輯:

package com.yujie.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
 * @author yujie.wang
 *服務端代碼示例,我單獨起了一個線程用於處理SocketChannel通信和業務處理邏輯
 */
public class ServerSocketChannel_Server_Thread {
	
	private ServerSocketChannel serverChannel;
	
	private ServerSocket serverSocket;
	
	private Selector selector;
	
	private Thread thread;
	
	private static int DEFAULT_BIND_PORT = 3456;
	
	public ServerSocketChannel_Server_Thread(){
		this(DEFAULT_BIND_PORT);
	}
	
	public ServerSocketChannel_Server_Thread(int bindPort){
		init(bindPort);
	}
	
	public void init(int bindPort){
		try {
			//打開一個服務端通道
			serverChannel = ServerSocketChannel.open();
			//獲得對等的ServerSocket對象
			serverSocket = serverChannel.socket();
			//將服務端ServerSocket綁定到指定端口
			serverSocket.bind(new InetSocketAddress(bindPort));
			System.out.println("Server listening on port: "+ bindPort);
			//將通道設置爲非阻塞模式
			serverChannel.configureBlocking(false);
			//打開一個選擇器
			selector = Selector.open();
			//將通道註冊到打開的選擇器上
			serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println("init exception: "+e);
		}

	}
	
	public void select(){
		try {
			//select()方法會阻塞,直到有準備就緒的通道有準備好的操作;或者當前線程中斷該方法也會返回
			//這裏的返回值不是選擇器中已選擇鍵集合中鍵的數量,而是從上一次select()方法調用到這次調用期間進入就緒狀態通道的數量
			selector.select();
			//獲得已選擇鍵的集合這個集合中包含了 新準備就緒的通道和上次調用select()方法已經存在的就緒通道
			Set<SelectionKey> set = selector.selectedKeys();
			Iterator it = set.iterator();
			while(it.hasNext()){
				SelectionKey key = (SelectionKey)it.next();
				//通過調用remove將這個鍵key從已選擇鍵的集合中刪除
				it.remove();
				if(key.isAcceptable()){
					handleAccept(key);
				}else if(key.isReadable()){
					handleRead(key);
				}else if(key.isWritable()){
					handleWrite(key);
				}
		
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void handleAccept(SelectionKey key){
		try {
			//因爲能註冊SelectionKey.OP_ACCEPT事件的只有 ServerSocketChannel通道,
			//所以這裏可以直接轉換成ServerSocketChannel
			ServerSocketChannel channel = (ServerSocketChannel)key.channel();
			//獲得客戶端的SocketChannel對象 ,accept這裏不會阻塞,如果沒有連接到來,這裏會返回null
			SocketChannel clientChannel = channel.accept();
			System.out.println("Accepted Connected from: "+ clientChannel);
			//將客戶端socketChannel設置爲非阻塞模式
			clientChannel.configureBlocking(false);
			//爲該客戶端socket分配一個ByteBuffer
			SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ);
			BufferFrame buffer = new BufferFrame(clientChannel,clientKey);
			clientKey.attach(buffer);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println("handleAccept exception: "+ e);
		}
	}
	
	public void handleRead(SelectionKey key){
		//從鍵中獲得相應的客戶端socketChannel
		BufferFrame buffer = (BufferFrame)key.attachment();
		//從通道中讀取數據 如果讀取數據失敗 則關閉通道 註銷選擇鍵中通道和緩衝區之間的註冊關係
		if(!buffer.read()){
			System.out.println("buffer read error");
			cleanSelectionkey(key);
		}
		//如果通道讀取完數據之後 處理數據 並改變通道所註冊的緩衝區的狀態
		if(buffer.readCompleteFrame()){
			//System.out.println("print data");
			buffer.printReadContent();
		}
	}
	
	/**
	 * 通過通道將數據寫會調用請求的客戶端
	 * @param key
	 */
	public void handleWrite(SelectionKey key){
		//System.out.println("handle write");
		BufferFrame buffer = (BufferFrame)key.attachment();
		if(!buffer.write())
			cleanSelectionkey(key);
	}
	
	/**
	 * 關閉通道 取消通道和選擇器之間的註冊關係
	 * @param key
	 */
	public void cleanSelectionkey(SelectionKey key){
		try {
			BufferFrame buffer = (BufferFrame)key.attachment();
			key.channel().close();
			key.cancel();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void serverStart(){
		//開啓一個線程
		thread = new Thread( new AcceptImplThread());
		thread.start();
	}
	
	/**
	 * 該方法時期望主線程阻塞,指導任務線程執行結束
	 */
	public void joinThread(){
		try {
			thread.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	class AcceptImplThread implements Runnable{

		@Override
		public void run() {
			// TODO Auto-generated method stub
			try {
				while(true){
					System.out.println("select begin to run");
					select();
					System.out.println("select end to run");
				}
			} catch (Exception e) {
				// TODO: handle exception
				System.out.println("run exception: "+e);
			}finally{
				try {
					for(SelectionKey key : selector.keys()){
						key.channel().close();
						key.cancel();
					}
				} catch (Exception e2) {
					// TODO: handle exception
				}
			}
		}
		
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ServerSocketChannel_Server_Thread server = new ServerSocketChannel_Server_Thread();
		server.serverStart();
		server.joinThread();
		System.out.println("server exit");
	}

}

BufferFrame類主要是完成對客戶端發送數據的讀寫轉換,以及通道自身讀寫事件的註冊轉換

package com.yujie.client;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

/**
 * @author yujie.wang
 * 該方法用於讀寫客戶端socketChannel
 * 這個類主要解決就是服務端數據的讀寫轉換和讀寫事件的註冊
 */
public class BufferFrame {
	private SocketChannel clientChannel;
	
	private SelectionKey key;
	
	private ByteBuffer buf;
	
	private BufferFrameState state = BufferFrameState.READING_FRAME_SIZE;
	
	public BufferFrame(SocketChannel channel, SelectionKey key){
		this.clientChannel = channel;
		this.key = key;
		buf = ByteBuffer.allocate(4);
	}
	
	public boolean read(){
		//讀取數據包頭部 獲取待讀取數據的大小
		if(state == BufferFrameState.READING_FRAME_SIZE){
			if(!internalRead())
				return false;
		}
		// 讀取完包的大小之後 取出數據大小 併爲緩衝區分配大小,改變當前讀取數據的狀態爲BufferFrameState.READING_FRAME
		if(buf.remaining() == 0){
			//System.out.println("read fram size end");
			int frameSize = buf.getInt(0);
			System.out.println("frameSize: "+ frameSize);
			if(frameSize < 0)
				return false;
			buf = ByteBuffer.allocate(frameSize);
			state = BufferFrameState.READING_FRAME;
		}else {
			return true;
		}
		//讀取客戶端發過來的數據
		if(state == BufferFrameState.READING_FRAME){
			if(!internalRead())
				return false;
			//讀取完數據之後清空該選擇鍵所感興趣的事件,並改變讀取數據的狀態爲READ_FRAME_COMPLETE
			if(buf.remaining() == 0){
				//System.out.println("read fram end");
				key.interestOps(0);
				state = BufferFrameState.READ_FRAME_COMPLETE;
				return true;
			}
		}
		System.out.println("read error");
		return false;
		
	}
	
	/**
	 * 從通道中讀取數據到buf中,每一次都從內核中儘量多的讀取數據嘗試填滿buf,當然如果一次填不滿buf數據,要進行多次讀取
	 * 當然如果沒有數據讀取 在通道處於非阻塞模式下改方法不會阻塞
	 * @return
	 */
	public boolean internalRead(){
		try {
			if(clientChannel.read(buf) < 0){
				System.out.println("read error inter");
				return false;
			}
			return true;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			System.out.println("BufferFrame read exception: "+ e);
			return false;
		}
	}

	/**
	 * 將數據寫回調用方
	 * @return
	 */
	public boolean write(){
		if(state == BufferFrameState.WRITING){
			try {
				//System.out.println("write begin");
				if(clientChannel.write(buf)<0)
					return false;
			} catch (IOException e) {
				// TODO Auto-generated catch block
				System.out.println("write exception: "+e);
				return false;
			}
			
			if(buf.remaining() == 0){
				System.out.println("write end");
				key.interestOps(SelectionKey.OP_READ);
			    buf = ByteBuffer.allocate(4);
			    state = BufferFrameState.READING_FRAME_SIZE;
			    return true;
			}
		}
		return false;

	}
	
	/**
	 * 改變當前緩衝區或者通道的狀態
	 */
	public void changeKeyState(){
		try {
			if(state == BufferFrameState.AWAITING_REGISTER_WRITE){
				//System.out.println("a");
				key.interestOps(SelectionKey.OP_WRITE);
				//System.out.println("b");
				state = BufferFrameState.WRITING;
			}else if(state == BufferFrameState.AWAITING_REGISTER_READ){
				key.interestOps(SelectionKey.OP_READ);
			    buf = ByteBuffer.allocate(4);
			    state = BufferFrameState.READING_FRAME_SIZE;
			} else if(state ==BufferFrameState.AWAITING_CLOSE){
				clientChannel.close();
				key.cancel();
			} else{
				System.out.println("changeSelectInterest was called, but state is invalid (" + state + ")");
			}
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println("changeKeyState exception: "+e);
		}
	}
	
	public void printReadContent(){
		buf.flip();
		while(buf.hasRemaining()){
			//System.out.println("positon: "  + buf.position()+ " limit:"+ buf.limit());
			System.out.print((char)buf.get());
		}
		buf.clear();
		System.out.println("");
		System.out.println("****** Read end ******");
		System.out.println("");
		
		//處理完數據之後 將當前緩衝區改爲寫數據狀態
		buf.put("ServerData".getBytes());
		buf.flip();
		//System.out.println("ready write data");
		state = BufferFrameState.AWAITING_REGISTER_WRITE;
		changeKeyState();
	}
	
	public boolean readCompleteFrame(){
		return state == BufferFrameState.READ_FRAME_COMPLETE;
	}
	
	/**
	 * 定義枚舉用於標示當前數據流的讀取狀態
	 * 這些狀態的實際轉換 要通過SelectionKey.interestOps(int ops)方法進行轉換
	 */
    private enum BufferFrameState {
    	//標示客戶端正在讀取數據大小 客戶端每次寫入數據都是先寫入 要讀取的數據大小 
	    READING_FRAME_SIZE,
	    //標示客戶端正在讀取數據
	    READING_FRAME,
	    //標示客戶端讀取數據完成。可以向寫入數據轉換了
	    READ_FRAME_COMPLETE,
	    //當客戶端讀取完數據之後,客戶端socketChannel可以註冊寫事件
	    AWAITING_REGISTER_WRITE,
	    //標示正在寫數據
	    WRITING,
	    //寫完數據之後 可以向讀取數據轉換
	    AWAITING_REGISTER_READ,
	    //標示當前通道可以進行關閉
	    AWAITING_CLOSE
	 }
}

如果正確執行客戶端和服務端代碼客戶端會輸出如下結果:

connect success
read begin
ServerData
****** Read end ******
read begin
ServerData
****** Read end ******
read begin
ServerData
****** Read end ******


服務端會輸出如下結果:

Server listening on port: 3456
select begin to run
Accepted Connected from: java.nio.channels.SocketChannel[connected local=/127.0.0.1:3456 remote=/127.0.0.1:56291]
select end to run
select begin to run
frameSize: 11
Hello World
****** Read end ******

select end to run
select begin to run
write end
select end to run
select begin to run
frameSize: 11
Hello World
****** Read end ******

select end to run
select begin to run
write end
select end to run
select begin to run
frameSize: 11
Hello World
****** Read end ******

select end to run
select begin to run
write end
select end to run
select begin to run



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