#NIO实战
1. 重识socket编程
1.1 概念
传统的socket编程是bio的,是阻塞的,也就是说在单线程的情况下,serversocket只可以接受一个客户端连接,有个处理方法是,把serversocket设置为多线程的,即每次都创建一条线程来处理客户端的业务,这样serversocket可以连接多个客户端。但是线程数的暴增会增加cpu的负担,而且线程的来回切换也会降低性能。
1.2 代码片段
位置:/Users/xin/code/code/ideaproject/zexin-nio/src/main/java/zexin/con.demo/socket
2. 初识NIO
2.1 概念
- NIO:New IO
- NIO 1.0
- Buffer
- Channel
- Selector
2.2 Buffer
2.2.1 概念
一个Buffer本质上是内存的一块,可以将数据写入这块内存,或者从这块内存获取数据
2.2.2 三大核心概念
- capacity
buffer的大小,一旦设定就不可以更改,比如capacity为1024的IntBuffer,代表一次可以存放1024个int类型的值。一旦buffer的容量达到capacity,需要情况buffer,才能重新写入值
- positon
下一个可读或可写的位置(下标)。写模式切换(flip)到读模式的时候,position会归0,这样就可以从头开始读写了
- limit
写操作模式下,limit代表能写入的最大的数据,刚开始同capacity。写模式切换到读模式,此时limit等于buffer中的实际大小,因为buffer不一定被写满了。
2.2.3 buffer实操
- duplicate():共享底层数组数据,新缓冲区的内容将为此缓冲区的内容。此缓冲区
内容的更改
在新缓冲区中是可见的,反之亦然;这两个缓冲区的位置、界限和标记值是相互独立的。与当前的buffer的position,mark,limit,capacity相同
public CharBuffer duplicate() {
return new HeapCharBuffer(hb,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
offset);
}
- slice():创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。
新缓冲区的内容将从此缓冲区的当前位置开始。此缓冲区内容的更改在新缓冲区中是可见的,反之亦然;这两个缓冲区的位置、界限和标记值是相互独立的。新缓冲区的位置将为零,其容量和界限将为此缓冲区中所剩余的字节数量,其标记是不确定的。
public CharBuffer slice() {
return new HeapCharBuffer(hb,
-1,
0,
this.remaining(),
this.remaining(),
this.position() + offset);
}
新缓冲区的容量、界限、位置和标记值将与此缓冲区相同。当且仅当此缓冲区为直接时,新缓冲区才是直接的,当且仅当此缓冲区为只读时,新缓冲区才是只读的
- clear():清除此缓冲区。将位置设置为 0,将限制设置为容量,并丢弃标记。此方法不能实际清除缓冲区中的数据,但从名称来看它似乎能够这样做,这样命名是因为它多数情况下确实是在清除数据时使用。
- mark():标记位置,在使用resert的时候,可以恢复到这个位置。如果针对同个buffer多次mark,后面的mark会覆盖前面的mark
- resert():恢复到mark的位置
- get(): 顺序读取数据,postion会变化(必须在读模式下)
- allocate: 创建一个堆内存的buffer
- allocatDirect:创建一个直接内存的buffer
- wrap:根据数组创建buffer,用于堆内存buffer,因为直接内存buffer的底层不是数组
- put():往buffer中写入数据
- compact():将缓冲区的位置设置为复制的字节数,而不是零,以便调用此方法后可以紧接着调用另一个相对 put 方法
## 其实就是把当前最后一个可写的位置设置为position,然后以容量大小为limit,把读模式转换成写模式了,并且去除mark标记
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
- rewind():重绕此缓冲区。将位置设置为 0 并丢弃标记。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
## 简单buffer的创建
public static void main(String[] args) {
ByteBuffer buffer0 = ByteBuffer.allocate(10);
if (buffer0.hasArray()) {
System.out.println("buffer0 array:" + buffer0.array());
System.out.println("buffer0 array offset:" + buffer0.arrayOffset());
}
System.out.println("buffer0 capacity:" + buffer0.capacity());
System.out.println("buffer0 limit:" + buffer0.limit());
System.out.println("buffer0 position:" + buffer0.position());
System.out.println("buffer0 remaining:" + buffer0.remaining());
System.out.println("--------------------------------------");
//分配直接Buffer内存.底层非数组
ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);
System.out.println("底层数组?" + buffer1.hasArray());
System.out.println("buffer0 capacity:" + buffer1.capacity());
System.out.println("buffer0 limit:" + buffer1.limit());
System.out.println("buffer0 position:" + buffer1.position());
System.out.println("buffer0 remaining:" + buffer1.remaining());
System.out.println("--------------------------------------");
//通过数组创建Buffer
byte[] bytes = new byte[10];
ByteBuffer buffer2 = ByteBuffer.wrap(bytes);
if (buffer2.hasArray()) {
System.out.println("buffer0 array:" + buffer2.array());
System.out.println("buffer0 array offset:" + buffer2.arrayOffset());
}
System.out.println("buffer2 capacity:" + buffer2.capacity());
System.out.println("buffer2 limit:" + buffer2.limit());
System.out.println("buffer2 position:" + buffer2.position());
System.out.println("buffer2 remaining:" + buffer2.remaining());
System.out.println("--------------------------------------");
//创建特定offset和length的buffer
byte[] bytes2 = new byte[10];
ByteBuffer buffer3 = ByteBuffer.wrap(bytes2, 2, 3);
if (buffer3.hasArray()) {
System.out.println("buffer3 array:" + buffer3.array());
System.out.println("buffer3 array offset:" + buffer3.arrayOffset());
}
System.out.println("buffer3 capacity:" + buffer3.capacity());
System.out.println("buffer3 limit:" + buffer3.limit());
System.out.println("buffer3 position:" + buffer3.position());
System.out.println("buffer3 remaining:" + buffer3.remaining());
/***
* buffer3 array:[B@1d44bcfa
buffer3 array offset:0
buffer3 capacity:10
buffer3 limit:5
buffer3 position:2
buffer3 remaining:3
* 假设buffer为 0123456789
* 那么positon为2,limit为5,中间可写的范围为234,并不包括5,在写模式下
*
*
*
*/
buffer3.put((byte)'H').put((byte)'e').put((byte)'l');
buffer3.flip();
byte[] valueArray = buffer3.array();
for (int index = 0; index < valueArray.length; index++) {
System.out.println(index + ":" + valueArray[index]);
}
}
2.3 Channel
2.3.1 概念
所有的nio操作始于通道,通道是数据来源或数据写入的目的,主要地,java.nio包中主要实现一下几个channel
- FileChannel:文件通道,用于文件的读和写
- DatagramChannel:用于UDP连接的接收和发送
- SocketChannel:TCP连接通道,简单理解就是TCP客户端
- ServerSocketChannel:TCP对应的服务端,用于监听某个端口进来的请求
2.3.2 NIO的读操作
NIO的读操作就是将数据从channel读到buffer中,进行后续处理,调用方法:
channel.read(buffer)
2.3.3 NIO的写操作
NIO的写操作就是将数据从Buffer中写入到Channel中,调用方法:
channel.write(buffer)
2.4 Selector
2.4.1 概念
selector是java nio中的一个组件,用于检查一个或多个nio channel的状态十分处于可读,可写
如此可以实现单线程管理多个channels,也就是管理多个网络连接
2.4.2 相关API
- selector
支持IO多路复用的抽象实体
注册selectable channel
- SelectionKey
表示Selector和被注册的channel之间关系,一份凭证
selectionkey保存channel感兴趣的事件
- Selector.select
代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪
更新所有就绪的SelectionKey的状态,并返回就绪的channel个数
在调用select之后,可以调用selectedKeys
获取相应的key,但是该方法会把所有可用的key都查出来,如果某个key被用过了,记得要remove掉。
在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,这个操作通过调用selectedKeys()方法
2.4.3 Selector实操
- Selector服务端
package zexin.com.demo.nio.selector;
import java.net.InetSocketAddress;
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.Iterator;
import java.util.Set;
/**
* selector的demo
*
* @author: xin
* @create: 2018-12-20 上午11:10
*/
public class EchoHandler implements Runnable{
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
private int num = 0;
public EchoHandler(int port) {
try {
//创建一个selector,其在多线程中是安全的
selector = Selector.open();
System.out.println("selector=" + selector);
//创建一个ServerSocketChannel,用于监听客户端连接
serverSocketChannel = ServerSocketChannel.open();
//设置阻塞模式为非阻塞
serverSocketChannel.configureBlocking(false);
//绑定IP和端口号
/***
* 参考网址:https://blog.csdn.net/aitangyong/article/details/49661907
* 服务端socket处理客户端socket连接是需要一定时间的。ServerSocket有一个队列,存放还没有来得及处理的客户端Socket,
* 这个队列的容量就是backlog的含义。如果队列已经被客户端socket占满了,如果还有新的连接过来,那么ServerSocket会拒绝新的连接。
* 也就是说backlog提供了容量限制功能,避免太多的客户端socket占用太多服务器资源
* 客户端每次创建一个Socket对象,服务端的队列长度就会增加1个
* 服务端每次accept(),就会从队列中取出一个元素
*/
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
//把channel注册到selector中.且只对socket的accept事件感兴趣.也就说只有是accept事件,该channel才会准备就绪.
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器在端口[" + port + "]等待客户请求......");
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
private void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
//选择有多少个channel是准备就绪的.在1000毫秒即1秒内无准备就绪的channel立马返回
int readyNum = selector.select(1000);
//在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,这个操作通过调用selectedKeys()方法.必须先select再调用下面的选择key
if (readyNum > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
SelectionKey key = null;
while (iter.hasNext()) {
key = iter.next();
System.out.println("进入迭代key=" + key);
//如果不把迭代remove掉,那么key会重复被使用.是这样的,在不remove的情况下,在客户端发送消息时,虽然readyNum返回的数量是1(read事件),但是selectedKeys会把之前accept事件的key也查出来,所以就会出现重复的accept的key被使用
iter.remove();
try {
handleInput(key);
} catch (Exception e) {
e.printStackTrace();
if (key != null) {
//取消键,只是把它加入了取消键集,下次不会把该键所相关的channel选中
key.cancel();
//关闭该键集所在的channel
if (key.channel() != null) {
key.channel().close();
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//多路复用器关闭后,所有注册在上面的channel等资源都会被自动关闭,所有不需要重复释放资源
if (selector != null) {
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws Exception {
if (key.isValid()) {
//处理接入的请求信息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
//把该socketChannel注册到selector通道中,并且感兴趣的时间是读事件
SelectionKey sk = socketChannel.register(selector, SelectionKey.OP_READ);
//原子加1(其内部维护了一个static final的原子数,无论创建多少SelectionKey,始终只有一个计数)
sk.attach(num++);
}
if (key.isReadable()) {
//处理读请求
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
//有数据.这里应该写个循环重复读,否则在超过1024个字节后面的数据无法读取
if (readBytes > 0) {
//切换到读模式
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
//把buffer的数据赋值到字节数组中
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("来自客户端[" + key.attachment() + "]的输入:" + body.trim());
if (body.trim().equals("quit")) {
System.out.println("断开客户端[" + key.attachment() + "]的链接");
key.cancel();
socketChannel.close();
} else {
String response = "来自服务端的相应:" + body;
doWrite(socketChannel, response);
}
} else if(readBytes < 0) {
//客户端链路关闭了
System.out.println("异常断开客户端[" + key.attachment() + "]的链接");
key.cancel();
socketChannel.close();
}
}
}
}
private void doWrite(SocketChannel channel, String response) throws Exception {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(bytes);
byteBuffer.flip();
channel.write(byteBuffer);
}
}
}
- Selector客户端
package zexin.com.demo.nio.selector;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
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;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* selector的客户端demo
*
* @author: xin
* @create: 2018-12-20 下午3:07
*/
public class EchoClienHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel2;
private ExecutorService executorService;
private volatile boolean stop;
public EchoClienHandler(String host, int port) {
this.host = host;
this.port = port;
this.executorService = Executors.newSingleThreadExecutor();
try {
selector = Selector.open();
socketChannel2 = SocketChannel.open();
//设置为非阻塞模式
socketChannel2.configureBlocking(false);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
try {
//socketChannel注册到selector中,对连接事件感兴趣
socketChannel2.register(selector, SelectionKey.OP_CONNECT);
//socketChannel开始连接
socketChannel2.connect(new InetSocketAddress(this.host, this.port));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
int readyNum = selector.select(1000);
//System.out.println("readyNum =" + readyNum);
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> iter = set.iterator();
SelectionKey key = null;
if (readyNum > 0) {
while (iter.hasNext()) {
key = iter.next();
System.out.println("key=" +key);
System.out.println("进入迭代");
//iter.remove();
try {
hadleInput(key);
} catch (Exception e) {
e.printStackTrace();
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (executorService != null) {
executorService.shutdown();
}
}
private void hadleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
System.out.println("连接到服务器....");
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("请输入消息,输…入`quit`退出");
executorService.submit(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("线程开始");
buffer.clear();
InputStreamReader ins = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(ins);
String msg = br.readLine();
if ("msg".equals("quit")) {
System.out.println("关闭客户端");
key.cancel();
key.channel().close();
sc.close();
stop = true;
break;
}
buffer.put(msg.getBytes());
buffer.flip();
sc.write(buffer);
System.out.println("请输入消息,输入`quit`退出1232");
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
//注册读事件
sc.register(selector, SelectionKey.OP_READ);
}
}
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(buffer);
if (readBytes > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String msg = new String(bytes, "UTF-8");
System.out.println(msg);
if ("quit".equals(msg)) {
this.stop = true;
}
} else if (readBytes < 0) {
//服务端关闭
System.out.println("服务端异常关闭");
key.cancel();
key.channel().close();
sc.close();
}
}
if(key.isWritable()){
System.out.println("The key is writable");
}
}
}
}
- Selector服务端启动
public class NIOEchoServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
EchoHandler timeServer = new EchoHandler(port);
new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
}
}
- Selector客户端启动
public class NIOEchoClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
new Thread(new EchoClienHandler("127.0.0.1", port), "NIOEchoClient-001").start();
}
}
3. NIO带给我们的好处
- 事件驱动模型
- 避免多线程
- 单线程处理任务
- 非阻塞IO,IO读写不再阻塞,而是返回0
- 基于block的传输,通常比基于流的传输更高效
- 更高级的IO函数,zero-copy(只是在数据准备过程中不需要等待拷贝完成,在数据从内核态到用户态还是需要数据拷贝的)
- IO多路复用大大提高了java网络应用的可伸缩性和实用性
4. 使用NIO的注意事项
- 使用NIO不一定等于高性能
- NIO不一定更快的场景
- 客户端应用
- 连接数小于1000
- 并发程度不高
- 局域网环境下
- NIO完全屏蔽了平台差异(Linux poll/select/epoll,FreeBSD Kqueue)
- NIO仍然是基于各个OS平台的IO系统实现,差异仍然存在
- 使用NIO做网络编程困难