传统的I/O:
传统的服务器的I/O是阻塞的,即当服务器端开启后,它会监听端口,直到有客户端发来请求,否则一直将是堵塞的,
NIO(非阻塞I/O):
基础概念:
通道:用于建立客户端和服务端的连接,可以将输入/输出流写入到通道,通道将缓冲区的数据块移入或移出到各种I/O源,如文件,socket,数据报。对于网络编程,只有三类通道类:SocketChannel,ServerSocketChannel,DatagramChannel.
缓冲区:在新的I/O中,不再是从输入输出流读取数据,而是从缓冲区中读写数据,流和缓冲区的区别就是流是基于字节的,通道是基于块的,即传送缓冲区中的数据块,一次读写一个缓冲区中的数据;Java中所有的基本数据类型都有特定的Buffer子类,ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer,在网络中,几乎只会使用ByteBuffer.
选择器:用来管理通道,可以将不同的通道注册到一个Selector对象,每个通道分配一个SelectionKey,SelectionKey类相当于通道的指针,可以保存一个对象附件,一般会存储这个通道上的连接的状态。
具体实现:
通道:网络编程的主要类有:
⑴SocketChannel:类可以读写TCP socket,数据必须编码到ByteBuffer对象中来完成读/写,每个SocketChannel都有一个对等端Socket对象。
⑵ServerSocketChannel:接受入站连接,你无法读取,写入或者连接ServerSocketChannel,只接受一个新的入站连接方法,accept()方法。
package com.dong.NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* Channel类的方法
* @author liuD
*/
public class testChannel {
public static void main(String[] args) throws IOException {
//不立即建立来连接,创建一个初始未连接的socket,必须使用connect()方法进行连接;
SocketChannel schannel = SocketChannel.open();
SocketAddress address = new InetSocketAddress("www.xxx.com",80);
schannel.connect(address);
//建立一个非阻塞的连接
SocketChannel schannel2 = SocketChannel.open(address);
//配置非阻塞通道,注意使用非阻塞通道,connect()会立即返回,然后程序就去做别的事情;必须调用finishConnect(); 对于阻塞的通道,finishConnect()方法将立即返回true;
schannel.configureBlocking(false);
//返回boolean值,用来判断连接是否建立;
schannel.isConnected();
ByteBuffer buffer = ByteBuffer.allocate(100);
ByteBuffer [] buffers = null;
//读取通道中缓冲区的数据,首先将数据读取的数据放入到缓冲区,直到缓冲区放满,然后返回放入的字节数,如果到达流的末尾,则下一次调用read(),返回-1;
schannel.read(buffer);
//从一个源填充多个缓冲区,read()接受一个ByteBuffer对象数组作为参数,按顺序填充数组中的各个ByteBuffer;
schannel.read(buffers);
//同上,从第0个缓冲区开始,填充100个缓冲区;
schannel.read(buffers, 0, 100);
//像通道中缓冲区写入数据,填充一个ByteBuffer,然后穿个某个写入方法,其原理和输入相同
schannel.write(buffer);
schannel.write(buffers);
schannel.write(buffers, 0, 100);
//关闭连接
schannel.close();
////ServerScoketChannel
//只有一个目的,就是接受入站连接,open方法并不是打开一个新的socket,而是只创建这个对象;
ServerSocketChannel server = ServerSocketChannel.open();
SocketAddress saddress = new InetSocketAddress(80);
//绑定监听端口
server.bind(saddress);
//监听端口入站连接,阻塞情况下,accept()方法,等待连接,并返回连接到远程客户端的一个SocketChannel对象,阻塞模式是默认的;
//在非阻塞模式下,如果没有入站连接,accept()方法将返回null,
server.accept();
}
}
缓冲区:缓冲区是NIO连接的基本单元;
package com.dong.NIO;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
/**
* ByteBuffer类的方法
* @author liuD
*/
public class testBuffer {
public static void main(String[] args) throws UnsupportedEncodingException {
//创建一个指定容量的缓冲区,游标位于缓冲区的开始位置,
ByteBuffer buffer = ByteBuffer.allocate(100);
//对于已经有的数据数组,一般要用缓冲区进行包***yte[] data = "xxx".getBytes("UTF-8");
ByteBuffer buffer1 = ByteBuffer.wrap(data);
//位置:缓冲区中用于读取或写入的下一个位置,这个位置从0开始,最大值等于缓冲区;
buffer1.position();
//容量:缓冲区中用来保存数据的大小,一旦在创建缓冲区时设置,此后就不能改变;
buffer1.capacity();
//限度:缓冲区可访问数据的末尾位置,即读取的位置,只能在限度之内,即使容量大于限度,,也不可以访问限度以外的;
buffer1.limit();
//标记:客户端中指定的索引,mark()可以将标记设置为当前位置;reset()可以将当前位置设置为所标记的位置;
buffer1.mark();
//注意读取缓冲区实际上不会以任何方式改变缓冲区中的数据,只可能向前或者向后设置位置,从而可以从特定位置开始读取;
//将位置设置为0,限度设置为容量,缓冲区的数据没有删除,只是改动位置,
buffer1.clear();
//将位置设置为0,但不改变限度
buffer1.rewind();
//将限度设置为当前位置
buffer1.flip();
//缓冲区中当前位置与限度之间的元素数
buffer1.remaining();
//判断当前位置和限度之间是否还有元素,有的话返回true
buffer1.hasRemaining();
//填充缓冲区:至多填充到其容量的大小;超出抛异常; put(index) 指定位置填充
buffer1.put((byte) 12);
//获得数据,get()会将位置前移一个元素,到达限度时,返回false; get(index) 获取指定位置
buffer1.get();
//复制缓冲区,返回值并不是克隆,复制的缓冲区共享相同的数据,修改一个缓冲区数据会反应到另一个缓冲区中;
buffer1.duplicate();
}
}
选择器:Selector,用来管理通道
package com.dong.NIO;
import java.io.IOException;
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.Set;
/**
*
* Selector选择器
* @author liuD
*/
public class testSelector {
public static void main(String[] args) throws IOException {
//创建一个选择器,用来管理连接的通道
Selector selector = Selector.open();
//向选择器中增加通道,selector类中没有增加通道的方法,可以通过register()方法通道类中声明,注意,FileChannel不可选择,
ServerSocketChannel server = ServerSocketChannel.open();
//将选择器传递给通道的注册方法,就可以向选择器注册这个通道;
server.register(selector, SelectionKey.OP_ACCEPT);
//SelectionKey类中的一个命名常量,标识通道注册的操作;
//SelectionKey.OP_ACCEPT;SelectionKey.OP_CONNECT;SelectionKey.OP_READ;SelectionKey.OP_WRITE;
//查询选择器,找出哪些通道已经准备好可以进行处理,
//选择就绪的通道:
//完成非阻塞选择,如果当前没有准备好要处理的连接,则立即返回
selector.selectNow();
//等待,直到至少有一个注册的通道准备好可以进行处理才返回
selector.select();
//获取准备就绪的通道,返回一个集合,依次处理各个SelectionKey,也可以从迭代器中删除键;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//不在使用selector选择器时,应该关闭它
selector.close();
//SelectionKey类,相当于通道的指针,可以保存一个对象附件,一般会存储通道的连接状态
for(SelectionKey key : selectionKeys) {
//测试键的操作,即SelectionKey
key.isAcceptable();
//使用Channel()方法可以获取准备就绪的通道;
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//如果SelectionKey存储了一个对象,则可以使用attachment()方法来获取该对象
ByteBuffer buffer = (ByteBuffer) key.attachment();
}
}
}
完整的NIO客户端和服务端例子:
NIO客户端:
package com.dong.NIO;
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.SocketChannel;
import java.util.Iterator;
/**
* NIO客户端
* @author liuD
*
*/
public class NIOClient {
//通道管理器
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些工作
* @throws IOException
*/
public void initClient(String ip,int port) throws IOException {
//获得一个Socket通道
SocketChannel channel = SocketChannel.open();
//设置为非阻塞
channel.configureBlocking(false);
//h获得一个通道管理器
this.selector =selector.open();
//客户端连接服务器,其方法执行并没有实现连接,需要listen()方法中调用Channel.finshConection();才算完成连接
channel.connect(new InetSocketAddress(ip, port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
*
* 采用轮询的方式监听selector上是否有需要处理的对象,如果有,则进行处理
* @throws IOException
*/
public void connect() throws IOException {
//轮询访问Selector
while(true) {
//选择一组可以进行I/O的操作的事件,放在selector中,客户端的该方法不会阻塞
selector.select();
//获得selector中选中的项的迭代器
Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()) {
SelectionKey key = ite.next();
//删除已经选择key,防止重复
ite.remove();
//连接事件的发生
if(key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
//如果正在连接,则完成连接
if(channel.isConnectionPending()) {
channel.finishConnect();
}
//设置成非阻塞
channel.configureBlocking(false);
//给服务端发送信息;
channel.write(ByteBuffer.wrap(new String("1111").getBytes("utf-8")));
//和服务器端连接成功后,为了可以接受到服务端的信息,需要给通道设置读的权限
channel.register(this.selector, SelectionKey.OP_READ);
}else if(key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
* @throws IOException
*/
public void read(SelectionKey key ) throws IOException {
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(512);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息:" + msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes("utf-8"));
channel.write(outBuffer);// 将消息回送给客户端
}
/**
*
* 启动测试
* @throws IOException
*/
public static void main(String args[]) throws IOException {
NIOClient client =new NIOClient();
client.initClient("localhost", 8000);
client.connect();
}
}
NIO服务端:
package com.dong.NIO;
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.SocketChannel;
import java.util.Iterator;
/**
* NIO客户端
* @author liuD
*
*/
public class NIOClient {
//通道管理器
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些工作
* @throws IOException
*/
public void initClient(String ip,int port) throws IOException {
//获得一个Socket通道
SocketChannel channel = SocketChannel.open();
//设置为非阻塞
channel.configureBlocking(false);
//h获得一个通道管理器
this.selector =selector.open();
//客户端连接服务器,其方法执行并没有实现连接,需要listen()方法中调用Channel.finshConection();才算完成连接
channel.connect(new InetSocketAddress(ip, port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
*
* 采用轮询的方式监听selector上是否有需要处理的对象,如果有,则进行处理
* @throws IOException
*/
public void connect() throws IOException {
//轮询访问Selector
while(true) {
//选择一组可以进行I/O的操作的事件,放在selector中,客户端的该方法不会阻塞
selector.select();
//获得selector中选中的项的迭代器
Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()) {
SelectionKey key = ite.next();
//删除已经选择key,防止重复
ite.remove();
//连接事件的发生
if(key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
//如果正在连接,则完成连接
if(channel.isConnectionPending()) {
channel.finishConnect();
}
//设置成非阻塞
channel.configureBlocking(false);
//给服务端发送信息;
channel.write(ByteBuffer.wrap(new String("1111").getBytes("utf-8")));
//和服务器端连接成功后,为了可以接受到服务端的信息,需要给通道设置读的权限
channel.register(this.selector, SelectionKey.OP_READ);
}else if(key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
* @throws IOException
*/
public void read(SelectionKey key ) throws IOException {
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(512);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息:" + msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes("utf-8"));
channel.write(outBuffer);// 将消息回送给客户端
}
/**
*
* 启动测试
* @throws IOException
*/
public static void main(String args[]) throws IOException {
NIOClient client =new NIOClient();
client.initClient("localhost", 8000);
client.connect();
}
}
内容参考:《Java网络编程》 作者 Elliotte Rusty Harold 译 李帅, 荆涛,,由衷感谢书籍作者提供的内容