转载自:http://blog.youkuaiyun.com/ns_code/article/details/15460405
http://blog.youkuaiyun.com/ns_code/article/details/15378417
http://blog.youkuaiyun.com/ns_code/article/details/15545057
Java NIO (New IO) 是一个可供选择的 Java API (从Java 1.4引入),它可以替代标准的java IO API。它提供了一种与标准IO不同的工作方式。
在标准的IO Socket编程中,是通过字符流和字节流进行操作,它不能前后移动流中的数据;而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,需要时可以在缓冲区中前后移动所保存的数据。
在标准IO的Socket编程中,套接字的某些操作可能会造成阻塞:accept()方法的调用可能会因为等待一个客户端连接而阻塞,read()方法也可能会因为没有数据可读而阻塞,write()方法在数据没有完全写入时也可能会发生阻塞,阻塞发生时,该线程被挂起,什么也干不了。NIO则具有非阻塞的特性,可以通过对channel的阻塞行为的配置,实现非阻塞式的信道。在非阻塞情况下,线程在等待连接,写数据等(标准IO中的阻塞操作)的同时,也可以做其他事情,这便实现了线程的异步操作。
非阻塞式网络IO的特点:1)把整个过程切换成小的任务,通过任务间协作完成;2)由一个专门的线程来处理所有的 IO 事件,并负责分发;3)事件驱动机制:事件到的时候触发,而不是同步的去监视事件;4)线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换。
NIO包(java.nio.*)引入了四个关键的抽象数据类型:
1、 Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
2、 Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
3、 Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。
4、 Selector:它将多元异步I/O操作集中到一个或多个线程中。
考虑一个即时消息服务器,可能有上千个客户端同时连接到服务器,但是在任何时刻只有非常少量的消息需要读取和分发,这就需要一种方法能阻塞等待,直到有一个信道可以进行I/O操作。NIO的Selector选择器就实现了这样的功能,一个 Selector实例可以同时检查一组信道的I/O状态,它就类似一个观察者,只要把需要探知的SocketChannel告诉Selector,接着做别的事情,当有事件(比如,连接打开、数据到达等)发生时,它会通知我们,传回一组SelectionKey,读取这些Key,就会获得刚刚注册过的SocketChannel,然后从这个 Channel中读取数据,接着可以处理这些数据。
Selector内部原理实际是在做一个对所注册的Channel的轮询访问,不断的轮询,一旦轮询到一个Channel有所注册的事情发生,比如数据来了,它就会读取Channel中的数据,并对其进行处理。一般在一个单独的线程中,通过调用select()方法,就能检查多个信道是否准备好进行I/O操作,由于非阻塞I/O的异步特性,在检查的同时,也可以执行其他任务。
基于NIO的TCP通信Demo之服务端:
public class NIOTCPServer implements Runnable {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
private volatile boolean stop = false;
public static void main(String[] args) throws Exception {
new Thread(new NIOTCPServer()).start();
}
public NIOTCPServer() {
try {
selector = Selector.open(); // 创建一个选择器
serverSocketChannel = ServerSocketChannel.open(); // 创建一个信道
serverSocketChannel.configureBlocking(false);// 配置信道为非阻塞模式
// 将该信道绑定到指定端口
serverSocketChannel.socket()
.bind(new InetSocketAddress(9000));
// 将选择器注册到信道
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("set SelectionKey.OP_ACCEPT on " + serverSocketChannel);
System.out.println("Server is start...");
/*
* telnet 127.0.0.1 9000
*/
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (!stop) {
try {
//无论有无读写事件发生,selector每隔1s都唤醒一次
//也可以使用无参的select方法,一直阻塞到有事件发生
int selected = selector.select(3000);
System.out.println("Selected=" + selected
+ ", selector.selectedKeys().size()=" + selector.selectedKeys().size());
// 获取准备好的信道所关联的Key集合的iterator实例
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
handleSelectionKeys(keyIterator);
} catch (Exception e) {
e.printStackTrace();
}
}
//多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (!key.isValid()) {
return;
}
//处理新接入的请求消息
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
}
private void doWrite(SocketChannel sc, String msg) throws IOException {
byte[] bytes = msg.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);//将字节数组复制到缓冲区
writeBuffer.flip();
sc.write(writeBuffer);
}
private void stop() {
this.stop = true;
}
// 服务端信道已经准备好了接收新的客户端连接
public void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssc.accept();
if (socketChannel == null) {
return;
}
socketChannel.configureBlocking(false);
socketChannel.socket().setTcpNoDelay(true);
socketChannel.socket().setKeepAlive(true);
// 将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ
socketChannel.register(key.selector(), SelectionKey.OP_READ);
System.out.println("accept channel:" + socketChannel);
System.out.println("set SelectionKey.OP_READ on " + socketChannel);
}
// 客户端信道已经准备好了从信道中读取数据到缓冲区
public void handleRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
long readBytes = socketChannel.read(readBuffer);
// 如果read()方法返回-1,说明客户端关闭了连接,可以安全地关闭
if (readBytes < 0) {
key.cancel();
socketChannel.close();
}
else if (readBytes > 0) {
readBuffer.flip();//将缓冲区当前的limit设置为position,position设置为0,用于读取
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);//将缓冲区的可读字节数组复制到bytes
String msgBody = new String(bytes, "UTF-8");
System.out.println("Server receive: " + msgBody);
doWrite(socketChannel, "Server have received msg: " + msgBody);
// 如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
System.out.println("set SelectionKey.OP_READ | SelectionKey.OP_WRITE on " + socketChannel);
}
}
// 客户端信道已经准备好了将数据从缓冲区写入信道
public void handleWrite(SelectionKey key) throws IOException {
// 获取与该信道关联的缓冲区,里面有之前读取到的数据
ByteBuffer buf = (ByteBuffer) key.attachment();
// 重置缓冲区,准备将数据写入信道
buf.flip();
SocketChannel socketChannel = (SocketChannel) key.channel();
// 将数据写入到信道中
socketChannel.write(buf);
if (!buf.hasRemaining()) {
// 如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读
key.interestOps(SelectionKey.OP_READ);
System.out.println("set SelectionKey.OP_READ");
}
// 为读入更多的数据腾出空间
buf.compact();
}
private void handleSelectionKeys(Iterator<SelectionKey> keyIter) throws IOException {
SelectionKey key;
while (keyIter.hasNext()) { // 循环取得集合中的每个键值
key = keyIter.next();
keyIter.remove();
try {
handleInput(key);
} catch (IOException e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
}
}
Selector类的select()方法会阻塞等待,直到有信道准备好了IO操作,或等待超时,或另一个线程唤醒了它(调用了该选择器的wakeup()方法)。select()方法返回的是自上次调用它之后,有多少通道变为就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
在用Iterator迭代selectedKeys()方法返回的SelectionKey集合时,每次迭代末尾注意调用remove()方法,以备下次该通道变成就绪时,Selector可以再次将其放入已选择键集中。如果不移除每个处理过的键,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它。所以说,select()方法每次返回的publicSelectedKeys都是一个Set实例。
基于NIO的TCP通信Demo之服务端:
public class NIOTCPClient implements Runnable {
private Selector selector;
private SocketChannel socketChannel;
public static void main(String[] args) throws Exception {
new Thread(new NIOTCPClient())
.start();
// new NIOTCPClient().work("127.0.0.1", 9898, "你好");
}
public NIOTCPClient() throws Exception {
this.selector = Selector.open();
this.socketChannel = SocketChannel.open();
this.socketChannel.configureBlocking(false);
}
@Override
public void run() {
doConnect();
while (true) {
try {
System.out.println("Client select()...");
selector.select(); // 阻塞等待
handleSelectionKeys(selector.selectedKeys());
} catch (IOException e) {
e.printStackTrace();
}
}
// //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
// if (selector != null) {
// try {
// selector.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
}
private void handleSelectionKeys(Set<SelectionKey> selectionKeys) throws IOException {
Iterator<SelectionKey> keyIter = selectionKeys.iterator();
SelectionKey key;
while (keyIter.hasNext()) { // 循环取得集合中的每个键值
key = keyIter.next();
keyIter.remove();
try {
handleInput(key);
} catch (IOException e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
}
private void doConnect() {
try {
//如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel, "zero");
}
else {
//syn已经发送,等待服务器的syn-ack消息,所以在注册SelectionKey.OP_CONNECT事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//是否连接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {// 连接成功,服务端已返回syn-ack消息
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel, "zero007");
}
else {
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String msgBody = new String(bytes, "UTF-8");
System.out.println("Client receive :" + msgBody);
}
else if (readBytes < 0) {
//对端链路关闭
key.cancel();
sc.close();
}
else {
//读到0字节,忽略
}
}
}
}
private void doWrite(SocketChannel sc, String msg) throws IOException {
byte[] bytes = msg.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("Client send :" + msg);
}
}
public void work(String ip, int port, String message) throws Exception {
// 创建一个信道,并设为非阻塞模式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 向服务端发起连接
if (!socketChannel.connect(new InetSocketAddress(ip, port))) {
// 不断地轮询连接状态,直到完成连接
while (!socketChannel.finishConnect()) {
// 在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性
// 这里为了演示该方法的使用,只是一直打印"----"
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("。。。。。。");
}
}
// 为了与后面打印的"."区别开来,这里输出换行符
System.out.print("\n");
// 分别实例化用来读写的缓冲区
byte[] msgBytes = message.getBytes();
ByteBuffer writeBuf = ByteBuffer.wrap(msgBytes);
ByteBuffer readBuf = ByteBuffer.allocate(msgBytes.length);
// 接收到的总的字节数
int totalBytesRcvd = 0;
// 每一次调用read()方法接收到的字节数
int bytesRcvd;
// 循环执行,直到接收到的字节数与发送的字符串的字节数相等
while (totalBytesRcvd < msgBytes.length) {
// 如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道
if (writeBuf.hasRemaining()) {
socketChannel.write(writeBuf);
}
// 如果read()接收到-1,表明服务端关闭,抛出异常
if ((bytesRcvd = socketChannel.read(readBuf)) == -1) {
throw new SocketException("Connection closed prematurely");
}
// 计算接收到的总字节数
totalBytesRcvd += bytesRcvd;
// 在等待通信完成的过程中,程序可以执行其他任务,以体现非阻塞IO的异步特性
// 这里为了演示该方法的使用,同样只是一直打印"."
System.out.print(".");
}
// 打印出接收到的数据
System.out.println("Received: "
+ new String(readBuf.array(), 0, totalBytesRcvd));
// 关闭信道
socketChannel.close();
}
}
再次说明一下,上面的服务端程序,select()方法第一次能选择出来的准备好的信道都是服务端信道,其关联键值的属性都为OP_ACCEPT,其有效操作都为 accept,在执行handleAccept方法时,为取得连接的客户端信道也进行了注册,属性为OP_READ,这样下次轮询调用select()方法时,便会检查到对read操作感兴趣的客户端信道(当然也有可能有关联accept操作兴趣集的信道),从而调用handleRead方法,在该方法中又注册了OP_WRITE属性,那么第三次调用select()方法时,便会检测到对write操作感兴趣的客户端信道(当然也有可能有关联read操作兴趣集的信道)。
服务器端在等待信道准备好的时候,线程没有阻塞,而是可以执行其他任务,客户端在等待连接和等待数据读写完成的时候,线程没有阻塞,也可以执行其他任务。
几个需要注意的地方
1、对于非阻塞 SocketChannel来说,一旦已经调用connect()方法发起连接,底层套接字可能既不是已经连接,也不是没有连接,而是正在连接。由于底层协议的工作机制,套接字可能会在这个状态一直保持下去,这时候就需要循环地调用finishConnect()方法来检查是否完成连接,在等待连接的同时,线程也可以做其他事情,这便实现了线程的异步操作。
2、write()方法的非阻塞调用只会写出其能够发送的数据,而不会阻塞等待所有数据,而后一起发送,因此在调用write()方法将数据写入信道时,一般要用到while循环,如:
while(buf.hasRemaining()) {
channel.write(buf);
}
3、任何对key(信道)所关联的兴趣操作集的改变,都只在下次调用了select()方法后才会生效。
4、selectedKeys()方法返回的键集是可修改的,实际上在两次调用select()方法之间,都必须手动将其清空,否则,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它,换句话说,select()方法只会在已有的所选键集上添加键,它们不会创建新的建集。
5、对于ServerSocketChannel来说,accept是唯一的有效操作,而对于SocketChannel来说,有效操作包括读、写和连接,另外,对于DatagramChannle,只有读写操作是有效的。
1386

被折叠的 条评论
为什么被折叠?



