NIO编程中,需要知道的的重要概念 通道(Channel,封装的Socket)、缓冲区(ByteBuffer等类,用于服务端和客户端数据传递)、选择器(Selector,用于处理Channel)。NIO处理时异步非阻塞的IO,NIO(只有Selector的一个线程,不断轮询处理Channel)效率比BIO高很多(BIO是一个请求一个线程)。NIO中常用的框架有Netty和Mina。
基本概念
NIO网络编程中,重要的类有,
ByteBuffer、(缓冲区)
Channel(ServerSocketChannel/SocketChannel) 通道,双向的
Selector(多路复用器,选择器)
1、缓冲区: 在操作系统中缓冲区是为了解决CPU的计算速度和外设输入输出速度不匹配的问题,因为外设太慢了,如果没有缓冲区,那么CPU在外设输入的时候就要一直等着,就会造成CPU处理效率的低下,引入了缓冲之后,外设直接把数据放到缓冲中,当数据传输完成之后,给CPU一个中断信号,通知CPU:“我的数据传完了,你自己从缓冲里面去取吧”。如果是输出也是一样的道理。
2、通道: 那么通道用来做什么呢?其实从他的名字就可以看出,它就是一条通道,您想传递出去的数据被放置在缓冲区中,然后缓冲区中怎么从哪里传输出去呢?或者外设怎么把数据传输到缓冲中呢?这里就要用到通道。它可以进一步的减少CPU的干预,同时更有效率的提高整个系统的资源利用率,例如当CPU要完成一组相关的读操作时,只需要向I/O通道发送一条指令,以给出其要执行的通道程序的首地址和要访问的设备,通道执行通道程序便可以完成CPU指定的I/O任务。
3、选择器: 另外一项创新是选择器,当我们使用通道的时候也许通道没有准备好,或者有了新的请求过来,或者线程遇到了阻塞,而选择器恰恰可以帮助CPU了解到这些信息,但前提是将这个通道注册到了这个选择器。
NIO编程中,采用的是Channel对Socket的封装,解决了TCP三次握手的问题,每一个客户端(SocketChannel)访问服务器,都会注册到Selector中,并以SelectionKey的集合形式存储起来,在轮询中,对SelectionKey的状态进行检查和业务操作。
SelectionKey.OP_CONNECT //连接
SelectionKey.OP_ACCEPT //接受请求
SelectionKey.OP_READ //读操作
SelectionKey.OP_WRITE //写操作
案例
这个案例中,我做了两个Client,不同的是一个Client只有些数据的作用,另外一个具有读写等功能
服务器端
服务器端中,需要注意的是selectionKey.isAcceptable()
判断后,需要获取到服务器端的ServerSocketChannel ,然后再获取到客户端发过来的SocketChannel ,并注册。
//获取到SelectionKey 里面的Channel
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//线程阻塞,获取客户端的SocketChannel,注册到服务器端
// 接受到此通道套接字的连接。
SocketChannel sc = channel.accept();
//设定非阻塞
sc.configureBlocking(false);
//注册准备读的事件
sc.register(this.selector,SelectionKey.OP_READ);
完整代码
package yellowcong.socket.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import org.junit.experimental.theories.Theories;
/**
* 创建日期:2017年10月7日 <br/>
* 创建用户:yellowcong <br/>
* 功能描述:
*/
public class Server implements Runnable{
//多路复用器
private Selector selector ;
private ByteBuffer byteBuff = ByteBuffer.allocate(1024);;
//port 端口号
public Server(int port) {
try {
this.selector = Selector.open();
ServerSocketChannel channel = ServerSocketChannel.open();
//设定是否是线程阻塞的
channel.configureBlocking(false);
//绑定端口
channel.bind(new InetSocketAddress(port));
//注册channel 到Selecor(多路复用器)中
channel.register(this.selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void run() {
System.out.println("服务器启动");
//在服务器端,轮询操作
while(true){
try {
//开启多路复用器监听操作
this.selector.select();
//获取Selector 里面注册的 SelectionKey(每注册一个Channel ,就会有一个SelectionKey,SelectionKey里面有Channel对象 )
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey selectionKey = keys.next();
//直接从容器中移除就可以了,不然重复处理同一条Channel
keys.remove();
//可以连接的情况 ,刚开始连接时比较特别的,需要获取到客户端管道,然后再注册到服务器端Selector
if(selectionKey.isAcceptable()){
//获取到SelectionKey 里面的Channel
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//线程阻塞,获取客户端的SocketChannel,注册到服务器端
// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
SocketChannel sc = channel.accept();
//设定非阻塞
sc.configureBlocking(false);
//注册准备读的事件
sc.register(this.selector,SelectionKey.OP_READ);
}
//可读的情况
if(selectionKey.isReadable()){
//获取到客户端的SocketChannel
SocketChannel sc =(SocketChannel) selectionKey.channel();
//晴空数据
this.byteBuff.clear();
int cnt = sc.read(this.byteBuff);
//数据没有的情况
if(cnt == -1){
selectionKey.channel().close();
selectionKey.cancel();
return;
}
//复位 ,将position置为1
this.byteBuff.flip();
//建立一个数组,大小 为传递过来的 数据长度
byte [] data = new byte[this.byteBuff.remaining()];
//将buffer数据读取到 数组中
this.byteBuff.get(data);
//打印传送过来的数据内容
System.out.println("服务器端:\t"+new String(data));
//进行下一步,注册写数据
sc.register(this.selector, SelectionKey.OP_WRITE);
}else if(selectionKey.isWritable()){//可写的情况
this.write(selectionKey);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 写数据到客户端
* 创建日期:2017年10月7日<br/>
* 创建用户:yellowcong<br/>
* 功能描述:
* @param selectionKey
*/
public void write(SelectionKey selectionKey){
try {
//获取客户端Channel
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
String sendText = "hello client"; //写道客户端的数据
this.byteBuff.clear(); //在使用ByteBuff前,清空里面数据
this.byteBuff.put(sendText.getBytes());
this.byteBuff.flip(); //写完数据后调用此方法,设定Bytebuff的postion为0
clientChannel.write(byteBuff);
//设置Channel的下一步操作时 读取
clientChannel.register(this.selector, SelectionKey.OP_READ);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server(8080);
new Thread(server).start();
}
}
客户端1
这个客户端中,我们可以发现,没有用到Selector(选择器),直接就可以发送请求到服务器端
package yellowcong.socket.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CountDownLatch;
/**
* 创建日期:2017年10月7日 <br/>
* 创建用户:yellowcong <br/>
* 功能描述:
*/
public class Client {
private static final String ADDR = "127.0.0.1";
private static final Integer PORT = 8080;
public static void main(String [] args){
try {
//打开Socker Channel
SocketChannel channel = SocketChannel.open();
//打开链接
channel.connect(new InetSocketAddress(ADDR, PORT));
ByteBuffer buff =ByteBuffer.allocate(1024);
while(true){
//不断重控制台读取数据
byte [] data = new byte[1024];
System.in.read(data);
// buff.put("发送文本".getBytes());
//写数据到buff里面
buff.put(data);
//复位
buff.flip();
//写道服务器
channel.write(buff);
//清空
buff.clear();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端2
客户端2里面有发送和接收的请求和操作,这个客户端和服务端,在获取连接的地方有些区别if(selectionKey.isConnectable()){}
判断的是是否获取到了连接,而服务器段是判断if(selectionKey.isAcceptable()){}
package yellowcong.socket.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;
/**
* 创建日期:2017年10月7日 <br/>
* 创建用户:yellowcong <br/>
* 功能描述:
*/
public class Client2 implements Runnable{
private static final String ADDR = "127.0.0.1";
private static final Integer PORT = 8080;
private Selector selector;
private ByteBuffer byteBuff =ByteBuffer.allocate(1024);
public Client2() {
try {
this.selector = Selector.open();
//打开Socker Channel
SocketChannel channel = SocketChannel.open();
//设定连接
channel.configureBlocking(false);
//打开链接
channel.connect(new InetSocketAddress(ADDR, PORT));
// 注册连接服务端socket动作
channel.register(this.selector, SelectionKey.OP_CONNECT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void run() {
while(true){
try {
Thread.sleep(1000);
//多路复用器 监听开始, 这个地方是阻塞的
this.selector.select();
//获取所有的 Channel 信息
Iterator<SelectionKey> selectedKeys = this.selector.selectedKeys().iterator();
//当存在Channel的情况
while(selectedKeys.hasNext()){
//获取SelectionKey后,删掉集合里面的这个SelectionKey
SelectionKey selectionKey = selectedKeys.next();
selectedKeys.remove();//保证channel只遍历一次
if(selectionKey.isConnectable()){
//连接
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (channel.isConnectionPending()) {
channel.finishConnect();
System.out.println("完成连接!");
byteBuff.clear();
byteBuff.put("Hello,Server".getBytes());
byteBuff.flip();
channel.write(byteBuff);
}
channel.register(this.selector, SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){
//可读
SocketChannel channel = (SocketChannel) selectionKey.channel();
//获取到客户端的SocketChannel
SocketChannel sc =(SocketChannel) selectionKey.channel();
//晴空数据
this.byteBuff.clear();
int cnt = sc.read(this.byteBuff);
//数据没有的情况
if(cnt == -1){
selectionKey.channel().close();
selectionKey.cancel();
return;
}
//复位 ,将position置为1
this.byteBuff.flip();
//建立一个数组,大小 为传递过来的 数据长度
byte [] data = new byte[this.byteBuff.remaining()];
//将buffer数据读取到 数组中
this.byteBuff.get(data);
System.out.println("接收数据");
//打印传送过来的数据内容
System.out.println("客户端:\t"+new String(data));
//进行下一步,注册写数据
sc.register(this.selector, SelectionKey.OP_WRITE);
}else if(selectionKey.isWritable()){
//可写
SocketChannel channel = (SocketChannel) selectionKey.channel();
byteBuff.clear();
//不断重控制台读取数据
byteBuff.put("Hello Server,I'm Client ".getBytes());
//复位
byteBuff.flip();
//写道服务器
channel.write(byteBuff);
//清空
byteBuff.clear();
//设定这个Channel的操作是发送到Read
channel.register(this.selector, SelectionKey.OP_READ);
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String [] args){
Client2 client = new Client2();
new Thread(client).start();
}
}