BIO:阻塞式IO,这种通信模型其实就是socket。
它是通过以流的形式进行输入和输出。
该模型中有四个方法,accept()、connect()、read()、write()这些方法都会产生阻塞。因为是阻塞通信,所以这种模型相当于是一个请求产生一个线程,当请求数量越多时,线程数量越多,由此会带来内存占用,内存碎片,cpu对线程的管理调度等问题。当环境处于高并发,高访问量时,BIO就不那么适合了。
由此诞生了NIO。
NIO是jdk1.4之后提出的一套新的非阻塞式IO。
在java.nio中,定义了通道Channel和缓冲区Buffer。
Buffer是一个缓冲区,大小可以自己设定。本质上就是一个数据的结构,是传输数据的载体。Channel是一个通道,可以同时进行数据的输入和输出。
首先来看BIO:
Server端:
@Test
public void testAccept() throws Exception{
ServerSocket server=new ServerSocket();
server.bind(new InetSocketAddress(9999));//绑定本机,端口是9999
Socket socket = server.accept();
System.out.println("测试阻塞");
}
Client端:
@Test
public void testConnect() throws IOException{
Socket client=new Socket();
client.connect(new InetSocketAddress(9999));
System.out.println("测试连接");
}
可以发现,如果说服务器端没有客户端进行连接,代码就走不到测试连接那一段,一直会处于阻塞状态。当客户端一连接上就输出测试阻塞。
再看其他方法:
Server端:
@Test
public void testWrite() throws Exception {
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(9999));
Socket socket = server.accept();
OutputStream out = socket.getOutputStream();
for (int i = 0; i < 10000; i++) {
out.write("hello world".getBytes());// 当一端写出数据而没有一端去读取数据的时候,就会产生阻塞。当前数据是写入到网卡缓冲区的。
System.out.println(i);
}
System.out.println("写出数据");
}
Client端:
@Test
public void testRead1() throws IOException {
Socket client = new Socket();
client.connect(new InetSocketAddress(9999));
}
@Test
public void testRead2() throws IOException {
Socket client = new Socket();
client.connect(new InetSocketAddress(9999));
InputStream inputStream = client.getInputStream();
byte[] bs=new byte[20];//创建一个数组的缓冲区
while(inputStream.read(bs)!=-1){
inputStream.read(bs);
System.out.println(new String(bs));
}
}
执行Junit测试。首先启动Server端,然后执行测试客户端的testRead1,发现服务器端虽然是写出了一些数据,但是还不到10000的时候就阻塞掉了。因为当前输出流输出数据是输出到网卡缓冲区,有一定的输出空间。所以能进行输出。但是最终缓冲区一满,就发生阻塞。我们启动testRead2的Junit测试,发现阻塞放开并输出所有i的值,证明当有一端进行输出,另一端进行输入才不会产生阻塞。所以write这个方法也是阻塞的。同理可以验证其他方法。
由此证明bio的阻塞方式。
而对于NIO:
Server端:
@Test
public void createServer() throws IOException{
//创建一个服务端通道
ServerSocketChannel ssc=ServerSocketChannel.open();
//设置为非阻塞,默认是true,false是非阻塞
ssc.configureBlocking(false);
//绑定一个端口
ssc.bind(new InetSocketAddress(9999));
//接收客户端连接
SocketChannel sc = ssc.accept();
System.out.println("测试accept是否是阻塞的,当前是服务器端");
}
Client端
@Test
public void createClient() throws Exception{
SocketChannel sc =SocketChannel.open();
sc.configureBlocking(false);//设置非阻塞模式
sc.connect(new InetSocketAddress(9999));
System.out.println("测试connect是否阻塞,当前是客户端");
}
从这个Junit测试中可以发现,哪怕就是服务器端开启,并没有客户端连接代码也能够往下继续输出。而对于客户端,哪怕是服务器端并不存在或者未开启,Client这边去连接,没有连接上,代码也能往下继续输出。
由此能够测试出nio中的accept、connect都是非阻塞的。同理可以测试其他方法。
但是此时使用nio也引出了另外的问题:
由此引出Selector
Selector是一个多路的复用器。就像是路由器和交换机。在一个Selector上可以同时注册多个非阻塞的通道,从而只需要很少的线程数既可以管理许多通道。
引入Selector之后,Selector会监听注册在其身上的事件。如果某个线程对应的客户端没有发生任何时间,Selector就会把这个线程阻塞掉,cpu就不会轮询这个处于阻塞状态的线程。从而避免了空转,节省了cpu资源。而当一旦某个线程的事件被Selector监听到,这个线程就会从阻塞状态被唤醒。
SelectionKey代表一个事件、并在Selector上注册事件:
结合Buffer、Selector:
Server端:
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
//创建一个ServerSocketChannel对象
ServerSocketChannel ssc=ServerSocketChannel.open();
//设置非阻塞式
ssc.configureBlocking(false);
//当前通道绑定9999款口号
ssc.socket().bind(new InetSocketAddress(9999));
//创建一个selector
Selector selector=Selector.open();
//注册事件,对服务通道来说,最初要注册ACCEPT事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true){
//该方法会产生阻塞,当有事件产生时,阻塞放开。
selector.select();
//获取到的监听事件集合,代码走到这,即表示有事件发生,所以需要获取到事件的集合
Set selectedKeys = selector.selectedKeys();
Iterator iterator = selectedKeys.iterator();
//遍历事件集合里的每个事件
while(iterator.hasNext()){
//当前一个sk就代表一个具体的事件
SelectionKey sk = iterator.next();
if(sk.isAcceptable()){//客户端接入事件
//获取服务端通道
ServerSocketChannel server=(ServerSocketChannel) sk.channel();
SocketChannel sc = server.accept();
//将通道设置为非阻塞模式
sc.configureBlocking(false);
//在通道上注册读事件和写事件
sc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
System.out.println("有客户端接入,当前处理的线程编号是:"+Thread.currentThread().getId());
}
if(sk.isReadable()){//如果是读事件,相当于客户端向服务端发送数据,服务端读取数据
SocketChannel sc=(SocketChannel) sk.channel();
ByteBuffer bf=ByteBuffer.allocate(20);
//将数据读取到缓冲区
sc.read(bf);
System.out.println("数据:"+new String(bf.array()+"\r\n 当前线程编号:"+Thread.currentThread().getId()));
//移除读事件的监听
//interestOps(),获取的是监听事件的状态:0000 0101
sc.register(selector, sk.interestOps()&~SelectionKey.OP_READ);
}
if(sk.isWritable()){//如果是写事件,服务器端向客户端写数据
SocketChannel sc=(SocketChannel) sk.channel();
ByteBuffer bf=ByteBuffer.wrap("这是一条服务器端向客户端发送的数据。二黑二黑,我是大黑!".getBytes());
sc.write(bf);
sc.register(selector, sk.interestOps()&~SelectionKey.OP_WRITE);
}
//处理完事件之后将当前事件移除,避免同一事件被重复处理
iterator.remove();
}
}
}
Client端:
public static void main(String[] args) throws Exception {
System.out.println("客户端启动");
Selector selector = Selector.open();
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress(9999));
sc.register(selector, SelectionKey.OP_CONNECT);
while (true) {
selector.select();
Set set = selector.selectedKeys();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (sk.isConnectable()) {
SocketChannel s = (SocketChannel) sk.channel();
if (!s.isConnected()) {
s.finishConnect();
}
s.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);//向Selector注册读和写的事件
}
if (sk.isWritable()) {
SocketChannel s = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.wrap("这是一条客户端向服务器端发送的数据。大黑大黑,我是二黑!".getBytes());
while (buffer.hasRemaining()) {
s.write(buffer);
}
s.register(selector, sk.interestOps() & ~SelectionKey.OP_WRITE);
}
if(sk.isReadable()){
SocketChannel s = (SocketChannel) sk.channel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
s.read(buffer);
System.out.println(new String(buffer.array()));
s.register(selector, sk.interestOps() & ~SelectionKey.OP_READ);
}
iterator.remove();
}
}
}
这里我们能够观察到,每启动一个服务,一个客户端,都是通过同一个线程来处理的请求。实现了Selector的监听机制,达到了复用,防止了cpu空转。