通过BIO+ 多线程方式可以解决掉多用户连接的问题,随着用户连接的增多,就会无限的创建新的线程处理用户连接但是线程的不能无限创建的,因为
怎么解决这个问题?
可以通过线程池来解决、复用线程达到减少线程创建的目的,假如线程池提供4个线程来做处理,假如线程池中4个线程都在read数据,进入到阻塞状态。这个时候是不能接收新用户的连接。
NIO模型最大的特点是将IO 的系统调用设置为非阻塞,进行系统调用时 ,如果数据准备好则会立即拿到数据执行,如果数据没有准备,会立即返回一个状态码(返回-1,ERR:),用户可以做自己事情,在一定时间需要继续查询
IO复用模型,是系统提供了(select、poll)方式用来同时监听多个用户的请求,一旦有事件完成则将结果通知给用户线程进行处理
NIO模型:说的是系统调用会立即返回
IO复用是指复用器select等同时监听多个用户的连接
IO复用结合BIO来,常使用的是IO复用+NIO模型来使用,才是真正意义上同步非阻塞模型
NIO中提供了选择器(Selector 类似底层操作系统提供的IO复用器:select、poll、epoll),也叫做多路复用器,作用是检查一个或者多个NIO Channel(通道)的状态是否是可读、可写······可以实现单线程管理多个channel,也可以管理多个网络请求
Channel:通道,用于IO操作的连接,在Java.nio.channels包下定义的,对原有IO的一种补充,不能直接访问数据需要和缓冲区Buffer进行交互
通道主要实现类:
SocketChannel:通过TCP读写网络中的数据,一般客户端的实现
ServerSocketChannel:监听新进来的TCP连接,对每一个连接都需要创建一个SocketChannel,一般是服务端的实现
Buffer:缓冲区
buffer的作用就是用户和channel通道进行数据交流的桥梁
public class Server {
public static void main(String[] args) {
//创建服务端ServerSocketChannel实例
ServerSocketChannel serverSocketChannel;
{
try {
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(6666));
System.out.println("服务端启动啦");
//监听
//设置serverSocketChannel为非阻塞
serverSocketChannel.configureBlocking(false);
//创建selector复用器
Selector selector = Selector.open();
//将监听事件注册到复用器上
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//等待系统返回已完成的事件
//select()本身是会阻塞,等系统告诉用户空间那些事件已经准备就绪,返回结果表示已准备完成的事件个数
while (selector.select() > 0) {
//感兴趣事件集合(指的就是注册到selector中的事件)
Set <SelectionKey> selectionKeys = selector.selectedKeys();
Iterator <SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//删除掉已经完成的事件
iterator.remove();
if (selectionKey.isAcceptable()) {
//当前是可接收事件已经准备就绪
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
//接收客户端连接,返回一个SocketChannel实例表示是客户端的连接
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("客户端连接上");
//设置SocketChannel实例为非阻塞
socketChannel.configureBlocking(false);
//将SocketChannel注册到复用器上,并关注读事件
socketChannel.register(selector,SelectionKey.OP_READ);
}
if (selectionKey.isReadable()) {
//当前有可读事件发生
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//读数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
//往Buffer中写数据
int read = socketChannel.read(buffer);
if (read == -1) {
//客户端已经关闭
socketChannel.close();
continue;
}
//进行读写模式的切换
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
//读取buff数据
buffer.get(bytes);
//接收数据
String msg = new String(bytes);
System.out.println("客户端:"+socketChannel.getRemoteAddress()+" 发送数据"+msg);
String recv = "[echo]:"+msg;
//回复消息
//先将Buffer清空
buffer.clear();
//往Buffer写数据
buffer.put(recv.getBytes());
//读写模式切换
buffer.flip();
//将Buffer数据读到channel通道
socketChannel.write(buffer);
//业务断开
if ("exit".equals(msg)) {
socketChannel.close();
}
}
}
}
//来轮序是什么事件完成
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1、创建ServerSocketChannel实例
2、对通道serverSocketChannel进行端口绑定bind
3、将通道设置为非阻塞configureBlocking设置为false
4、创建复用器实例Selector(Selector.open())
5、将serverSocketChannel注册到复用器上,并关注ACCEPT事件
6、等待系统返回已完成事件集合(select)
7、通过遍历感兴趣事件集合
8、如果是accept事件完成,则进行accept操作,接收客户端连接,将客户端连接channel设置为非阻塞,并关注read事件
9、循环第6步,
10、如果是read事件完成,则进行读数据操作
…
public class Client {
public static void main(String[] args) {
try {
//创建SocketChannel通道
SocketChannel socketChannel = SocketChannel.open();
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//实例化复用器
Selector selector = Selector.open();
//连接服务端,该connect不会阻塞,会立即返回 boolean true:连接成功 false:表示还未连接成功
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 6666))){
//表示连接不成功 当前正在连接,将当前的可连接事件交给内核帮助监听
socketChannel.register(selector,SelectionKey.OP_CONNECT);
//等待连接完成
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isConnectable()) socketChannel.finishConnect();
}
}
//要么是连接成功
//给服务端发送数据
Scanner scanner = new Scanner(System.in);
String msg = null;
//注册读事件
socketChannel.register(selector,SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while ((msg =scanner.nextLine()) != null) {
//重复性读操作
buffer.clear();
msg +="\n";
//将数据写入到Buffer中
buffer.put(msg.getBytes());
//读写模式切换
buffer.flip();
//将数据从 Buffer中写入channel通道,对于Buffer而言,是读取数据
socketChannel.write(buffer);
//等内核数据准备完成
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
//将数据从通道写入Buffer
channel.read(allocate);
//读写模式切换
allocate.flip();
//remaining 实际读取的数据长度
byte[] bytes = new byte[allocate.remaining()];
//将Buffer数据读到byte数组中
allocate.get(bytes);
String recv = new String(bytes);
System.out.println(recv);
}
}
//判断是否结束
if ("exit".equals(msg)) {
break;
}
}
//关闭资源
selector.close();
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1、创建SocketChannel的通道实例
2、将通道SocketChannel设置为非阻塞
3、实例化复用器(Selector.open)
4、主动连接服务端(connect),当前会立即返回,返回结果为true,则表示连接成功
5、如果返回为false,将通道socketChannel注册到复用器中,等待系统返回连接成功。调用finishConnect
6、和服务端进行读写操作(通过通道进行读写,需要借助Buffer)
…
在BIO中connect操作是一个阻塞方法,在NIO中设置为非阻塞,交给IO复用器来进行监听关注的事件是否完成(CONNECT事件),内核来帮助监听感兴趣事件是否完成,前提是事件必须触发,发生之后内核才能够监听(connect),在NIO中SocketChannel是设置为非阻塞,当前操作会立即返回,当调用connect之后,会立即返回一个Boolean类型的结果,表示是连接成功还是正在连接重,当前的connect事件才触发,触发之后内核才能帮助监听,当然前提是将connect事件注册到复用器上,内核才能关注到该事件。
客户端断开连接,服务端会接收到-1,占用空间,服务端认为是有数据可以读取,就会一直有可读事件发生需要服务端处理,判断通道接收是否为-1,是则结束接收
写操作在NIO中也是一个事件,注意:写事件是需要主动发起写操作,一般写完之后立即write操作不会进行阻塞,即通常写操作并不需要注册。
channel是和用户的操作IO相连,但是通道不能直接使用(需要使用Buffer)
读操作:从channel里读取的数据通过Buffer交给用户
写操作:将用户要发送的数据通过Buffer交给channel
使用示例:读操作
Java NIO 的 Buffer 用于和 **NIO Channel(通道)**交互。数据是从通道读入缓冲区,从缓冲区写入到通道中。缓冲区本质上是块可以写入数据,再从中读数据的内存。该内存被包装成 NIO 的 Buffer 对象,并提供了一系列方法,方便开发者访问该块内存
使用Buffer读写数据一般四步走:
当向 buffer 写数据时,buffer 会记录写了多少数据。一旦要读取数据,需通过 flip()
将 Buffer 从写模式切到读模式。在读模式下,可读之前写到 buffer 的所有数据。一旦读完数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear()
或 compact()
方法。
5. clear()
会清空整个缓冲区
6. compact()
只会清除已经读过的数据。任何未读数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
使用示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
Buffer的实现底层是通过特定类型(byte、long…)数组来存储数据
数组中数据的操作需要借助4个指针来操作:
// Invariants: mark <= position <= limit <= capacity
private int mark = -1; //标记
private int position = 0; //位置
private int limit; //限制
private int capacity; //容量
标记、位置、限制和容量值遵守以下不变式
0<=标记<=位置<=限制<=容量
新创建的缓冲区总有一个0位置和一个未定义的标记。初始限制可以为0,也可以为其他值,这取决于缓冲区类型及其构建方式。一般情况下,缓冲区的初始内容是未定义的。
capacity:
作为一个内存块,Buffer有个固定大小,即capacity。你只能往里写capacity个byte、long,char等。一旦Buffer满,需将其清空(通过读或清除数据)才能继续往里写数据。
position:
取决于Buffer处在读还是写模式:
limit:
因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。即你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。
Buffer是一个抽象类,其实现的子类有ByteBuffer
(字节缓冲区)、CharBuffer
(字符缓冲区) ·······
Java NIO中Buffer有如下类型:
这些Buffer类型代表了不同的数据类型,即可通过这些类型来操作缓冲区中的字节。
缓冲区有两种:堆上开辟的空间,堆外开辟的空间
要想获得一个Buffer对象首先要进行分配。每个Buffer类都有一个allocate方法。
Buffer的创建:
ByteBuffer为例:
ByteBuffer allocate(int capacity)
:在堆上创建指定大小的缓冲ByteBuffer allocateDirect(int capacity)
:在堆外空间创建指定大小的缓冲ByteBuffer wrap(byte[] array)
:通过byte数组实例创建一个缓冲区ByteBuffer wrap(byte[] array, int offset, int length)
:指定byte数据中的内容写入到一个新的缓冲区向Buffer写数据
写数据到Buffer有两种方式:
int bytesRead = inChannel.read(buf);
put()
方法写到Buffer里buf.put(127);
从Buffer读数据
从Buffer读数据有两种方式:
int bytesWritten = inChannel.write(buf);
buf.get();
flip()
方法:
flip()
方法将Buffer从写模式切换到读模式。调用flip()
方法会将position设回0,并将limit设置成之前position的值。换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等 —— 现在能读取多少个byte、char等。
选择器或者叫做IO复用器,作用是用来检查一个或者是多个NIO Channel的状态是否是可读,可写…,可以实现单线程管理多个channel,也可以实现多线程管理channel
Selector selector = Selector.open();
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
selector.select();
Set selectionKeys = selector.selectedKeys();
关于通过SelectionKey的channel()
方法返回的类型可以强转为不同的channel类型,如下:
public abstract SelectableChannel channel();
返回的是SelectableChannel
类型
其中ServerSocketChannel
和SocketChannel
类型都是SelectableChannel
的子类,SelectableChannel
是一个抽象类
在NIO中使用的configureBlocking
,register
都是定义在SelectableChannel
类中的方法,ServerSocketChannel
和SocketChannel
都是直接继承SelectableChannel
中的方法
register方法是需要接受两个参数
SelectionKey register(Selector sel, int ops)
第二个参数是“感兴趣的集合”,这个集合是selector需要监听channel对什么事件感兴趣,可以监听的事件类型有四种:connect、read、write、accept事件
SelectionKey中提供了感兴趣事件,总共有四种
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
占用了int的4个bit位
提供的一些方法介绍
boolean isValid() :当前感兴趣事件是否有效
void cancel():取消事件:告诉内核,注册的某一个事件不需要在关注了
boolean isAcceptable() :判断当前是否是可接受事件
…
selectionKey.channel():返回的是该selectionKey对应的channel
selectionKey.selector();返回该selectionKey对应的selector实例
int readyOps():返回需要监控的可读事件
三种键集合
已注册键的集合、已选择键的集合、已取消键的集合
已注册键的集合:
所有的注册到选择器上的事件都会放到已注册事件集合上,包含已经失效的键,通过keys()
方法获取
selector.keys();//已注册事件集合
已选择键的集合:
已选择键的集合是已注册集合的子集,主要是选择器已经监听准备就绪的键的集合
通过selectedKeys()方法拿到结果(可能为空)
已取消键的集合:
是已注册键的集合的子集,包含的是cancel()方法调用多的键,不会是调用cancel方法立即取消,需要先加入到该已取消键的集合
//select() 一直阻塞直至有事件准备就绪才返回 .selectedKeys()不会为空
//select(long timeout) :在指定时间内阻塞,selectedKeys()可能为空
//int selectNow() 不会阻塞,会立即返回 selectedKeys()可能会空
Java的selector选择过程依赖本地操作系统所提供的IO复用模型
调用select()方法执行过程
一个socketchannel
的通道,可以立即为对应选择过程中的键,一旦通道注册,键存在已注册键集合,可能存在已选择键的集合和已取消键的集合
一个通道上又可以有读事件、可以有写事件、可连接事件和可接收事件,对应选择过程当中interset
集合和read
集合
//select()
:一直阻塞直至有事件准备就绪才返回 .selectedKeys()不会为空
//select(long timeout)
:在指定时间内阻塞,selectedKeys()可能为空
//int selectNow()
:不会阻塞,会立即返回 selectedKeys()可能会空
返回就绪事件的个数
返回值都是int类型表示的是有多少通道已经就绪,自上一次调用select()方法后有多少个通道变成就绪状态,在之前select()调用时就进入就绪状态的通道不会被基础本次的调用中,在前一次select()调用进入就绪但现在已经不处于就绪的通道也不会被进入,一旦调用select()方法,并且返回值不为0时,则可以通过SelectionKeys()
方法来访问已选择键的集合
wakeup()
:wakeup()
方法的调用会使处于阻塞状态的select()
方法返回,选择器上第一个还没有返回的选择操作立即返回,如果当前还没有进行中的选择操作,下一次的select()
方法的一次调用会立即返回close()
:通过close关闭seletor操作,使任何一个在选择操作中的阻塞的线程都被唤醒,同时使得注册到selector上的所有的channel被注销,所有的键被取消,但是channel本身不会关闭思路:
主线程主要是进行accept事件的关注,子线程完成socketchannel的读写事件的关注
public class MutilThreadServer {
public static void main(String[] args) {
//获取ServerSocketChannel实例
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9999));
System.out.println("主线程已启动");
serverSocketChannel.configureBlocking(false);
//复用器
Selector selector = Selector.open();
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//循环监听
while (selector.select() > 0) {
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (!selectionKey.isValid()) continue;
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("Thead:"+Thread.currentThread().getName()+",新用户连接:"+socketChannel.getRemoteAddress());
//将socketChannel交给子线程进行读写处理
new SubThread(socketChannel).start();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class SubThread extends Thread {
private SocketChannel socketChannel;
private Selector selector;
private ByteBuffer buffer;
public SubThread(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
try {
this.selector = Selector.open();
this.socketChannel.configureBlocking(false);
this.socketChannel.register(selector, SelectionKey.OP_READ);
buffer = ByteBuffer.allocate(1024);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
//关注读事件
while (selector.select() > 0) {
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (!selectionKey.isValid()) continue;
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
//读数据
buffer.clear();
int read = channel.read(buffer);
if (read == -1) {
//表示客户端发送完数据
channel.close();
break;
}
//读写切换
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String msg = new String(bytes);
System.out.println("Thread:"+Thread.currentThread().getName()+"处理用户:"+channel.getRemoteAddress()+",数据:"+msg);
//给客户端发送数据
buffer.clear();
String recv = "[echo]:"+msg+"\n";
buffer.put(recv.getBytes());
//读写模式切换
buffer.flip();
channel.write(buffer);
if ("exit".equals(msg)) {
channel.close();
break;
}
}
}
}
} catch (Exception e) {
}
}
}
public class Client {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999))){
socketChannel.register(selector,SelectionKey.OP_CONNECT);
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isConnectable()) socketChannel.finishConnect();
}
}
Scanner scanner = new Scanner(System.in);
String msg = null;
socketChannel.register(selector,SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while ((msg =scanner.nextLine()) != null) {
buffer.clear();
msg +="\n";
buffer.put(msg.getBytes());
buffer.flip();
socketChannel.write(buffer);
selector.select();
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
channel.read(allocate);
allocate.flip();
byte[] bytes = new byte[allocate.remaining()];
allocate.get(bytes);
String recv = new String(bytes);
System.out.println(recv);
}
}
if ("exit".equals(msg)) {
break;
}
}
selector.close();
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}