网络编程之BIO、NIO

       已经很久没更新博客了,惭愧。在这之前先讲一下面试可能会问到的三次握手与四次挥手,也就是Tcp如何建立连接?

       假设A城市往B城市发送信件,先A发到B,B收到,在发给A,在A发给B,建立起初步通信。三次挥手是为了证明A,B的收信和发信能力是ok的,这样就证明连接是通常的。

        第一次握手:当A发到B时,B收到信后,此时B城市就明白了,A城市的发信能力和B城市的收信能力是ok。

        第二次握手:当B发到A时,A收到信后,此时A城市就明白了,B城市的发信能力和A城市的收信能力是ok,加上之前的发信,同时也就知道了A(自己)的发信能力和B城的收信能力是ok的,这就相当于A知道了双方都是OK的,但B还疑惑,因为它第一次虽然知道了A的发信能力个自身的收信能力是OK的,但并不不知道城B(自身)的的发信能力和A城市的收信能力如何,所以需要第三次握手。

        第三次握手:A发给B  当B收到后,就知道了B(自身的发信能力)和A城市的收信能力同样是ok的,既然双方都知根知底,那就掏心掏肺喜结连理吧。即完成首次通信的建立。

       大概流程可见如下图:

三次握手TCP协议连接成功后,可以得出结论
第一次握手A-->BA得出结论:啥也不知道,不确定自己是否发送ok 
B得出结论:A发  B收 ok 
第二次握手B-->AA可以得出结论:A收  B发,加上之前的第一次发送可以推出A发  B收也ok,所以明白AB发送接受都ok
B得出结论:基于第一次知道A发  B收 ok,但是并不知道自身的发送是否成功和A的接受能力
第三次握手A-->BA发给B,B收到消息后就可以证明:B的发信能力和A的自收信能力ok

      第一次挥手:A发到B,告诉B我要挂了,此时B就明白了A准备要挂了。

      第二次挥手:B发到A,告诉A,我还在忙,先别着急挂电话,于是A就知道B还没准备好挂电话的意思。

      第三次挥手:B发到A,好了,我忙完了可以挂了,此时A知道了B想挂了,但A毕竟怕老婆,不敢先挂电话,于是就说那我挂了哟,这也就是第四次要发到B的话了。

      第四次挥手:A发到B,不管此时B有没有收到,A都会等待2ms,如果时间内没收到消息则说明老婆大人先挂了,如果老婆收到了,则说明A挂了,也会挂掉电话。

还可能面试涉及的问题:

1. 什么是长连接和短连接?

       在Http1.0中默认使用的是短连接,也就是说浏览器与服务器每进行一次http操作就建立一次连接,但任务结束后就中断连接,如果客户端浏览器访问的某个HTML或者其他类型的web页包含其它web资源,如图像文件、css文件;当浏览器每遇到这样一个web资源,就会建立一次http会话。

      但在Http1.1起,默认使用长连接。用以保持连接特性,使用长连接的http协议会在响应头中加入这行代码:

在使用长连接的情况下,当一个网页打开完成后,客户端与服务器之间用于传输Http数据Tcp连接不会关闭,如果客户端再次访问这个服务器的网页,就会继续使用这一次建立好的连接,keep-alive不会永远保持连接,它有一个保持时间,可以在不同的服务器软件中设定这个时间。实现长连接要求客户端和服务器都必须支持长连接。

2. Http协议与Tcp  Ip协议的关系?

      Http的长连接与短连接实际上本质是Tcp的长连接与短连接,Http属于应用层,Tcp属于传输层,Ip属于网络层(七层网络模型)。Ip协议只要解决网络路由与寻址问题,Tcp主要解决如何在Ip层之上可靠地传递数据包,使用网络上的另一端收到客户端发出的所有包,并且顺序与发出顺序一致。

 

      言归正传, 其实这个还得牵涉到我之前做的一个模块,当时是在负责写一个日志下载的功能,其实现就是利用B/S架构去完成的,也就是利用socket编程在结合多线程去完成这样的一个功能。设备作为服务端(日志源),浏览器作为客户端(发请求),形成一个通信,由于InputStream类read()方法是阻塞的,所以就必须利用到多线程或者线程池,每发一个请求就利用一个新的线程这样就互不干扰了,下面是我一个小demo模拟(类似于聊天)了上述业务。

客户端代码:

package NIO.clientpkg;

import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.Scanner;


/**
 *目的:客户端向服务端发送请求服务端接受请求,并回应相同的信息,一旦客户端写入bye,则结束本次操作
 */
public class Client {


    public static void main(String[] args) throws Exception{
        SocketAddress socketAddress = new InetSocketAddress ("localhost",9001);
        Socket socket = new Socket ();
        socket.connect (socketAddress,1000*5);//5second
        //发送输入的数据  以打印流的方式
        PrintStream out = new PrintStream (socket.getOutputStream ());
        //接受服务端传来的数据,将其保存在打印流中
        Scanner in = new Scanner (socket.getInputStream ());
        System.out.println ("请输入你要发送给服务端的信息");
        Scanner scanner = new Scanner (System.in);
        while (true){
            //发送消息
            if (scanner.hasNext ()){ //hasNext和next都是半阻塞的方法,会一直处于等待,所以必须用一个变量来接收
                //输入的消息
                String str = scanner.next ();
                out.println (str);
                if (str.equals ("bye")){
                    System.out.println ("服务端发过来的消息:" + in.next ());
                    break;//不能立即退出,会导致客户端退出服务端还处于连接丢包
                }
                //接收消息
                if (in.hasNext ()){
                    System.out.println ("服务端发过来的消息:" + in.next ());
                }
            }
        }
        socket.close ();
        scanner.close ();
        in.close ();
        out.close ();
    }

}

服务端代码:

package NIO.serverpkg;

import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 /**
 *目的:客户端向服务端发送请求服务端接受请求,并回应相同的信息,一旦客户端写入byebye,则结束本次操作
 *TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
 *TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,
 *就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
 */
public class Server2 {

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor (10,50,5, TimeUnit.SECONDS,new LinkedBlockingQueue<> (100));

    public static void main(String[] args) throws Exception{

        //通常服务端在启动的时候回绑定一个众所周知的地址(ip+端口)用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动
        //分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
        ServerSocket serverSocket = new ServerSocket (9001);//初始化socket,对端口进行绑定,并且对端口进行监听
        //这里要用一个while来接受不断发过来的请求,,但是因为accept会阻塞,故这里必须用线程
        while (!serverSocket.isClosed ()){
            Socket socket = serverSocket.accept ();//侦听并接受到此套接字的连接。此方法在进行连接之前一直阻塞。这应该是一个一直在进行的新线程,因为她面对的可能是诸多追求者
            if (socket.isConnected ()){
                System.out.println ("连接成功男朋友为:" + socket.toString ());
            }
            threadPoolExecutor.execute (() -> {
                try {
                    PrintStream out = new PrintStream (socket.getOutputStream ());//发送输入的数据  以打印流的方式
                    Scanner in = new Scanner (socket.getInputStream());//扫描流负责接受客户端发来的请求,一直不断变化的,因为socket是不断变化的
                    while (true){
                        if (in.hasNext ()){//阻塞的方法 inputStream类的read()方法
                            String str = in.next ();
                            System.out.println ("接受到客户端发来的信息:" + str);
                            out.println (str);//接收到消息  并回复同等内容
                            if (str.equals ("bye")){
                                break;
                            }
                        }
                    }
                    out.close ();
                    in.close ();
                    socket.close ();
                } catch (IOException e) {
                    e.printStackTrace ();
                }

            });
        }
        System.out.println ("结束服务端");
        serverSocket.close ();

    }

}

启动一个服务端和两个客户端,客户端分别向服务端发送:我是client2,我是client2_1:

题外话:

我们都知道socket是遵守tcp协议的,那如果浏览器访问我的服务端可行吗?我们测试一下:显然是不可行的因为浏览器请求是http请求,明显可以看出客户端请求是发过来了的,但是响应给浏览器确实无效的,因为协议不一致,所以只需要返回固定格式的数据(基于http协议)给客户端即可,tomcat服务器其实也是socket连接,然后对报文 请求头和体实现了一个组装。

改写代码(返回http格式):

package NIO.serverpkg;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerHttp {


    public static void main(String[] args) throws Exception{

        ServerSocket serverSocket = new ServerSocket (9001);
        while (!serverSocket.isClosed ()){
            Socket socket = serverSocket.accept ();
            if (socket.isConnected ()){
                System.out.println ("连接成功男朋友为:" + socket.toString ());
            }
            InputStream in = socket.getInputStream ();
            BufferedReader reader = new BufferedReader(new InputStreamReader (in, "utf-8"));
            String msg = "";
            while ( (msg = reader.readLine () ) != ""){
                System.out.println (msg);
                if (msg.length () == 0){
                    break;
                }
            }
            System.out.println("收到数据,来自:"+ socket.toString());
            OutputStream out = socket.getOutputStream ();
            out.write("HTTP/1.1 200 OK\r\n".getBytes());
            out.write("Content-Length: 11\r\n\r\n".getBytes());
            out.write("Hello World".getBytes());
            out.flush();
        }
        System.out.println ("结束服务端");
        serverSocket.close ();

    }

}

http格式如下:

上面只是演示下而已,言归正传,多线程或者固定线程池这样设计是存在问题的,当并发高的话,线程会出现不够用的情况,而且当线程处理的业务逻辑属于耗时操作io或者长期占用不关闭连接时,必定会出现线程不够用的情况,则又会衍生性能问题,那么该如何解决呢?

正题  NIO的引入

阻塞(blocking)IO含义:资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。(一般用于获取数据)

非阻塞(non-blocking)IO:资源不可用时,IO请求离开返回,返回一个不可用标识。(一般用于获取数据)

同步(syncronous)IO:应用阻塞在发送或接受数据的状态,知道数据传输或成功返回。(一般是拿到数据后进行的一个处理方式)

异步(asyncronous)IO:应用发送或接受数据后立刻返回,实际处理是异步执行的。(一般是拿到数据后进行的一个处理方式)

ServerSocket##accept方法,InputStream##read方法都是阻塞API,操作系统底层API中,默认socket操作都是阻塞的,send/recv等接口也都是阻塞的。所带来的的问题就是在处理网络IO操作时,一个线程只能处理一个网络连接。好在jdk1.4提供了新的java非阻塞ioAPI--NIO。其中里面涉及三个核心组件:Buffer缓冲区、

Buffer缓冲区

        缓存区的本质是一个可以写入数据的内存块(类似数组),然后可以再次读取,此内存块包括在NIO  Buffer对象中,该对象提供了一种范方法,可以更轻松的使用内存块。

         使用Buffer进行数据写入与读取需要以下四个操作: 

  1. 将数据写入缓冲区
  2. 调用buffer.flip(),转换为读取模式
  3. 缓冲区读取数据
  4. 调用buffer.clear()或者buffer.compact()转为写模式

Buffer三个重要属性:

  1. capacity容量:作为一个内存块,Buffer具有一定的固定大小,也可以称之为容量
  2. position位置:写入模式时代表写入模式的位置,读取模式时代表读取模式时的位置
  3. limit限制:写入模式,限制等于buffer的容量(不动)。读取模式下,限制等于写入的数据量(动)

举例说明:如下图初始化的时候呢,假定是一个8个字节的数组,默认初始化的时候是一个写模式,写一个字节position位置就移动一格,假设写了3个字节进去,想要读取的话,则要调用flip()方法,切换至读模式,那么读的limit就是写入的数据量,也就3了,然后position从左到右从第一个位置读到第三个位置为节点。如下图(读完之后在写注意覆盖情况)

ByteBuffer的内存类型:

ByteBuffer提供了堆内内存和堆外内存的两种实现(堆外内存的获取方式:ByteBuffer bf = ByteBuffer.allocateDirect(noBytes));

好处:1.进行文件IO或者网络IO是比heapBuffer少一次拷贝。(file/socket  ---> os memory --->jvm heap)GC会移动对象内存,在写file或者socket的过程中,jvm的实现中会将数据复制到堆外,在进行写入。

            2.GC范围之外,降低GC压力,但实现了自动管理,DirectBuffer中有一个cleaner对象(phatomReference),cleaner被GC前会执行clear方法,触发DirectBuffer中定义的allocateDirect

建议:1.性能确实可观的时候才是用,分配给大型长寿命;(网络传输、文件读写场景)

            2.通过虚拟机参数maxDirectMemorySize大小,防止耗尽整个机器的内存

堆内内存示例:

package com.dongnaoedu.network.nio.demo;

import java.nio.ByteBuffer;

public class BufferDemo {
    public static void main(String[] args) {
        // 构建一个byte字节缓冲区,容量是10  堆内内存
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        // 默认写入模式,查看三个重要的指标
        System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));
        // 写入2字节的数据
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        // 再看数据
        System.out.println(String.format("写入3字节后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 转换为读取模式(不调用flip方法,也是可以读取数据的,但是position记录读取的位置不对)
        System.out.println("#######开始读取");
        byteBuffer.flip();
        byte a = byteBuffer.get();
        System.out.println(a);
        byte b = byteBuffer.get();
        System.out.println(b);
        System.out.println(String.format("读取2字节数据后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));
        // 继续写入3字节,此时读模式下,limit=3,position=2.继续写入只能覆盖写入一条数据
        // clear()方法清除整个缓冲区。compact()方法仅清除已阅读的数据。转为写入模式
        byteBuffer.compact(); // buffer : 1 , 3
        byteBuffer.put((byte) 3);//从2开始 2 3 4位置为4
        byteBuffer.put((byte) 4);
        byteBuffer.put((byte) 5);
        System.out.println(String.format("最终的情况,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));
        // rewind() 重置position为0
        // mark() 标记position的位置
        // reset() 重置position为上次mark()标记的位置

控制台输出:
初始化:capacity容量:10, position位置:0, limit限制:10
写入3字节后,capacity容量:10, position位置:3, limit限制:10
#######开始读取
1
2
读取2字节数据后,capacity容量:10, position位置:2, limit限制:3
最终的情况,capacity容量:10, position位置:4, limit限制:10

    }
}

 

Channer通道

ChannelApi覆盖了整个UDP/TCP网络和文件IO,比如FileChannel、SocketChannel、ServerSocketChannel 

与标准IO  Stream操作的区别:在一个管道内进行读取和写入  Stream通常是单向的(input和output),可以非阻塞读取和写入操作通道,通道始终读取或写入缓冲区。

SocketChannel:

SokcketChannel用于建立tcp网络连接,类似java.net.socket 

有两种socketChannel的形式:

1、客户端主动发起的服务连接  SocketChannel socketChannel = SocketChannel.open ();

2、服务端获取的新连接  SocketChannel socketChannel = serverSocketChannel.accept ();

注意点:

写:使用socketChannel .write(byteBuffer)  向通道内写数据时,可能尚未写入任何数据时就可能返回,所以需要循环调用write()

读:使用socketChannel.read (byteBuffer)读取通道内数据时,可能直接返回而根本不读取任何数据,根据返回的int判断读取多少字节

ServerSocketChannel:

ServerSocket则是监听新建立的tcp连接通道,类似ServerSocket

SocketChannel socketChannel = serverSocketChannel.accept ();如果设置为非阻塞模式,如果没有连接进来则立即返回null,所以要检查返回的socketChannel是否为null

下面是通过socketchannel,serversocketchannel,bytebuffer 单线程实现服务器的多个客户端连接

服务端代码

package com.dongnaoedu.network.humm;


import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * @author Heian
 * @time 19/06/16 20:00
 * @copyright(C) 2019 深圳市长亮保泰
 * 用途:
 */
public class ServerSocketChannel1 {

    private static ArrayList<SocketChannel> SocketChannelList = new ArrayList<>();//解决无法获取多个客户端连接

    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open ();
        serverSocketChannel.configureBlocking (false);//设置为非阻塞模式
        serverSocketChannel.socket ().bind (new InetSocketAddress (8080));
        System.out.println ("服务端启动了");

        while (true){
            SocketChannel socketChannel = serverSocketChannel.accept ();//非阻塞  如果没有挂起的连接则直接返回null
            if(socketChannel != null){//因为是非阻塞的,没有连接返回null
                //tcp请求  读取响应
                System.out.println("收到新连接 : " + socketChannel.getRemoteAddress());
                socketChannel.configureBlocking (false);// 默认是阻塞的,一定要设置为非阻塞
                SocketChannelList.add (socketChannel);
            }else {
                // 没有新连接的情况下,就去处理现有连接的数据,处理完的就删除掉
               Iterator<SocketChannel> iterator = SocketChannelList.iterator ();
                while (iterator.hasNext ()){
                    SocketChannel channel = iterator.next ();//新的连接没发送消息 就会去重新遍历scoketchannnel
                    ByteBuffer receiveBf = ByteBuffer.allocate (1024);
                    if (channel.read(receiveBf) == 0) {// 等于0,代表这个通道没有数据需要处理,那就待会再处理
                        continue;
                    }
                    while (channel.isOpen () && channel.read (receiveBf) != -1){//按照1kb大小去读取socketchannel 的数据,没有返回0,不阻塞但不断做轮询
                        // 长连接情况下,需要手动判断读取数据有没有读取结束,可能数据量很大,远超过1kb (此处做一个简单的判断: 超过0字节就认为请求结束了)
                        if (receiveBf.position() > 0) break;//如果读到了,就相当于会把数据写入到 receiveBf
                    }
                    if(receiveBf.position () == 0)continue;//如果没有数据则结束此次循环,终止下面的操作
                    receiveBf.flip ();//切换至读取模式
                    byte[] bytes = new byte[receiveBf.remaining ()];
                    ByteBuffer byteBf = receiveBf.get (bytes);
                    String reveiveMsg = new String (byteBf.array (),"utf-8");
                    System.out.println ("收到"+channel.getRemoteAddress ()+"客户端发来的消息为:" + reveiveMsg);
                    //响应结果  这里随便响应一个
                    String response = "HTTP/1.1 200 OK\r\n" +
                            "Content-Length: 11\r\n\r\n" +
                            "Hello World";
                    ByteBuffer sendBf = null;
                    if ("bye".equals (reveiveMsg)){
                        sendBf = ByteBuffer.wrap ("bye".getBytes ());
                    }else {
                        sendBf = ByteBuffer.wrap (response.getBytes ());
                    }
                    while (sendBf.hasRemaining ()){
                        channel.write (sendBf);//非阻塞  继续循环等待新的连接  或者处理同一个客户端发来的请求
                    }
                }

            }
        }
        // 用到了非阻塞的API, 在设计上,和BIO可以有很大的不同.继续改进
    }

}

客户端代码

package com.dongnaoedu.network.humm;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * @author Heian
 * @time 19/06/16 19:37
 * @copyright(C) 2019 深圳市长亮保泰
 * 用途:
 */
public class SocketChannel1 {

    public static void main(String[] args) throws Exception{
        SocketChannel socketChannel = SocketChannel.open ();
        socketChannel.configureBlocking (false);//设置为非阻塞模式
        socketChannel.connect (new InetSocketAddress ("127.0.0.1",8080));
        while (!socketChannel.finishConnect ()){
            Thread.yield ();//没有连接则阻塞在此
        }
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入:");
        while (true){
            if (scanner.hasNext ()){  //不输入就阻塞到此
                String sendMsg = scanner.next ();
                ByteBuffer sendBf = ByteBuffer.allocate (1024);
                sendBf.put (sendMsg.getBytes ());
                sendBf.flip ();
                while (sendBf.hasRemaining ()){
                    socketChannel.write (sendBf);//发送数据   向通道写入数据
                }
                //读取响应数据
                ByteBuffer receiveBf = ByteBuffer.allocate (1024);//默认是写模式
                while (socketChannel.isConnected () && socketChannel.read (receiveBf) != -1){//非阻塞  没有值就返回0
                    // 长连接情况下,需要手动判断读取数据有没有读取结束,可能数据量很大,远超过1kb (此处做一个简单的判断: 超过0字节就认为请求结束了)
                    if (receiveBf.position() > 0) break;//读操作会默认读到的数组存到receiveBf
                }
                receiveBf.flip ();//切换至读模式
                byte[] bytes = new byte[receiveBf.limit()];
                ByteBuffer bf= receiveBf.get (bytes);//将刚才写入的数据 按照1kb大小读取
                String receiveMsg = new String (bf.array ());
                System.out.println ("读取到的数据为:" + receiveMsg);
                if ("bye".equals (receiveMsg)){
                    break;
                }
            }
        }
        socketChannel.close ();
        scanner.close ();

    }

}



步骤:先启动服务端,然后开启多个客户端实例,我这边开了两个。

 

但很明显上述依然存在性能问题,就是不断地通过轮询很浪费cpu资源,假设有100个channel,那么也就会去轮询100次,而且有的是连接上的但是没发数据过来,一直在那做轮询,所以很浪费,那么怎么解决呢?往下看

selector选择器

selector是nio的一个组件,可以检查一个或多个nio通道,并确定哪些通道可以进行读取和写入,实现单个线程管理多个通道,实现单线程管理多个通道,从而实现多个网络连接。说白了它就是一个管家,专门来管理channel通道的连接。

一个线程使用selector监听多个channel的不同事件,四个事件分别对应selectionkey的四个常量:

  1. connect连接(SelectionKey.OP_CONNECT)          
  2. accept准备就绪(OP_ACCEPT)
  3. Read读取(OP_READ)
  4. Write写入(OP_WRITE)

实现一个通道处理多个通道的核心概念理解:事件驱动机制。

非阻塞的网络通道,我们只需要selector注册通道感兴趣的事件类型,线程通过监听事件来触发响应代码的执行(更底层的是操作系统的多路复用),channel对象通过注册的方式,交给Selector管家管理,管理你需要关注的事件,每一个被管理的对象就是一个key,所以会形成一个keys数组,数组元素中就有channel和SelectionKey的状态,一旦被监听到会返回另外一个SelectionKey的集合,里面每个selectionkey存储着channel和这个被管理对象的一些状态(比如是否收到一个连接selectionkey.isAcceptable(),是否有数据刻度selectionkey.isReadable(),是否有连接进来selectionkey.isConnectable()),然后遍历集合,拿到你所需的key,继续交给selector管理,继续去侦听你关注的事件。

下面是selector的大概操作流程:

package com.dongnaoedu.network.nio.demo;

import java.io.IOException;
import java.net.InetSocketAddress;
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;

public class SelectorDemo {
    public static void main(String[] args) throws IOException {

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//客户端永远是被动地,没有read或者write方法
        Selector selector = Selector.open();// 创建Selector
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);// serverSocketChannel注册OP_READ事件
        serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口一定要发生在注册之后,防止你启动之后有连接进来没被监听

        while(true) {
            int readyChannels = selector.select();// 会阻塞,直到有事件触发  调用此方法,监听才开始工作,监听所有连接进来的客户端
            if(readyChannels == 0) continue;
            Set<SelectionKey> selectedKeys = selector.selectedKeys();// 获取被触发的事件集合
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            while(keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                if(key.isAcceptable()) {
                    SocketChannel socket = ((ServerSocketChannel) key.channel()).accept();//通过key拿到对应客户端的channel对象
                    socket.register(selector, SelectionKey.OP_READ);//先跳出此循环,返回上一个循环,当有新事件进来则(可能使我们刚注册的事件)
                    // serverSocketChannel 收到一个新连接,只能作用于ServerSocketChannel

                } else if (key.isConnectable()) {
                    // 连接到远程服务器,只在客户端异步连接时生效

                } else if (key.isReadable()) {
                    // SocketChannel 中有数据可以读

                } else if (key.isWritable()) {
                    // SocketChannel 可以开始写入数据
                }

                // 将已处理的事件移除
                keyIterator.remove();
            }
        }

    }
}

        就是假设一个客户端连接到服务端,然后交给管家管理,然后将事件指派给管家。需要明白的一点就是,同一时刻管家只会侦听同一个通道的一件事情,比如多个客户端将写很多事件交给管家,然后管家就会帮很多人去侦听它们关注的这些事件,然后在各自的客户端去遍历你自身侦听事件的集合(不知道这里说的对不对),比如,执行到写,执行写入操作完成后,你肯定要获取响应,又可以切换到读。然后管家又会去判断您这边是否允许读操作。

下面是改良后的类似聊天的额小程序,有点redis多路复用和消息通知机制的味道,效率高。

服务端:

package com.dongnaoedu.network.humm;

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实现非阻塞服务器
 */
public class NIOServerV2 {

    public static void main(String[] args) throws Exception {

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 1. 创建服务端的channel对象
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        Selector selector = Selector.open();// 2. 创建Selector
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0); // 3. 把服务端的channel注册到selector,注册accept事件
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
        serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 4. 绑定端口,启动服务
        System.out.println("启动成功");
        while (true) {
            // 5. 启动selector(管家)
            selector.select();// 阻塞,直到事件通知才会返回
            Set<SelectionKey> selectionKeys = selector.selectedKeys();//拿到所有客户端的事件
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();//强转为ServerSocketChannel
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("收到新连接:" + socketChannel);
                } else if (key.isReadable()) {// 客户端连接有数据可以读时触发
                    try {
                        SocketChannel socketChannel = (SocketChannel) key.channel();// 不再是新连接,则直接强转为SocketChannel
                        ByteBuffer receivebf = ByteBuffer.allocateDirect(2048);
                        while (socketChannel.isOpen() && socketChannel.read(receivebf) != -1) {
                            // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                            if (receivebf.position() > 0) break;
                        }
                        if (receivebf.position() == 0) continue; // 如果没数据了, 则不继续后面的处理
                        receivebf.flip();
                        byte[] content = new byte[receivebf.remaining()];
                        receivebf.get (content);
                        System.out.println("收到数据,来自:" + socketChannel.getRemoteAddress()+":" + new String (content));
                        // TODO 业务操作 数据库 接口调用等等  服务端类似生产者   提供数据给消费者

                        // 响应结果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                        while (buffer.hasRemaining()) {
                            socketChannel.write(buffer);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        key.cancel();
                    }
                }
            }
        }
    }
}

客户端:

package com.dongnaoedu.network.humm;

import java.io.IOException;
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.Scanner;
import java.util.Set;

public class NIOClientV2 {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        socketChannel.connect(new InetSocketAddress("localhost", 8080));//非阻塞会立即返回
      
        while (true) {
            selector.select();//开启管家
            Set<SelectionKey> selectionKeys = selector.selectedKeys();//可读 可写 连接成功
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                if (selectionKey.isConnectable()) {
                    try {
                        if (socketChannel.finishConnect()) {
                            System.out.println("连接成功-" + socketChannel);
                            //ByteBuffer buffer = ByteBuffer.allocateDirect(2048);
                            //selectionKey.attach(buffer); // attach 类似于我们发邮件中的附件 也可以不传,这里只是为了演示此功能
                            selectionKey.interestOps(SelectionKey.OP_WRITE);//连接成功了,将事件切换至写事件
                            //socketChannel.register (selector,SelectionKey.OP_WRITE,buffer);  //这个也可以  上面两段代码等于这一段
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        return;
                    }
                } else if (selectionKey.isWritable()) {// 可以开始写数据
                    //ByteBuffer buf = (ByteBuffer) selectionKey.attachment();
                    //buf.clear();//取到这个附件,将其清空  这里没必要写  这是为了演示下
                    ByteBuffer sendbf = ByteBuffer.allocate (1024);
                    Scanner scanner = new Scanner(System.in);
                    System.out.print("请输入:");
                    String msg = scanner.next ();
                    //scanner.close();//这里不能关闭 具体参考https://www.cnblogs.com/qingyibusi/p/5812725.html
                    sendbf.put(msg.getBytes());
                    sendbf.flip ();//在写入数据后,一定要切换至读模式
/*
                    如果我不做那个flip切换到写模式,那么它默认是写模式,假设我写了一个1,那么position 就是1  limit1024,capacity也是1024,这样通过socketchannel写入通道内的就是
                    位置1到1024,那肯定是数据为空的,如果我切换至写,那么position就变成了0,kimit就变成了1,那socketchannel写入通道的就是0到1
*/
                    while (sendbf.hasRemaining()) {
                        socketChannel.write(sendbf);
                    }
                    selectionKey.interestOps(SelectionKey.OP_READ);// 切换到感兴趣的事件
                } else if (selectionKey.isReadable()) {// 可以开始读数据
                    System.out.println("收到服务端响应:");
                    ByteBuffer receivebf = ByteBuffer.allocate(1024);
                    while (socketChannel.isOpen() && socketChannel.read(receivebf) != -1) {//没有数据,就不断轮询  不能说是阻塞
                        // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                        if (receivebf.position() > 0) break;
                    }
                    receivebf.flip();//切换至读取模式
                    byte[] content = new byte[receivebf.remaining()];
                    ByteBuffer bf = receivebf.get (content);
                    System.out.println("收到服务端端数据:" + socketChannel +new String(bf.array (),"utf-8"));
                    selectionKey.interestOps(SelectionKey.OP_WRITE);
                }
            }
        }
    }

}

总结:

BIO阻塞IO线程等待时间长,一个线程负责一个连接,线程多且利用率低,NIO非阻塞IO,线程利用率高,一个线程处理多个连接事件,性能强大,tomcat8已经移除了BIO网络处理相关代码,默认采用NIO处理网络请求,NIO为开发者提供了丰富的API,但是在网络应用中直接使用API比较繁琐,而且将性能提升光有NIO是不够的,还需要将多线程结合,而且开源社区有对NIO进行封装的框架,如Netty、Mina等,后续将继续更新。

 

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值