多線程+非阻塞通信導致多次處理讀事件
有問題的代碼: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();
}
}
}