多線程+非阻塞通信導致多次處理讀事件

多線程+非阻塞通信導致多次處理讀事件

有問題的代碼:Echo服務端

簡單的解釋一下,事件處理器handler有一個handle(SelectionKey)方法,根據方法的不同,執行不同的操作。對於讀事件,創建一個線程進行讀事件處理。

public class MutiThreadServer {
	private static final int PORT = 8888;
	private ServerSocketChannel severSocketChannel;
	private Selector selector;
	private EventHandler handler;//自定義的事件處理器
	
	public MutiThreadServer() throws IOException {
		//創建serverSocketChannel
		this.severSocketChannel = ServerSocketChannel.open();
		//開啓非阻塞方式
		this.severSocketChannel.configureBlocking(false);
		//綁定地址,開啓監聽
		this.severSocketChannel.bind(new InetSocketAddress(PORT));
		
		//創建selector
		this.selector = Selector.open();
		//註冊accepte事件
		this.severSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//創建事件處理器
		this.handler = new EventHandler(selector);
	}
	
	private static class EventHandler{
		
		private Selector selector;
		
		public EventHandler(Selector selector) {
			this.selector = selector;
		}
		
		public void handle(SelectionKey key) throws IOException {
			if(key.isAcceptable()) {
				ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
				SocketChannel clientChannel = serverSocketChannel.accept();
				clientChannel.configureBlocking(false);
				clientChannel.register(this.selector, SelectionKey.OP_READ, Boolean.FALSE);
			}else if(key.isReadable()) {

				//啓動一個線程進行讀事件的處理
				System.out.println("觸發讀事件");
				//讀事件已經交給一個線程處理
				(new ReadHandler(key)).start();
			}
		}
		
		private static class ReadHandler extends Thread{
			private SelectionKey selectionKey;
			private SocketChannel clientChannel;
			private String clientIp;
			private int clientPort;
			public ReadHandler(SelectionKey key) throws IOException {
				this.selectionKey = key;
				this.clientChannel = (SocketChannel) key.channel();
				InetSocketAddress address = (InetSocketAddress)clientChannel.getLocalAddress();
				clientIp = address.getHostString();
				clientPort = address.getPort();
			}
			@Override
			public void run() {
				ByteBuffer buffer = ByteBuffer.allocate(1024);
				try {
					clientChannel.read(buffer);
					buffer.flip();
					//服務端輸出
					String content = new String(buffer.array(), 0, buffer.limit());
					System.out.println("收到信息 [" + this.clientIp + " : " + this.clientPort + "] " + content);
					//回送給客戶端				
					this.clientChannel.write(buffer);
					//收到exit,退出
					if("exit".equals(content)) {
						System.out.println("與 [ " + this.clientIp + " : " + this.clientPort + "]的連接已斷開");
						try {
							this.clientChannel.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					}					
				
					//準備下一次輸入
					buffer.clear();	
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

		}
	}
	
	public void service() {
		//是否有事件觸發
		try {
			while(selector.select() > 0) {
				//獲得已經觸發的事件
				Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
				while(selectionKeys.hasNext()) {
					SelectionKey key = selectionKeys.next();
					//刪除即將處理過的事件,因爲使用多線程進行處理,避免多次處理
					selectionKeys.remove();
					handler.handle(key);
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				this.severSocketChannel.close();
				this.selector.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		try {
			(new MutiThreadServer()).service();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

當客戶端發送一次數據後,服務端的輸出:
在這裏插入圖片描述

產生問題的原因

可以看到,讀事件被處理了三次。
造成這個結果的原因:

事件處理速度 > 三次select()的間隔,也就是說,讀事件還在處理的時候,下一次的select()已經運行,由於讀事件還沒有完成,所以讀事件仍然是被觸發的狀態,導致了多次的處理讀事件。

驗證問題

產生問題的原因:事件處理速度 > select()速度
所以,增加select()之間的間隔事件。通過sleep(5000),是兩次select()之間的的間隔增加到5s

while(selector.select() > 0) {
	//獲得已經觸發的事件
	Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
	while(selectionKeys.hasNext()) {
		SelectionKey key = selectionKeys.next();
		//刪除即將處理過的事件,因爲使用多線程進行處理,避免多次處理
		selectionKeys.remove();
		handler.handle(key);
	}
	try {
		Thread.currentThread().sleep(5000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

運行結果:
在這裏插入圖片描述

問題解決

通過線程睡眠的方式,只是驗證對問題的分析是否正確,並沒有真正的解決這個問題,通信的數據量是不確定的,很難確定sleep()的值,況且,通過sleep()本身就與非阻塞通信矛盾。

多次處理的原因:第二次處理事件時,不知道該事件已經被處理過了

所以,可以通過一個標記,表示這個事件已經被處理,不需要再次處理了。

每個SelectionKey,都可以綁定一個對象,在這裏,可以綁定一個Boolean,表示是否已經被處理了。只要在處理時判斷它是否被處理了即可避免多次處理。在處理完成後,一定要把標記修改爲False,否則,下一個讀事件觸發時,就沒辦法處理了。

public class MutiThreadServer {
	private static final int PORT = 8888;
	private ServerSocketChannel severSocketChannel;
	private Selector selector;
	private EventHandler handler;//自定義的事件處理器
	
	public MutiThreadServer() throws IOException {
		//創建serverSocketChannel
		this.severSocketChannel = ServerSocketChannel.open();
		//開啓非阻塞方式
		this.severSocketChannel.configureBlocking(false);
		//綁定地址,開啓監聽
		this.severSocketChannel.bind(new InetSocketAddress(PORT));
		
		//創建selector
		this.selector = Selector.open();
		//註冊accepte事件
		this.severSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//創建事件處理器
		this.handler = new EventHandler(selector);
	}
	
	private static class EventHandler{
		
		private Selector selector;
		
		public EventHandler(Selector selector) {
			this.selector = selector;
		}
		
		public void handle(SelectionKey key) throws IOException {
			if(key.isAcceptable()) {
				ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
				SocketChannel clientChannel = serverSocketChannel.accept();
				clientChannel.configureBlocking(false);
				clientChannel.register(this.selector, SelectionKey.OP_READ, Boolean.FALSE);
			}else if(key.isReadable()) {
				if((Boolean)key.attachment()) {
					return ;
				}
				//啓動一個線程進行讀事件的處理
				System.out.println("觸發讀事件");
				//讀事件已經交給一個線程處理
				key.attach(Boolean.TRUE);
				(new ReadHandler(key)).start();
			}
		}
		
		private static class ReadHandler extends Thread{
			private SelectionKey selectionKey;
			private SocketChannel clientChannel;
			private String clientIp;
			private int clientPort;
			public ReadHandler(SelectionKey key) throws IOException {
				this.selectionKey = key;
				this.clientChannel = (SocketChannel) key.channel();
				InetSocketAddress address = (InetSocketAddress)clientChannel.getLocalAddress();
				clientIp = address.getHostString();
				clientPort = address.getPort();
			}
			@Override
			public void run() {
				ByteBuffer buffer = ByteBuffer.allocate(1024);
				try {
					clientChannel.read(buffer);
					buffer.flip();
					//服務端輸出
					String content = new String(buffer.array(), 0, buffer.limit());
					System.out.println("收到信息 [" + this.clientIp + " : " + this.clientPort + "] " + content);
					//回送給客戶端				
					this.clientChannel.write(buffer);
					//收到exit,退出
					if("exit".equals(content)) {
						System.out.println("與 [ " + this.clientIp + " : " + this.clientPort + "]的連接已斷開");
						try {
							this.clientChannel.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					}					
				
					//準備下一次輸出
					buffer.clear();	
					//事件處理完成,下一次觸發該事件時,是未處理狀態
					this.selectionKey.attach(Boolean.FALSE);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

		}
	}
	
	public void service() {
		//是否有事件觸發
		try {
			while(selector.select() > 0) {
				//獲得已經觸發的事件
				Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
				while(selectionKeys.hasNext()) {
					SelectionKey key = selectionKeys.next();
					//刪除即將處理過的事件,因爲使用多線程進行處理,避免多次處理
					selectionKeys.remove();
					handler.handle(key);
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				this.severSocketChannel.close();
				this.selector.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		try {
			(new MutiThreadServer()).service();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

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