java nio网络编程_Java NIO之网络编程

3. 还有挂起的例外情况吗

1 2 9 - 1 4 0 对于例外情况,需检查标志 s o _ o o b m a r k 和 S S _ R E C V A T M A R K 。 直 到 进 程 读 完 数 据流中的同步标记后,例外情况才可能存在。

原来,select调用的底层实现里面,把很多个事件都只是归并进了可读和可写这两种状态。比如在我之前看来,server端的socket已经将连接排队,就代表可连接状态,可是在select看来,这就是可读状态。

有了前面的一些基础,现在上一段Java NIO的代码

//创建一个selector

Selector selector =Selector.open();//创建一个ServerSocketChannel

ServerSocketChannel servChannel =ServerSocketChannel.open();

servChannel.configureBlocking(false);//绑定端口号

servChannel.socket().bind(new InetSocketAddress(8080), 1024);//注册感兴趣事件

servChannel.register(selector, SelectionKey.OP_ACCEPT);//select系统调用

selector.select(1000);

Set selectedKeys =selector.selectedKeys();

Iterator it =selectedKeys.iterator();

SelectionKey key= null;while(it.hasNext()) {

key=it.next();

it.remove();if(key.isValid()) {//处理新接入的请求消息

if(key.isAcceptable()) {

ServerSocketChannel ssc=(ServerSocketChannel) key.channel();//接收客户端的连接,并创建一个SocketChannel

SocketChannel sc =ssc.accept();

sc.configureBlocking(false);//将SocketChannel和感兴趣事件注册到selector

sc.register(selector, SelectionKey.OP_READ);

}if(key.isReadable()) {//读数据的处理

}

}

}

分析这段代码之前,先搞清楚selector、SelectionKey、pollArray等几个数据结构以及相互持有关系。

45fba9b1db03f1db9dbfb799ca87a556.png

pollArray干的是数组的活,但是并不是一个直接的数组。

selector诞生的时候,随之关联了一块内存(pollArray),然后用unsafe类来小心翼翼的按字节顺序写入数据,最终实现了数组结构的功能。这种看似怪异的实现方式,应该是处于效率的考虑吧。

selector并没有直接持有pollArray,而是持有一个pollArray的封装类PollArrayWrapper的引用。

//The poll fd array

PollArrayWrapper pollWrapper; //在selector的父类里面//The set of keys with data ready for an operation

protected Set selectedKeys;

selectedKeys是一个集合,代表poll系统调用后返回的所有就绪事件,里面存放的数据结构是SelectionKey。

final SelChImpl channel; //package-private

public finalSelectorImpl selector;//Index for a pollfd array in Selector that this key is registered with

private int index; //pollArray里面的索引值,保存在这里是方便实现数组操作

private volatile int interestOps; //注册的感兴趣事件掩码

private int readyOps; //就绪事件掩码

SelectionKey不但持有channel,还持有selector;interestOps、readyOps与pollArray里面的eventOps、reventOps对应。

Java定义了一些针对文件描述符的事件,其实也是对底层操作系统poll定义的事件的一个映射。事件用掩码来表示,非常方便进行位操作。如下:

public static final short POLLIN = 0x0001; //文件描述符可读

public static final short POLLOUT = 0x0004; //文件描述符可写

public static final short POLLERR = 0x0008; //文件描述符出现错误

public static final short POLLHUP = 0x0010; //文件描述符挂断

public static final short POLLNVAL = 0x0020; //文件描述符不对

public static final short POLLREMOVE = 0x0800; //文件描述符移除

@Nativestatic final short POLLCONN = 0x0002; //可连接

我记得POLLCONN在之前的版本中直接被赋值成POLLOUT,这里改成了0x0002,这里我是真不知道为什么。希望高手来回复一下。

最终这些事件都会传递到内核的poll系统调用,去监控所有传递给poll的文件描述符。

回到之前的NIO代码

1、先看看 servChannel.register(selector, SelectionKey.OP_ACCEPT) 是如何实现注册的

一路调用后,会到一个关键方法

protected finalSelectionKey register(AbstractSelectableChannel ch,intops,

Object attachment)

{if (!(ch instanceofSelChImpl))throw newIllegalSelectorException();

SelectionKeyImpl k= new SelectionKeyImpl((SelChImpl)ch, this);

k.attach(attachment);synchronized(publicKeys) {

implRegister(k);//这一步把channel的文件描述符fd添加到pollArray(见上图)

}

k.interestOps(ops);//这一步把感兴趣事件eventOps添加到pollArray(见上图)

returnk;

}

具体的逻辑肯定比注释要复杂。接下来看看pollArray的内存操作,以添加文件描述符fd为例

void putDescriptor(int i, intfd) {int offset = SIZE_POLLFD * i +FD_OFFSET;

pollArray.putInt(offset, fd);

}final void putInt(int offset, intvalue) {

unsafe.putInt(offset+address, value);

}

最终还是用unsafe直接修改内存

2、再看看最核心的selector.select(1000)。次方法最终调用doSelect方法,而doSelect方法的实现有多种,我们就以poll版本进行探秘

//做了很多删减

protected int doSelect(longtimeout)throwsIOException

{//执行最核心的poll系统调用

pollWrapper.poll(totalChannels, 0, timeout);//将到来的就绪事件更新保存

int numKeysUpdated =updateSelectedKeys();returnnumKeysUpdated;

}

poll系统调用会把用户空间的线程挂起,也就是阻塞调用,timeout指定多长时间后必须返回。

updateSelectedKeys方法根据poll返回的channel就绪事件,去更新pollArray对应fd的reventOps(见上图),以及selector的selectedKeys。

/*** Copy the information in the pollfd structs into the opss

* of the corresponding Channels. Add the ready keys to the

* ready queue.*/

protected intupdateSelectedKeys() {int numKeysUpdated = 0;//Skip zeroth entry; it is for interrupts only

for (int i=channelOffset; i

int rOps =pollWrapper.getReventOps(i);if (rOps != 0) {

SelectionKeyImpl sk=channelArray[i];

pollWrapper.putReventOps(i,0); //重置为0,即为未就绪

if(selectedKeys.contains(sk)) {//把事件的掩码翻译成SelectionKey中定义的操作(OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT)

if(sk.channel.translateAndSetReadyOps(rOps, sk)) {

numKeysUpdated++;

}

}else{

sk.channel.translateAndSetReadyOps(rOps, sk);if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) {//更新selectedKeys

selectedKeys.add(sk);

numKeysUpdated++;

}

}

}

}returnnumKeysUpdated;

}

把就绪事件的掩码进行翻译,感觉就像是Java做的一层适配,让我们用户不用去关注事件掩码等细节

看一下实现这一逻辑的一段代码,在ServerSocketChannel类里面:

/*** Translates native poll revent set into a ready operation set*/

public boolean translateReadyOps(int ops, intinitialOps,

SelectionKeyImpl sk) {int intOps = sk.nioInterestOps(); //Do this just once, it synchronizes

int oldOps =sk.nioReadyOps();int newOps =initialOps;if ((ops & PollArrayWrapper.POLLNVAL) != 0) {//This should only happen if this channel is pre-closed while a//selection operation is in progress//## Throw an error if this channel has not been pre-closed

return false;

}if ((ops &(PollArrayWrapper.POLLERR| PollArrayWrapper.POLLHUP)) != 0) {

newOps=intOps;

sk.nioReadyOps(newOps);return (newOps & ~oldOps) != 0;

}//这里将可连接当作可读来看待的

if (((ops & PollArrayWrapper.POLLIN) != 0) &&((intOps& SelectionKey.OP_ACCEPT) != 0))

newOps|=SelectionKey.OP_ACCEPT;

sk.nioReadyOps(newOps);return (newOps & ~oldOps) != 0;

}

通过上面的分析,大概有了一个清晰的思路:

Java NIO主要是基于底层操作系统提供的的IO多路复用功能,比如Linux下的select/poll、epoll等系统调用。Java层面为每个selector开辟了一块内存,用来保存用户注册的所有channel、所有感兴趣事件,并最终当作参数传递给底层的系统调用,最后将内核返回的结果封装成selectedKeys等数据结构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值