Netty框架-java基础网络-2 -原生jdk之AIO,BIO,NIO,Buffer

原生JDK网络编程BIO编程

ServerSocker 负责绑定就IP地址,启动端口

那么Socker负责连接操作

一般在编程里面会起一个线程负责监听客户端的连接,当客户端发起一个连接以后,他会为每一个客户端新起一个线程,由线程对客户端进行应答,应答后就销毁线程;

这就就相当于每有一个客户端连接就需要新起一个线程;

缺点: 开销线程数量比较大;

在实际使用BIO编程中一般会使用线程池,用线程池来管理线程,每当有客户端请求之后,打包成一个任务,方放到想成里面去,这样就可以使用少量的线程,来处理大量的任务;这就是伪异步IO模型

缺点:当处理大任务需要花很多时间的时候,就有很多任务在队列里面排队;这也是他的一个弊端;

伪异步IO解决的问题就是节约线程数量的;

BIO标准模式代码

客户端代码

public class BioClient {
​
    public static void main(String[] args) throws InterruptedException,
            IOException {
        //通过构造函数创建Socket,并且连接指定地址和端口的服务端
        Socket socket =  new Socket(DEFAULT_SERVER_IP,DEFAULT_PORT);
        System.out.println("请输入请求消息:");
        //启动读取服务端输出数据的线程
        new ReadMsg(socket).start();
        PrintWriter pw = null;
        //允许客户端在控制台输入数据,然后送往服务器
        while(true){
            pw = new PrintWriter(socket.getOutputStream());
            pw.println(new Scanner(System.in).next());
            pw.flush();
        }
    }
​
    //读取服务端输出数据的线程
    private static class ReadMsg extends Thread {
        Socket socket;
​
        public ReadMsg(Socket socket) {
            this.socket = socket;
        }
​
        @Override
        public void run() {
            //负责socket读写的输入流
            try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()))){
                String line = null;
                //通过输入流读取服务端传输的数据
                //如果已经读到输入流尾部,返回null,退出循环
                //如果得到非空值,就将结果进行业务处理
                while((line=br.readLine())!=null){
                    System.out.printf("%s\n",line);
                }
            } catch (SocketException e) {
                System.out.printf("%s\n", "服务器断开了你的连接");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                clear();
            }
        }
​
        //必要的资源清理工作
        private void clear() {
            if (socket != null)
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
​
}
​

服务器端代码

​
public class BioServer {
    //服务器端必须
    private static ServerSocket server;
    //线程池,处理每个客户端的请求
    private static ExecutorService executorService
            = Executors.newFixedThreadPool(5);
​
    private static void start() throws IOException{
​
        try{
            //通过构造函数创建ServerSocket
            //如果端口合法且空闲,服务端就监听成功
            server = new ServerSocket(DEFAULT_PORT);
            System.out.println("服务器已启动,端口号:" + DEFAULT_PORT);
            while(true){
​
                Socket socket= server.accept();
                System.out.println("有新的客户端连接----" );
                //当有新的客户端接入时,打包成一个任务,投入线程池
                executorService.execute(new BioServerHandler(socket));
            }
        }finally{
            if(server!=null){
                server.close();
            }
        }
    }
​
    public static void main(String[] args) throws IOException {
        start();
    }
​
}
​

BIO对于服务器来说,对于高并发的时候性能不是很让人满意的;

所以后面关于服务器的网络通讯人们就提出了NIO和AIO的模式;

一般BIO 在客户端编程里面会比较多,因为客户端不需要接收外来请求;

一般java开发是服务器程序;

原生JDK 网络编程AIO,

异步IO采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数;

在AIO中就两个核心类:

Asynchronous、

服务器端:最主要的工作就是接收客户端的连接,然后接收客户端发送的数据,然后把数据写给客户端;

在AIO模式里面当我调用之后我就不管了,然后由服务器处理好了以后就通知你;你开始找我要网络中的数据;中间的阻塞 不需要等待了

AIO 模式, 应用程序向操作系统发起一个注册,然后操作系统准备好了,通知你,你要的数据已经准备好了,什么时候操作系统准备好了 应用程序是不知道的;

在JDK 里提供了一个接口 CompletionHandler

completed(V result ,A attachment) 方法 处理数据

result 返回的结果

attachment 代表IO操作的时候附加的一个参数;

failed() 方法,当数据操作失败的时候你要怎么做? 你要告诉JDK 调用failed 这个方法;

AIO编程里面不管是 读,写,连接 都是异步操作;

BIO如果是服务器不要用BIO,如果是客户端可以使用BIO;

BIO是所有三种模型中编程最简单的应用;

服务器的话就是用AIO或者NIO, 但是linux 操作系统不支持AIO,

在linux 上编程,使用NIO;

经过测试得出。客户端和服务器端使用两种不同模式是可以连接上的

客户端方便我们开发,我们使用阻塞式IO,服务器端应对我们的高并发我们使用IO复用模型;

原生JDK网络编程- Buffer

重要属性 capacity : 表示我可以写的最大容量 position : 表示我已经写到了这个位置 limit :表示这个数组能写的范围,动态调节阀无论你怎么写 你都只能写到limit的位置

Buffer-Read

表示就是只能读到我写的范围,只能是position到limit的位置;

buffer.flip() 方法就是把写模式转化为读模式

Buffer 就可以理解为放在内存里面的一个数组

我们用的最多的是ByteBuffer,因为在网络上传输都是字节传输,

Buffer 是直接在堆上分配,和直接内存上分配;

原生JDK网络编程- NIO之Reactor模式

NIO 也被称为反应器,和spring的依赖倒转 有点像;

我们调用某个东西,一般是我们主动去发起调用,

但是在反应堆模式里面,我们一个具体的事件处理程序,他不是调用反应器,而是在反应器注册一个事件处理器,我们表示对某些事件感兴趣,有事件来了,应用程序通过事件处理器对某一个事件发生反应;这种控制逆转也被称为好莱坞法则;相当于你把你的名片给我,我有戏了我找你,你不要来找我;这也被称为反应器模式;

可以分为:

单线程反应器,

多线程反应器;

有一个主反应线器程和一个子的反应器线程

其他的工作线程和单线程一样;

在Netty 是用多线程反应器线程模式;

原生JDK网络编程- NIO

Buffer 最终是使用来和NIO使用的

重要概念

Selector

Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行。

事件订阅和Channel管理:

应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。

Channel : 他是一个抽象,是一个通道,是我们应用程序和操作系统交换事件传递内容的聚到;

意味着我们的应用程序可以向操作系统读数据,也可以向操作系统写数据; 当然写和读都是通过Buffer;

服务器端有一个:ServerScoketChannel

客户端有一个:SokcetChannel

还有一个数据包也就是UDP通道: DatagramChannel 通道

通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据。

· 所有被Selector(选择器)注册的通道,只能是继承了SelectableChannel类的子类。

· ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。

· ScoketChannel:TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口 到 服务器IP:端口的通信连接。

· DatagramChannel:UDP 数据报文的监听通道。

通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

操作类型SelecttionKey

JAVA NIO共定义了四种操作类型:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT(定义在SelectionKey中),分别对应读、写、请求连接、接受连接等网络Socket操作。ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时OS会通知channel,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器SocketChannel指由服务器ServerSocketChannel.accept()返回的对象。

OP_READOP_WRITEOP_CONNECTOP_ACCEPT
服务器ServerSocketChannelNNN*Y*
服务器SocketChannel*Y**Y*NN
客户端SocketChannel*Y**Y**Y*N

服务器启动ServerSocketChannel,关注OP_ACCEPT事件。

客户端启动SocketChannel,连接服务器,关注OP_CONNECT事件。

服务器接受连接,启动一个服务器的SocketChannel,这个SocketChannel可以关注OP_READ、OP_WRITE事件,一般连接建立后会直接关注OP_READ事件。

客户端这边的客户端SocketChannel发现连接建立后,可以关注OP_READ、OP_WRITE事件,一般是需要客户端需要发送数据了才关注OP_READ事件。

连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、OP_WRITE事件。

我们可以看看每个操作类型的就绪条件。

*操作类型**就绪条件及说明*
OP_READ当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪费CPU。
OP_WRITE当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不断就绪浪费CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很可能满,注册该操作类型就很有必要,同时注意写完后取消注册。
OP_CONNECT当SocketChannel.connect()请求连接成功后就绪。该操作只给客户端使用。
OP_ACCEPT当接收到一个客户端连接请求时就绪。该操作只给服务器使用。

NIO代码实现

客户端代码

public class NioClient {
​
    private static NioClientHandle nioClientHandle;
​
    public static void start(){
        if(nioClientHandle !=null)
            nioClientHandle.stop();
        nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT);
        new Thread(nioClientHandle,"Client").start();
    }
    //向服务器发送消息
    public static boolean sendMsg(String msg) throws Exception{
        nioClientHandle.sendMsg(msg);
        return true;
    }
    public static void main(String[] args) throws Exception {
        start();
        Scanner scanner = new Scanner(System.in);
        while(NioClient.sendMsg(scanner.next()));
​
    }
​
}
​
​
​

客户端处理器

public class NioClientHandle implements Runnable{
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
​
    private volatile boolean started;
​
    public NioClientHandle(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            //创建选择器
            selector = Selector.open();
            //打开通道
            socketChannel = SocketChannel.open();
            //如果为 true,则此通道将被置于阻塞模式;
            // 如果为 false,则此通道将被置于非阻塞模式 检测是否为阻塞模式
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
​
    }
    public void stop(){
        started = false;
    }
    @Override
    public void run() {
        /**
         * 在run方法里面 对事件使用选择器,
         * 拿到事件之后一一的做处理, 放到handleInput() 里面就我关注这个事件一一做相关的处理
         *还有发送消息是在主程序里面发
         *
         */
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        //循环遍历selector
        while(started){
            try {
                //阻塞,只有当至少一个注册的事件发生的时候才会继续
                selector.select();
                //获取当前有哪些事件可以使用
                Set<SelectionKey> keys = selector.selectedKeys();
                //转换为迭代器
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()){
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (IOException e) {
                        e.printStackTrace();
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
​
        //selector关闭后会自动释放里面管理的资源
        if(selector!=null){
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
​
    }
​
    //具体的事件处理方法
    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            //获得关心当前事件的channel
            SocketChannel sc = (SocketChannel)key.channel();
            if(key.isConnectable()){//连接事件 如果是他关心的事件
                if(sc.finishConnect()){}
                else{System.exit(1);}
            }
            //有数据可读事件 连接完成了就需要读消息
            if(key.isReadable()){// 判断写事件;
                //创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                //读取到字节,对字节进行编解码
                if(readBytes>0){ //实际读到的数据
                    //将缓冲区当前的limit设置为position,position=0,
                    // 用于后续对缓冲区的读取操作
                    buffer.flip();
                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String result = new String(bytes,"UTF-8");
                    System.out.println("accept message:"+result);
                }else if(readBytes<0){// 小于0 表示链路已经关闭了;
                    key.cancel();
                    sc.close();
                }
            }
        }
    }
​
    //发送消息
    private void doWrite(SocketChannel channel,String request)
            throws IOException {
        //将消息编码为字节数组
        byte[] bytes = request.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
    }
​
    private void doConnect() throws IOException {
        /*如果此通道处于非阻塞模式,
        则调用此方法将启动非阻塞连接操作。
        如果立即建立连接,就像本地连接可能发生的那样,则此方法返回true。
        否则,此方法返回false,
        稍后必须通过调用finishConnect方法完成连接操作。*/
        if(socketChannel.connect(new InetSocketAddress(host,port))){}
        else{
            //连接还未完成,所以注册连接就绪事件,向selector表示关注这个事件
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }
​
    //写数据对外暴露的API
    public void sendMsg(String msg) throws Exception{
        socketChannel.register(selector,SelectionKey.OP_READ);
        doWrite(socketChannel,msg);
    }
}
​
​

服务器端代码

/**
 * 类说明:nio通信服务端
 */
public class NioServer {
​
    private static NioServerHandle nioServerHandle;
​
    public static void start(){
        if(nioServerHandle !=null)
            nioServerHandle.stop();
        nioServerHandle = new NioServerHandle(DEFAULT_PORT);
        new Thread(nioServerHandle,"Server").start();
    }
    public static void main(String[] args){
        start();
    }
​
}
​
​

服务器端处理器

​
/**
 * 类说明:nio通信服务端处理器
 */
public class NioServerHandle implements Runnable{
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;
    /**
     * 构造方法
     * @param port 指定要监听的端口号
     */
    public NioServerHandle(int port) {
​
        try {
            selector = Selector.open();
            serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            // 指明服务器在哪个端口监听
            serverChannel.socket().bind(new InetSocketAddress(port));
            serverChannel.register(selector,SelectionKey.OP_ACCEPT);
            started = true;
            System.out.println("服务器已启动,端口号:"+port);
        } catch (IOException e) {
            e.printStackTrace();
        }
​
    }
    public void stop(){
        started = false;
    }
    @Override
    public void run() {
        //循环遍历selector
        while(started){
            try{
                //阻塞,只有当至少一个注册的事件发生的时候才会继续.
                selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()){
                    key = it.next();
                    it.remove();
                    try{
                        handleInput(key);
                    }catch(Exception e){
                        if(key != null){
                            key.cancel();
                            if(key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch(Throwable t){
                t.printStackTrace();
            }
        }
        //selector关闭后会自动释放里面管理的资源
        if(selector != null)
            try{
                selector.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
    }
    private void handleInput(SelectionKey key) throws IOException{
        /**
         * 这里关注的不在是客户端的连接事件,
         * 而是关注客户端的接收连接的事件
         * 需要新起一个socker
         */
        if(key.isValid()){
            //处理新接入的请求消息
            if(key.isAcceptable()){
                //获得关心当前事件的channel
                ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                //通过ServerSocketChannel的accept创建SocketChannel实例
                //完成该操作意味着完成TCP三次握手,TCP物理链路正式建立 。接收连接事件
                SocketChannel sc = ssc.accept();
                System.out.println("======socket channel 建立连接" );
                //设置为非阻塞的
                sc.configureBlocking(false);
                //连接已经完成了,可以开始关心读事件了
                sc.register(selector,SelectionKey.OP_READ);
            }
            //读消息
            if(key.isReadable()){
                System.out.println("======socket channel 数据准备完成," +
                        "可以去读==读取=======");
                SocketChannel sc = (SocketChannel) key.channel();
                //创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                //读取到字节,对字节进行编解码
                if(readBytes>0){
                    //将缓冲区当前的limit设置为position=0,
                    // 用于后续对缓冲区的读取操作
                    buffer.flip();
                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes,"UTF-8");
                    System.out.println("服务器收到消息:" + message);
                    //处理数据
                    String result = response(message) ;
                    //发送应答消息
                    doWrite(sc,result);
                }
                //链路已经关闭,释放资源
                else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }
            }
​
        }
    }
    //发送应答消息
    private void doWrite(SocketChannel channel,String response)
            throws IOException {
        //将消息编码为字节数组
        byte[] bytes = response.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
    }
​
}
​
​

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值