BIO的步骤流程说明:
BIO代码实现:
// 用于存储读取到的数据
// 用于存储读取到的数据
byte[] bytes = new byte[1024];
ServerSocket serverSocket= null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080)) ; // 绑定监听的端口
for(;;){
//线程阻塞,等待连接
Socket socket = serverSocket.accept();
// 如果有客户端连接进来就往下走,如果没有读取到数据会堵塞
InputStream input = socket.getInputStream();
try{
int read = input.read(bytes);
String content = new String(bytes);
System.out.println(content);
}} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
客户端:
try {
Socket socket = new Socket("127.0.0.1",8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
总结:
其中BIO可以通过采用多线程完成并发请求处理,accept方法处阻塞,不变,如果监听到有客户端连接进来,则开启一个线程,并且将获取到的和客户端长连接交互的socket 作为对象传入子线程中,然后让read方法的阻塞在子线程中出现,而主线程则继续执行,然后循环到accept()方法中等待第二个客户端线程的链接。
其中负责监听的socket对象在创建之后会返回一个文件描述符,用于bind一个端口,这个bind调用的是操作系统的内核函数,调用listen 方法用于监听,而accept方法则阻塞住不往下走,直到客户端通过操作系统完成三次握手的过程之后accept会接受到这个客户端,返回一个文件描述符(简单理解就是和客户端交互的socket对象),然后进入read方法,进行阻塞。可以通过开辟线程来执行read,而accept则循环监控连接;
缺点:线程频繁创建和销毁(线程栈消耗),上线文切换频繁,不安全。无法解决单线程问题
NIO non-blocking IO(非阻塞IO)关键组成:
- channel 通道
- buffer 缓冲区
- selector 选择器
1、channel通道:
Java NIO的通道类似流,但又有些不同,channel既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。通道可以异步地读写。通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。
通道的类型(参考右边链接):http://ifeve.com/channels/
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel 从文件中读写数据。
DatagramChannel 能通过UDP读写网络中的数据。
SocketChannel 能通过TCP读写网络中的数据。
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
以ServerSocketChannel为例:首先先开启ServerSocketChannel,调用open()方法,并且可以非阻塞的监听新进连接,如果不设置为非阻塞也会和BIO 一样会放弃cpu的执行权,直到有新连接被监听到才会往下执行;该对象可产生一个用于监听的ServerSocket对像,ServerSocket可以绑定监听指定的端口;注意:FileChannel不能切换到非阻塞模式
// 开启通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 获取服务端监听套接字
ServerSocket serverSocket = serverChannel.socket();
// 绑定监听的端口 也可以直接用ServerSocketChannel 来监听
// serverChannel.bind(new InetSocketAddress(8080));
serverSocket.bind(new InetSocketAddress(8080));
// 设置通道为非阻塞 accept()方法会立即返回,如果没有监听到新连接返回null,监听到
//新连接则返回一个与新连接交互的SocketChannel通道
serverChannel.configureBlocking(false);
通道和缓冲区的数据交互
//将 Buffer 中数据写入 Channel
channel.write(buff)
//从 Channel 读取数据到 Buffer
channel.read(buff)
channel 注册到选择器上,register()如果已经注册过的,更新对象和更新channel对什么事件感兴趣,注册之后返回一个selectorKey,用于标识channel 和channel感兴趣的事件,如果是有客户端连接这个channel,并且事件是这个channel感兴趣的 ,那么操作系统会将这个连接分配给这个channel。关于底层应用的的共享内存啊、内核轮询之类的可以自己去看马士兵的NIO里面介绍的挺多的。
2、buffer 缓冲区:
缓冲区本质上是一个内存块,您可以在其中写入数据,然后可以在以后再次读取。该内存块包装在NIO Buffer对象中,数据从通道读取到缓冲区,然后从缓冲区写入通道,该对象提供了一组方法,可以更轻松地使用该内存块。Java NIO 有以下Buffer类型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
要获取Buffer对象,您必须首先分配它。每个Buffer类都有一个allocate()执行此操作的方法
ByteBuffer buf = ByteBuffer.allocate(1024);
2.1、使用Buffer来读取和写入数据通常遵循以下四个步骤:
- 将数据写入缓冲区
- 呼叫 buffer.flip()
- 从缓冲区读取数据
- 调用buffer.clear()或buffer.compact()清除数据
2.1.1 将数据写入缓冲区
数据写入缓冲区有两种方式:
int bytes = channel.read(buf); // 从通道读取数据写入缓冲池
buf.put("hello world".getByte()); // 在程序中向缓冲池添加数据
2.1.2 呼叫 buffer.flip()
buffer中的flip方法涉及到bufer中的Capacity,Position和Limit三个概念
- 容量 (capacity)
- 位置 (position)
- 限制 (limit)
容量是在调用allocate(1024)时定义的容量,有兴趣可以看看源码:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
// 传入的都是容量所以说开始的时候cap = lim
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0);
}
// 调用上级
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
// 容量赋值position的开始位置赋值为0,标记设为-1,mark是起辅助判断作用。
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
// 最后会到这里来
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
所以初始化之后的值:limit = cap = 1024(我传入的值) 而mark = -1 , position = 0 ;
读取过程(下面链接讲的挺清楚的):
https://blog.csdn.net/u013096088/article/details/78638245
读取结束(消息长15):position 位于15;
调用flip()方法之后:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
limit = 15 ; positon 重置;也就是说调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)
简答点描述就是:你将文件按一定的顺序叠起来,然后领导突然说把已经叠好的按顺序交给他,你只能把现在到了第几个文件记住,然后把已经整理的交给领导。大概就这个意思吧,可能例子不够恰当
2.1.3 从缓冲区读取数据
int bytes = channel.write(buf); // 从缓冲池中向通道写入数据
byte b = buffer.get(0); // 从缓冲池中获取数据
2.1.4 调用buffer.clear()或buffer.compact()清除数据
读取所有数据后,需要清除缓冲区,以使其可以再次写入。您可以通过两种方式执行此操作:通过调用clear()或通过 compact()。该clear()方法清除整个缓冲区。该compact() 方法仅清除您已读取的数据。任何未读的数据都将移至缓冲区的开头,并且现在将在未读的数据之后将数据写入缓冲区。
3、selector 选择器
一个selector对象可以通过调用Selector.open()来创建,这个工厂方法会使用系统默认的selector provider来创建一个新的selector对象。或者我们还可以通过实现抽象类SelectorProvider自定义一个selector provider,然后调用它的openSelector()来创建,
例如:new SelectorProviderImpl().openSelector()
除非调用selector.close(),否则该selector将会一直保持打开状态。
通过channel的register方法,将channel注册到给定的selector中,并返回一个表示注册关系的SelectionKey 对象。
SelectionKey key = channel.register(selector,
SelectionKey.OP_ACCEPT|SelectionKey.OP_READ|SelectionKey.OP_WRITE|SelectionKey.OP_CONNECT);
// 为了确保selector捕捉到信息(也就是有客户端的行为才响应)需要调用select方法
// 该方法属于一个阻塞方法,即没有内容不会往下执行,表示监听的端口没有人连接也没有人读写
selector.select();
第一个表示选择器,参数二表示注册到选择器之后这个channel对什么事件感兴趣,SelectionKey抽象类里定义了4中,分别:是:
- OP_ACCEPT: 接收连接进行事件,表示服务器监听到了客户连接
- OP_READ: 读就绪事件,表示通道中已经有了可读的数据
- OP_WRITE: 写就绪事件,表示已经可以向通道写数据了
- OP_CONNECT:表示客户与服务器的连接已经建立成功
你可以获取selector中的所有selector中的selectionKeys的集合:
selectionKeys = selector.selectedKeys();
// 获取遍历selectionKeys的迭代器,你通过这个key值的遍历找出这个选择器中你感兴趣的事件进行业务操作。
iterator = selectionKeys.iterator();
// 这个key你捕捉到了并且你处理了,那么就应该将这个key移除这个集合,避免重复处理相同事件
iterator.remove(selectionKey);
太深的内容我自己理解也不到位,希望将自己能理顺的知识做个总结,方便有需要的人参考;下面是我开发中用到的NIO例子;
完整的NIO 的Demo :
public class ServerCrawImpl implements IServerCraw {
public ServerCrawImpl() {
}
@Override
public void startup() {
ServerSocketChannel serverChannel;
Selector selector;
try {
// 开启管道
serverChannel = ServerSocketChannel.open();
// 获取服务端监听socket
ServerSocket serverSocket = serverChannel.socket();
//绑定端口
serverSocket.bind(new InetSocketAddress(8603));
// 设置socket非阻塞
serverChannel.configureBlocking(false);
//创建Selector复用器,选择器
selector = Selector.open();
//将多个Channel类型事件注册到多路复用器 实现Selector管理Channel,其中的事件可以多个
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
log.debug(ExceptionUtil.getStackMsg(e));
return;
}
while (true) {
try {
//如果selector管理的管道没有客户端请求,客户端的读,客户端的写的交互,则阻塞,返回0
int eventCount = selector.select();
if (eventCount == 0) {
continue;
}
} catch (Exception e) {
log.debug(ExceptionUtil.getStackMsg(e));
break;
}
// 获取所有管道相关信息的keys集合
Set<SelectionKey> keys = selector.selectedKeys();
// 获取遍历keys集合的迭代器
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
// 获取当前的channel对应 key
SelectionKey key = iterator.next();
// 移除,防止重复处理
iterator.remove();
//处理业务
try {
// key存在并且得到的是连接请求
if (key.isValid() && key.isAcceptable()) {
// 获取与客户端交互的channel
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//接受客户端的请求
SocketChannel client = server.accept();
//客户端 服务端 全都设置为 非阻塞
client.configureBlocking(false);
client.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE, ByteBuffer.allocate(2048));
}
if (key.isValid() && key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(3);
int res = 0;
try {
// 从缓存中读取数据到通道中返回给客户端,需要判断一下客户端收到的消息长度和自己发送的长度是否一致
res = client.read(buffer);
} catch (Exception e) {
client.close();
log.debug(ExceptionUtil.getStackMsg(e));
break;
}
//如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭
if (res == -1) {
client.close();
} else if (res == 0) {
continue;
} else if (res > 0) {
continue;
}
}
if (key.isValid() && key.isWritable()) {
try {
// 从缓冲池中获取数据
// 业务操作
} catch (Exception e) {
log.debug("异常:" + ExceptionUtil.getStackMsg(e));
}
}
}
} catch (Exception e) {
log.debug(ExceptionUtil.getStackMsg(e));
}
}
}
}
}
NIO 的弊端:客户端连接就绪之后都会有文件描述符的read阻塞监听,NIO 会重复调用read(),会曾加内核的压力;
我也仅限会用,太深的东西也没办法理解透彻,有不对的欢迎指正。