java网络编程(三)--非阻塞IO(通道、缓冲区、选择器)

传统的基于流的多线程IO,相当于一个连接一个线程,生成多个线程以及在线程之间切换的开销是不容忽视的;例如,每个线程需要大约1MB的RAM。

如果一个线程可以负责多个连接,可以选取一个准备好接收数据的连接,尽快填充这个连接所能管理的尽可能多的数据,然后转向下一个准备好的连接,这样速度就会更快.

作为大吞吐量服务器的所有现代操作系统几乎都支持这种非阻塞IO,java.nio包就是为 服务器设计的。


SocketAddress address = new InetSocketAddress("rama.poly.edu", 19);

SocketChannel client = SocketChannel.open(address);   //TCP客户端的通道

ByteBuffer buffer = ByteBuffer.allocate(74);  //创建一个容量为74字节的缓冲区

WritableByteChannel out = Channels.newChannel(System.out);   //将System.out封装在一个通道中

while(client.read(buffer) != -1) { //通道从Socket读取数据填充到缓冲区

buffer.flip(); //回绕到数据的开头

out.write(buffer); //将缓存区中的数据输出到控制台

buffer.clear(); //清空缓冲区,为了重用缓冲区

}


以上是客户端在阻塞模式下利用通道和缓冲区;可以设置在非阻塞模式下运行,此时,即使没有任何可用的数据,read()也会 立即返回。


client.configureBlocking(false);    //设置为非阻塞模式

while(true) {

int n = client.read(buffer);

if(n > 0) { //非阻塞模式下,read()读不到任何数据时,会返回0

buffer.flip();

out.write(buffer);

buffer.clear();

}

}


TCP客户端使用通道和缓存区是可以的,不过实际上通道和缓冲区主要用于高效处理很多并发连接的服务器系统。这就需要使用选择器,允许服务器查找所有准备好读或写的连接.


byte[] rotation = new byte[95*2];
for(byte i = ' '; i <= '~'; i++) {
rotation[i - ' '] = i;
rotation[i + 95 - ' '] = i;
}

ServerSocketChannel serverChannel;  //服务器端通道
Selector selector;//选择器

try {
serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket(); //获取对等端ServerSocket
InetSocketAddress address = new InetSocketAddress(19);
ss.bind(address);//将通道绑定到服务器的19端口
serverChannel.configureBlocking(false);  //服务端非阻塞通道,当没有入站连接时,accept()立即返回null
selector = Selector.open();  //创建选择器
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //将serverChannel注册到选择器并指定关注的操作是accept
//即判断服务器socket是否准备好接受一个新连接
}catch(IOException ex) {
ex.printStackTrace();
return ;
}

while(true) {
try {
selector.select();//检查是否有可操作的通道,如果没有通道就绪,选择器就会等待
//这样,一个线程就可以同时处理多个连接
}catch(IOException ex) {
ex.printStackTrace();
break ;
}

Set readKeys = selector.selectedKeys();
Iterator iterator = readKeys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();

try {
if(key.isAcceptable()) { //就绪的是服务器通道,产生一个socket通道,并注册到选择器
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false); //客户端通道处于非阻塞模式
SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
ByteBuffer buffer = ByteBuffer.allocate(74);
buffer.put(rotation, 0, 72);
buffer.put((byte)'\r');
buffer.put((byte)'\n');
buffer.flip();//回转,准备排空
key2.attach(buffer);
//将通道要写入网络的缓冲区存储在SelectionKey的附件中
}else if(key.isWritable()) { //就绪的是客户端通道,可以写到网络了
SocketChannel client = (SocketChannel)key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();
if(!buffer.hasRemaining()) {
buffer.rewind();
int first = buffer.get();
buffer.rewind();
int position = first - ' ' + 1;
buffer.put(rotation, position, 72);
buffer.put((byte)'\r');
buffer.put((byte)'\n');
buffer.flip();
}
client.write(buffer);
}
}catch(IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
}
}



在NIO中,不再向输出流写入数据和从输入流读取数据,而是要从缓冲区中读写数据。

从编程角度看,流和通道之间的关键区别在于流是基于字节的,而通道是基于块的。流设计为按顺序一个字节接一个字节地传送数据;性能考虑,也可以传送字节数组。不过,基本的概念都是一次传送一个字节的数据;通道会传送缓冲区中的数据块。

第二个关键区别是,通道和缓冲区支持对同一个对象的读写。


缓冲区有多种类型,网络程序几乎只使用ByteBuffer,缓冲区的属性有位置position、容量capacity、限度limit、标记mark.

与读取流不同,读取缓冲区实际上不会以任何方式改变缓冲区中的数据。只可能向前或向后设置位置,从而可以从缓冲区中某个特定位置开始读取。


空的缓冲区一般由分配allocate方法创建,通常用于输入。

预填充数据的缓冲区由包装wrap方法创建,一般用于输出。

ByteBuffer buffer = ByteBuffer.allocate(100);


byte[] data = "some data".getBytes("UTF-8");

ByteBuffer buffer = ByteBuffer.wrap(data);



ServerSocketChannel类只有一个目的,接受入站连接。你无法读取、写入。




你可能感兴趣的:(java)