放进NIO体系进行网络编程的工作流程:
Selector的创建
通过调用Selector.open()方法创建一个Selector,如下:
Selector selector = Selector.open();
向Selector注册通道
通过Channel.register()方法来实现,
注意:Channel和Selector一起使用时,Channel必须处于非阻塞模式下。
channel.configureBlocking(false); //设置通道为非阻塞模式
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
register()方法的第二个参数:是一个“兴趣(interest)集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。
可以监听四种不同类型的事件:
- Connect 链接就绪,某个channel成功连接到另一个服务器称为“连接就绪”。
- Accept 接收就绪,一个server socket channel准备好接收新进入的连接称为“接收就绪”。
- Read 读就绪,一个有数据可读的通道可以说是“读就绪”。
- Write 写就绪,等待写数据的通道可以说是“写就绪”。
这四种事件用SelectionKey的四个常量来表示:
1.SelectionKey.OP_CONNECT
2.SelectionKey.OP_ACCEPT
3.SelectionKey.OP_READ
4.SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey说明
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。
这个对象包含了一些有用的属性:
1.interest集合
2.ready集合
3.Channel
4.Selector
5.附加的对象(可选)
interest集合
interest集合是你所选择的感兴趣的事件集合。
可以通过SelectionKey读写interest集合,像这样:
SelectionKey selectionKey=channel.register(selector, SelectionKey.OP_xxxx);
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
可以看到,用“和”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。
ready集合
ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。
可以这样访问ready集合:
int readySet = selectionKey.readyOps();
可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
另外
从SelectionKey访问Channel和Selector很简单。如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
附加的对象
可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。
例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
选择器的select()
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。
这些方法返回你所感兴趣的事件(如连接、接受、读或写)同时这些事件已经准备就绪的那些通道。
换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
下面是select()方法:
1.select()阻塞到至少有一个通道在你注册的事件上就绪了。
2.select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数),一般用这个,不能无限阻塞。
3.selectNow()不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。
如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。
select()方法有返回值,返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。
如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,
如果另一个通道就绪了,它会再次返回1。
如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,
但在每次select()方法调用之间,只有一个通道就绪了。
选择器的selectedKeys()
一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法获取到:
Set selectedKeys = selector.selectedKeys();
当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey对象。
这个对象代表了注册到该Selector的通道。
可以通过SelectionKey的selectedKeySet()方法访问这些对象。
可以遍历这个已选择的键集合来访问就绪的通道。如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// 一个连接被ServerSocketChannel接受
} else if (key.isConnectable()) {
// 与远程服务器建立了连接
} else if (key.isReadable()) {
// 一个channel做好了读准备
} else if (key.isWritable()) {
// 一个channel做好了写准备
}
keyIterator.remove();
}
这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。
注意每次迭代末尾的keyIterator.remove()一定要调用
,Selector不会自己从已选择键集中移除SelectionKey实例。
必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。若不移除,下次还是读取原来的数据,这是要命的。
SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。
选择器的wakeUp()
某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。
只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。
阻塞在select()方法上的线程会立马返回。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,上一个调用select()方法的阻塞线程会立即醒来(wake up)。
选择器的close()
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。
通道本身并不会关闭。
客户端与服务端简单交互实例
下面的程序涉及到一些网络编程的知识:
- 服务器必须先建立ServerSocket或者ServerSocketChannel 来等待客户端的连接
- 客户端必须建立相对应的Socket或者SocketChannel来与服务器建立连接
- 服务器接受到客户端的连接受,再生成一个Socket或者SocketChannel与此客户端通信
服务器:
/*
* nio网络编程实例,这是服务端
* 1.建立ServerSocketChannel通道,设置非阻塞 ,并绑定一个地址,客户链接到这个地址,等待客户端的链接
* 2.获取选择器的实例,注册通道到里面去,设置感兴趣的事件,ServerSocketChannel这种通道只有一个事件--链接事件
* 3.准备缓冲区:两个,一个读,一个写
* 4.实现与客户端交互,首先一个死循环,不停的获取各种客户端链接请求通道,轮询作用似的
* a.选择器的select(1000)方法,会获取各通道对象个数(通道感兴趣事件的)1000是表示阻塞一秒,如果一秒之内没结果,重新轮询
b.有通道就绪,则拿到通道对象键值集合Set,遍历集合,找到接入链接的通道,让其接入,返回一个能读写的通道,不是上面的接入通道
c.读写通道也要注册到选择器里面,先读事件,同一个通道的多次注册,如是不同的事件改变,只是注删了一个通道
*/
public class ServerDemo {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1",8000));
serverSocketChannel.configureBlocking(false);
Selector selector=Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //接入事件就绪
ByteBuffer readBuffer= ByteBuffer.allocate(1024);
ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
writeBuffer.put("this is server".getBytes()); //写缓冲区中写入这个字符串
writeBuffer.flip(); //翻转非直接缓冲区,非直接缓冲区,postion指针不能自动指向开缓冲区开头处
while(true) {
int nReady=selector.select(1000); //捕获所有通道
if(nReady==0) continue; //一个通道都没有,一秒后继续查询
Set<SelectionKey> keys =selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key=iterator.next();
iterator.remove(); //手动移除,让下次轮询通道是全新的,而不是这次的
if(key.isAcceptable()) { //如果是准备链接的通道,就让客户端链接, 当然了,这个程序简单只有一个通道
SocketChannel channel=serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ); //不用担心,下次读客户发送的信息,因为有死循在那不的轮询
}else if(key.isReadable()) { //如果通道对象key是有可读事件,就读到缓冲区中
SocketChannel channel = (SocketChannel) key.channel();
readBuffer.clear();
channel.read(readBuffer);
readBuffer.flip();
/*while(readBuffer.hasRemaining()) { //判断缓冲区还没有数据,有的话,从缓冲区读数据,打印到控制台
System.out.print("读到的数据是:"+(char)readBuffer.get()); //读得是字节码,中文会乱码,一次读一个字节
}*/
System.out.println("服务器端收到的数据,直接打印:"+ new String(readBuffer.array()));
key.interestOps(SelectionKey.OP_WRITE);
}else if(key.isWritable()){
SocketChannel channel =(SocketChannel)key.channel();
writeBuffer.rewind();
channel.write(writeBuffer);
key.interestOps(SelectionKey.OP_READ); //以便下次再读
}
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
客户端
/*
* nio网络编程实例,这是客户端
* 不用实例化,链接通道(类似洒店的迎宾)
* 1.通道
* 2.缓冲区,两个,一个读,一个写
* 3.先向服务器写,再读数据
*
*/
public class ClientDemo {
public static void main(String[] args) {
try {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("127.0.0.1",8000)); //链接到服务器
ByteBuffer readBuffer= ByteBuffer.allocate(1024);
ByteBuffer writeBuffer=ByteBuffer.allocate(1024);
writeBuffer.put("this is client".getBytes()); //写缓冲区中写入这个字符串
writeBuffer.flip();
while(true) {
writeBuffer.rewind(); //重置缓冲区,写入
channel.write(writeBuffer);
readBuffer.clear();
channel.read(readBuffer);
readBuffer.flip();
System.out.println("客户端收到的数据:"+new String(readBuffer.array()));
}
}catch (IOException e) {
e.printStackTrace();
}
}
}