多线程+非阻塞通信导致多次处理读事件

多线程+非阻塞通信导致多次处理读事件

有问题的代码: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();
		}
	}
}

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