NIO中Selector重复触发写事件的问题

本文探讨了使用Java NIO实现客户端与服务器通信时遇到的一个问题:客户端不断触发写操作,导致服务器接收重复消息。通过分析源码揭示了原因,并提出了解决方案。

最近使用NIO在写client和server端的Demo的时候,发生一个问题,就是client一直触发写操作,这样就导致服务端可能接收到客户端写入的多个重复的信息,首先看一下客户端和服务端的代码。

服务端

package com.ly.search.nio.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;

public class TimeServer {

    private int port;

    public TimeServer(int port) {
        this.port = port;
    }

    public void service() {
        ServerSocketChannel ssc = null;
        try {
            ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
            ssc.bind(new InetSocketAddress(port), 10);
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
			System.out.println("service run.....");
            while (true) {
                if (selector.select() == 0) {
                    continue;
                }

                Iterator<SelectionKey> i = selector.selectedKeys().iterator();
                while (i.hasNext()) {
                    SelectionKey sk = i.next();
					i.remove();

                    if (sk.isAcceptable()) {
                        SocketChannel sc = ((ServerSocketChannel) sk.channel())
                                .accept();
                        sc.configureBlocking(false);
						sc.register(selector, SelectionKey.OP_READ);
                    } else if (sk.isReadable()) {
                        SocketChannel sc = (SocketChannel) sk.channel();
						ByteBuffer bb = ByteBuffer.allocate(1024);
                        int readBytes = sc.read(bb);
                        if (readBytes > 0) {
                            bb.flip();
                            byte[] req = new byte[bb.remaining()];
                            bb.get(req);
                            String reqMsg = new String(req, "utf-8");
                            System.out
                                    .println("The time server receive order : "
                                            + reqMsg);
                            String respMsg = "QUERY TIME ORDER"
                                    .equalsIgnoreCase(reqMsg) ? new Date(
                                            System.currentTimeMillis())
                                                    .toString()
                                            : "BAD ORDER";
                            byte[] resp = respMsg.getBytes("utf-8");
                            ByteBuffer respBb = ByteBuffer
                                    .allocate(resp.length);
                            respBb.put(resp);
                            respBb.flip();
                            sc.register(sk.selector(), SelectionKey.OP_WRITE,
                                    respBb);
                        }
                    } else if (sk.isWritable()) {
                        ByteBuffer respBb = (ByteBuffer) sk.attachment();
                        if (respBb.hasRemaining()) {
                            SocketChannel sc = (SocketChannel) sk.channel();
                            sc.write(respBb);
                        }
                    }

                }

            }

        } catch (IOException e) {
			e.printStackTrace();
        } finally {
            if (ssc != null) {
                try {
                    ssc.close();
                } catch (IOException e) {
					e.printStackTrace();
                }
            }
        }

    }

    public static void main(String[] args) {
        new TimeServer(8888).service();
    }

}

客户端代码:

package com.ly.search.nio.clinet;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class TimeClient {

    public static void main(String[] args) throws Exception {
        SocketChannel sc = SocketChannel.open();
        Selector selector = Selector.open();
        sc.configureBlocking(false);
		boolean isConnect = sc.connect(new InetSocketAddress("localhost", 8888));
		if (!isConnect) {
			sc.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
		}

        boolean isStop = false;
        while (true) {
			int count = selector.select();
			if (count == 0)
				continue;
            Iterator<SelectionKey> i = selector.selectedKeys().iterator();
            while (i.hasNext()) {
                SelectionKey sk = i.next();
				i.remove();
				if (sk.isValid()) {
					if (sk.isConnectable()) {
						if (sc.finishConnect()) {
							sc.register(selector, SelectionKey.OP_WRITE);
						} else {
							System.exit(1);
						}
					} else if (sk.isWritable()) {
						System.out.println("writing");
						byte[] req = "QUERY TIME ORDER".getBytes("utf-8");
						ByteBuffer bb = ByteBuffer.allocate(req.length);
						bb.put(req);
						bb.flip();
						sc.write(bb);
					} else if (sk.isReadable()) {
						System.out.println("reading");
						ByteBuffer bb = ByteBuffer.allocate(1024);
						int readBytes = sc.read(bb);
						bb.flip();
						byte[] resp = new byte[readBytes];
						bb.get(resp);
						String respMsg = new String(resp, "utf-8");
						System.out.println("Now is " + respMsg);
						isStop = true;
						break;
					}
				}
            }

			if (isStop) {
				sc.close();
				break;
			}
        }

    }

}


首先运行服务端,然后运行客户端,运行的结果如下:

服务端:


然后客户端执行了多次的写操作,截图如下:



可以看到服务端收到了两条信息。

这里有两个疑问?

1.为什么客户端没有执行读事件?

2.为什么客户端一直执行写事件?

这个需要查看下SelectableChannel的register方法的源码:


我们一直点下去,进入AbstractSelectableChannel的register方法


重点看一下k.interestOps(ops)方法,然后进入SelectionKeyImpl.interestOps方法


在进入nioInterestOps(ops)方法


可以看到直接覆盖了之前设置的interestOps,也就是说我们在客户端完成连接,设置SelectionKey.OP_WRITE事件,其实已经把之前的事件全部覆盖了。

这样客户端只要接收到操作系统的可写事件,那么一直触发可写操作。

其实我们可以在看下和操作系统相关的源码,也就是看一下channel.translateAndSetInterestOps(ops,this)方法,由于是客户端,那么直接看SocketChannelImpl类


所以也是重新覆盖。


通过源码的阅读,我们大概知道怎么解决客户端重复写的问题,也就是在执行完OP_WRITE操作之后,在重新设置下OP_READ事件,这样就能只对读事件感兴趣。

修改之后的客户端源码如下:

package com.ly.search.nio.clinet;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class TimeClient {

    public static void main(String[] args) throws Exception {
        SocketChannel sc = SocketChannel.open();
        Selector selector = Selector.open();
        sc.configureBlocking(false);
		boolean isConnect = sc.connect(new InetSocketAddress("localhost", 8888));
		if (!isConnect) {
			sc.register(selector, SelectionKey.OP_CONNECT);
		}

        boolean isStop = false;
        while (true) {
			int count = selector.select();
			if (count == 0)
				continue;
            Iterator<SelectionKey> i = selector.selectedKeys().iterator();
            while (i.hasNext()) {
                SelectionKey sk = i.next();
				i.remove();
				if (sk.isValid()) {
					if (sk.isConnectable()) {
						if (sc.finishConnect()) {
							sc.register(selector, SelectionKey.OP_WRITE);
						} else {
							System.exit(1);
						}
					} else if (sk.isWritable()) {
						System.out.println("writing");
						byte[] req = "QUERY TIME ORDER".getBytes("utf-8");
						ByteBuffer bb = ByteBuffer.allocate(req.length);
						bb.put(req);
						bb.flip();
						sc.write(bb);
						sc.register(selector, SelectionKey.OP_READ);
					} else if (sk.isReadable()) {
						System.out.println("reading");
						ByteBuffer bb = ByteBuffer.allocate(1024);
						int readBytes = sc.read(bb);
						bb.flip();
						byte[] resp = new byte[readBytes];
						bb.get(resp);
						String respMsg = new String(resp, "utf-8");
						System.out.println("Now is " + respMsg);
						isStop = true;
						break;
					}
				}
            }

			if (isStop) {
				sc.close();
				break;
			}
        }

    }

}

执行之后,服务端打印结果如下:


客户端的打印结果如下:



总结一下:

SelectableChannel.register(selector, SelectionKey.OP_WRITE);的方法会直接对兴趣进行重新赋值,也就是会覆盖掉之前的兴趣设置。

可写事件的触发有操作系统决定,由于NIO是异步的,只要内核中的网卡的缓冲区可写就会触发可写事件。






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值