文章目录
- 前言
- 阻塞和非阻塞
- Java 的 IO 和 NIO
- NIO 中的几个概念 Channel 、Buffer 和 Selector
- ServerSocketChannel 和 SocketChannel 是 Socket 场景下对 Channel 的一种具体实现
- 将 ServerSocketChannel 转化为 SocketChannel 的方法 accept()
- SelectionKey 是 Channel 和 Selector 的关联凭证
- SelectionKey 的四个状态
- SelectionKey 预期状态的设定和更新-感兴趣的事件
- SelectionKey 实际状态的判断
- 可以通过 SelectionKey 获取 Channel,强制转化为具体类型
- SelectionKey 可以携带附加对象
- SelectionKey 携带附加对象的获取
- 代码
- 参考资料
前言
在网络通信中,服务器端需要接收客户端传递的信息并进行处理,如果在接收信息的同时服务器端只能等待IO,显然会造成服务器资源的浪费,也可能造成服务的阻塞。所以本文介绍NIO 并在 Socket 通信中加以实践运用。
阻塞和非阻塞
本段摘自 《Java 高并发程序设计》
“阻塞和非阻塞通常用来形容多线程间对相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。”
非阻塞就是相反的意思
NIO 既可以设置为阻塞的,也可以设置为非阻塞的
Java 的 IO 和 NIO
Java 的 IO 是阻塞的,简单说就是一个线程在处理一个信息的时候,从输入、处理、成功返回或者报错,期间处理端的资源会被一直占用,而 IO 本身是一个十分耗费时间的过程,从而造成了资源的浪费和效率的下降。而NIO,又被成为 New IO 或者 Non-block I/O,其允许数据传输
先
存储到缓存中,处理端通过轮询缓存的方式对已准备好对队列进行处理,I/O传输和数据处理分离了,从而提高了 IO 效率。
(更信息的内容请移步 https://www.cnblogs.com/diegodu/p/6823855.html ,但是建议先别看了)
NIO 中的几个概念 Channel 、Buffer 和 Selector
NIO 通过一组机制将数据传输和处理进行了分离,从而可以实现非阻塞的效果。
Channel
:字面意思就是通道,比如服务端要获取客户端的信息,就要先获取通道,然后通过通道来进行信息的获取
Buffer
:数据并非直接从客户端到服务端,而是先被放到缓存区中,然后服务端从通道中读取或者写入缓存,这个缓存就是 Buffer (描述的不是很书面化,但是我相信你有明白我的意思)
Selector
:服务端并非直接获取到了 Channel ,而是需要先获取 Selector ,可以认为 Selector 是 Channel 的管理者,因为服务肯定不止一个,所以 Channel 也肯定不止一个。
由于Buffer的存在,Channel 可以判断是否有数据需要处理,这样有则处理,没有则不处理,数据来来就先存在 Buffer 中,然后通过 Channel 去读或写。IO 部分的操作和业务流程操作解耦了。
ServerSocketChannel 和 SocketChannel 是 Socket 场景下对 Channel 的一种具体实现
类似于标准和实现, Channel 是一个抽象,参考下图可以看到具体的类(或接口)关系,
ServerSocketChannel 和 SocketChannel 是 对 Channel 的一种具体实现,而且其适用于 Socket 场景
将 ServerSocketChannel 转化为 SocketChannel 的方法 accept()
ServerSocketChannel serverSocketChannel;
SocketChannel clientChannel=serverSocketChannel.accept();
SelectionKey 是 Channel 和 Selector 的关联凭证
Channel 需要被注册到 Selector 进行"托管"
每次向选择器(Selector)注册通道(Channel)时就会创建一个选择键。
通过调用某个键(SelectionKey)的 cancel 方法关闭其通道,或者通过关闭其选择器来取消该键之前,它一直保持有效。取消某个键(SelectionKey)不会立即从其选择器中移除它;相反,会将该键(SelectionKey)添加到选择器(Selector)的已取消键集,以便在下一次进行选择操作时移除它。
可通过调用某个键(SelectionKey)的 isValid 方法来测试其有效性。
SelectionKey 的四个状态
OP_READ
: 可读
OP_WRITE
:可写
OP_CONNECT
:可连接
OP_ACCEPT
:可以接收
SelectionKey 预期状态的设定和更新-感兴趣的事件
有两种方式可以设定或者更新
第一种:Channel 在注册到 Selector 时可以指定一个状态,表示这个 Channel 接下来可以做什么,状态有四种OP_READ
、OP_WRITE
、OP_CONNECT
、OP_ACCEPT
比如下面到代码向 selector 注册 ServerSocketChannel ,状态为 OP_ACCEPT,表示 selector 可以为 ServerSocketChannel 准备接收其他请求到相关工作了
第二种是直接调用 SelectionKey 的成员方法
需要注意的是,可以一次绑定多个类型,如下方式二的事例
//方式一:绑定时设定感兴趣事件
SelectionKey acceptKey=serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//方式二:调用成员方法设定感兴趣事件
selectionKey.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);//重新注册感兴趣的消息事件
SelectionKey 实际状态的判断
在将 Channel 在注册到 Selector 时 我们指定了这个 Channel 需要做到准备工作,这之后还需要判断通道是否满足了我们到预期状态
比如初始化时预期状态为 OP_READ,那么就要判断现在是否可以读了,可读状态满足后才可以读取或者设置为 OP_WRITE ,可写状态满足后才可以写
// boolean 判断 selectionKey 是否可读
selectionKey.isReadable()
可以通过 SelectionKey 获取 Channel,强制转化为具体类型
可以通过 SelectionKey 获取 Channel,强制转化为具体类型
//通过 SelectionKey 获取 ServerSocketChannel 对象
ServerSocketChannel server=(ServerSocketChannel) selectionKey对象.channel()
//通过 SelectionKey 获取 SocketChannel 对象
SocketChannel server=(SocketChannel) selectionKey对象.channel()
SelectionKey 可以携带附加对象
SelectionKey 本身是可以携带附加对象的,添加的方式有两种
第一种是在 Channel 绑定到 Selector 到时候
第二种是调用 SelectionKey 的成员方法 .attach();
// 方式一:Channel 绑定到 Selector 时
SelectionKey clientKey=clientChannel.register(selector, SelectionKey.OP_READ,"附加内容,可以是Object");
//方式二:调用 SelectionKey 成员方法
SelectionKey clientKey=clientChannel.register(selector, SelectionKey.OP_READ);
clientKey.attach("附加内容,可以是Object");
SelectionKey 携带附加对象的获取
直接看代码
//你具体是啥类型就强制转化为啥类型
Object object=(Object) selectionKey.attachment();
代码
NIO 实现 Socket 服务端
相比 IO 实现 Socker 通信,NIO 很繁琐,要有充分的思想准备。
基本思路是,获取 selector ,注册 channel 且初始化状态为 OP_ACCEPT ,然后依次轮训 channel ,对于满足可接受的,修改状态为 OP_READ,并从新注册到 selector,对于满足可读到,读取数据
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
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.nio.channels.spi.SelectorProvider;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 使用线程池实现 Socket 服务端 处理 Socket 请求
* NIO
* @author jie.wu
*/
public class ConcurentSocketNioServer {
private Selector selector;//NIO 的 Selector
private ExecutorService pools=Executors.newCachedThreadPool();//声明一个线程池
/**
* <p>统计某一个Socket 花费的时间<p>
* key:Socket<br>
* value:开始的时间戳<br>
*/
public static Map<Socket,Long> time_start=new HashMap<Socket,Long>(10240);
@SuppressWarnings("unused")
private void startServer() throws IOException{
selector=SelectorProvider.provider().openSelector();//通过工厂方法获得一个 Selector 实例
ServerSocketChannel ssc=ServerSocketChannel.open();//获得表示服务端的 ServerSocketChannel 实例
ssc.configureBlocking(false);//设置为非阻塞模式,默认是阻塞模式
InetSocketAddress socketAddress=new InetSocketAddress(10000);//设置 Socket 端口
ssc.socket().bind(socketAddress);//将 Socket 服务地址和端口绑定到 ServerSocketChannel
//将 ServerSocketChannel 注册 到 selector,并将感兴趣到事件设置为 Accept
//每向selector 注册一个ServerSocketChannel就会返回一个 SelectionKey 作为凭证
//这个凭证是有状态的,比如接收、读、写、销毁等,这里我们并没有下一步的操作
SelectionKey acceptKey=ssc.register(selector, SelectionKey.OP_ACCEPT);
//截止到目前,我们一共只向 selector 注册了一个 SelectionKey ,感兴趣状态为 Accept
//等待-分发 网络消息
for(;;){
//The number of keys, possibly zero, whose ready-operation sets were updated
//这是一个阻塞方法,如果当前没有任何数据准备好,它就会等待,一旦有数据可读,它就会返回,返回值是准备就绪的 SelectionKey 的数量
int keyNumbers=selector.select();
//System.out.println("准备就绪的 SelectionKey 数量="+keyNumbers);
//获取 selector 中的所有 SelectionKey 的集合
Set<SelectionKey> readyKeys=selector.selectedKeys();
//遍历集合
Iterator<SelectionKey> i=readyKeys.iterator();
long endTime=0;//服务器端处理结束时间
while(i.hasNext()){
SelectionKey sk=(SelectionKey) i.next();
i.remove();//将待处理的 SelectionKey 从集合中移除
//SelectionKey 是否可以接收
if(sk.isAcceptable()){
//如果是,重新初始化一个 channel,感兴趣事件为 accept 进行客户端的接收
doAccept(sk);
}
//SelectionKey 是否可读
else if(sk.isValid()&&sk.isReadable()){
//如果时间记录Map 还不包含这个 Socket
if(!time_start.containsKey(((SocketChannel)sk.channel()).socket())){
time_start.put(((SocketChannel)sk.channel()).socket(), System.currentTimeMillis());
}
//如果可读,就进行读取
doRead(sk);
}
//SelectionKey 是否可写
else if(sk.isValid()&&sk.isWritable()){
//如果可写,则进行写操作
doWrite(sk);
endTime=System.currentTimeMillis();
long beginTime=time_start.remove(((SocketChannel)sk.channel()).socket());
System.out.println("spend:"+(endTime-beginTime)+"ms");
}else{
System.out.println("没有预期状态");
}
}
}
}
/**
* 处理 Accept 状态的 SelectionKey
* @param sk
*/
private void doAccept(SelectionKey sk){
//获取 SocketChannel 方式有两种,第一种 ServerSocketChannel 返回 SocketChannel
ServerSocketChannel server=(ServerSocketChannel) sk.channel();
SocketChannel clientChannel;
//第二种 是强制转化-不可行
//SocketChannel clientChannel=(SocketChannel) sk.channel();
try {
clientChannel=server.accept();
clientChannel.configureBlocking(false);//非阻塞模式,IO结束后再通知线程来处理
//向 selector 注册了一个 SelectionKey ,感兴趣状态为 Read
SelectionKey clientKey=clientChannel.register(selector, SelectionKey.OP_READ);
EchoClient echoClient=new EchoClient();
clientKey.attach(echoClient);
InetAddress clientAddress=clientChannel.socket().getInetAddress();
System.out.println("NIO Accept from :"+clientAddress.getHostAddress()+".");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private class EchoClient{
private LinkedList<ByteBuffer> outq;
EchoClient(){
outq=new LinkedList<ByteBuffer>();
}
public LinkedList<ByteBuffer> getOutputQueue(){
return outq;
}
public void enqueue(ByteBuffer bb){
outq.addFirst(bb);
}
}
@SuppressWarnings("unused")
private void doRead(SelectionKey sk){
SocketChannel channel=(SocketChannel) sk.channel();
ByteBuffer bb=ByteBuffer.allocate(8192);//准备 8k 的缓冲区
int len;
try {
len=channel.read(bb);
if(len<0){
disconnect(sk);
return;
}
// byte b[] = new byte[1024];
// StringBuffer sbf = new StringBuffer();
// for (int n; (n = channel.read(bb)) != -1;) {
// sbf.append(new String(b, 0, n));
// }
// if(sbf.length()==0){
// disconnect(sk);
// return;
// }else{
// System.out.println("服务器端收到客户端消息:"+sbf.toString());
// }
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println("Failed to read from client.");
e.printStackTrace();
disconnect(sk);
return;
}
bb.flip();//调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
pools.execute(new HandMsg(sk,bb));
}
@SuppressWarnings("unused")
private void disconnect(SelectionKey sk){
SocketChannel sc=(SocketChannel)sk.channel();
try {
sc.finishConnect();
}catch (IOException e){
e.printStackTrace();
System.out.println("Fail to finish SocketChannel");
}
}
class HandMsg implements Runnable{
private SelectionKey sk;
private ByteBuffer bb;
public HandMsg(SelectionKey sk,ByteBuffer bb){
this.sk=sk;
this.bb=bb;
}
@Override
public void run() {
EchoClient echoClient=(EchoClient) sk.attachment();
echoClient.enqueue(bb);//将数据压入队列
sk.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);//重新注册感兴趣的消息事件
//强迫 selector 立即返回,selector.wakeup 主要是为了唤醒阻塞在selector.select上的线程,
//让该线程及时去处理其他事情,例如注册channel,改变interestOps、判断超时等等。
selector.wakeup();
}
}
@SuppressWarnings("unused")
private void doWrite(SelectionKey sk){
SocketChannel channel=(SocketChannel) sk.channel();
//获取该 SelectionKey 携带的附属信息
EchoClient echoClient=(EchoClient) sk.attachment();
LinkedList<ByteBuffer> outq=echoClient.getOutputQueue();
ByteBuffer bb=outq.getLast();
int len;
try {
len = channel.write(bb);
if(len==-1){
disconnect(sk);
return;
}
if(bb.remaining()==0){
//The Buffer was complately written,remove it
outq.removeLast();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(outq.size()==0){
sk.interestOps(SelectionKey.OP_READ);
}
}
public static void main(String[] args) {
ConcurentSocketNioServer c=new ConcurentSocketNioServer();
try {
c.startServer();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
参考资料
[1]、https://blog.csdn.net/zjy15203167987/article/details/82084093
[2]、《Java 高并发程序设计》
[3]、https://www.cnblogs.com/diegodu/p/6823855.html
[4]、https://www.cnblogs.com/Free-Thinker/p/6231743.html
[5]、https://blog.csdn.net/iteye_11587/article/details/82681312
[6]、https://www.cnblogs.com/sally-zhou/p/6624901.html