网络通信概述到Socket网络编程及NIO通信编程模式

目录

 

1. 网络通信的分层

2. 网络通信三要素

2.1 协议:

2.2 IP地址:

2.3 端口号

3. HTTP请求消息和响应消息

4. TCP通信(Socket网络编程)

4.1 套接字(Socket):

4.2 TCP/IP协议的特点

4.3 TCP协议的开发

4.4 文件上传综合案例

4.5 BS-浏览器-服务器通信模拟

5. 通信中的编程模型

5.1 通信中的编程模型

5.2 NIO三大核心原理

5.2.1 Buffer缓冲区

5.2.2 Channel通道

5.2.3 Selector选择器

5.3 NIO入门实例


行到水穷处,坐看云起时。

码字不易,喜欢就点个关注❤,持续更新技术内容。

1. 网络通信的分层

按照TCP/IP模型分四个层次:

  1. 应用层:qq微信浏览器,用到的协议(HTTP、FTP、SMTP)

  2. 传输层:TCP/IP-UDP协议,,

  3. 网络层:IP协议,

  4. 数据链路层:进入到硬件中

2. 网络通信三要素

2.1 协议:

网络协议是计算机网络客户端与服务端通信必须事先约定和彼此遵守的通信规则,如TCP、HTTP、UDP等等。

  1. UDP(User Datagram Protocol):位于传输层,面向无连接的不可靠的传输协议,直接发消息,不管对方是否在线,发消息后也不需要确认,性能号好,但会丢失一些数据,如即时性视频会议、实时游戏。了解即可。

  2. TCP(Transmission Control Protocol):位于传输层,面向连接的安全可靠的传输控制协议。在通信之前必须确定对方在线并且连接成功才可以通信,如下载文件、浏览网页等可靠传输。是建立在IP协议之上的传输层协议(包含IP协议),TCP通信协议的过程涉及建立连接(三次握手)、数据传输和关闭连接(四次挥手)三个阶段,以及拥塞控制和流量控制机制。这些步骤和机制保证了TCP通信的可靠性、顺序性和稳定性。TCP协议利用IP协议提供的网络基础设施进行数据传输的控制。

  3. HTTP协议(Hypertext Transfer Protocol):位于应用层,HTTP协议是数据传输协议,建立在在TCP协议之上的应用层协议(包含TCP协议),用于在Web上进行数据传输。HTTP协议定义了客户端和服务器之间的消息格式和交互规则,支持请求和响应模型,可实现客户端向服务器请求获取资源、提交数据等操作。HTTP协议通过TCP协议传输HTTP请求和响应消息,客户端和服务器之间通过HTTP消息进行通信。

2.2 IP地址:

IP地址用来给一个网络中的计算机设备做标识唯一的编号,基于IP协议(Internet Protocol),IP协议位于网络层,封装自己的IP地址和对方的IP地址和端口,即用于互联网上的数据传输和路由选择。TCP负责提供面向连接的安全可靠的数据传输,而IP负责提供数据包的路由和寻址。

IPv4:32位,4字节。如192.168.67.87。由于IPv4地址空间有限,IPv6被设计为下一代IP协议

IPv6:128位,提供了更大的地址空间和其他改进,可以实现为所有设备分配IP。

局域网:公司内部使用

城域网

广域网(公网):可以在任何地方访问,全球公用。

ipconfig:查看本机IPz地址

ping:检查本机与某个IP指定的机器能否联通(ping空格地址)

2.3 端口号

端口号:可以唯一标识设备中的应用程序(进程),用两个字节表示的整数,取值范围0-65535,端口号冲突会导致当前程序启动失败,报端口占用异常。

端口号可以分为以下三种类型:

  1. 系统端口号(保留): 熟知端口是指范围在0到1023之间的端口号。这些端口号通常用于一些广泛使用的服务,例如HTTP(端口号80)、HTTPS(端口号443)、FTP(端口号21)、SSH(端口号22)等。熟知端口是由互联网号码分配机构(Internet Assigned Numbers Authority,简称IANA)进行管理和分配的。

  2. 注册端口: 注册端口是指范围在1024到49151之间的端口号。这些端口号通常用于一些应用程序或服务的自定义应用。虽然它们不像熟知端口那样被广泛使用,但可能被特定的应用程序或服务所需。注册端口的分配权也由IANA进行管理。

  3. 动态/私有端口: 动态/私有端口是指范围在49152到65535之间的端口号。这些端口号用于临时分配给客户端应用程序,以便与服务器进行通信。当客户端应用程序与服务器建立连接时,通常会从动态/私有端口范围中分配一个未使用的端口号。这些端口在连接关闭后可以被重新使用。

协议+IP地址+端口号的三元组合标识网络中的进程,进程间的通信就可以利用这个标识进行交互。

3. HTTP请求消息和响应消息

请求数据的格式分为三个内容:(请求行、消息头:请求头、消息体:请求体)

  • 请求行:包含http请求方式(GET)及请求资源的路径(URL)和http协议版本。

  • 请求头:从请求消息的第二行开始,包含十几条头字段,注明一些信息,格式为key:value的形式,比如Accept:(浏览器可以接收的资源类型)、User-Agent:(浏览器版本信息)、Cookie:(存储在用户浏览器中的会话标识符,用于保持会话信息的状态,如登录状态,购物车商品状态) 等等。

  • 请求体:与请求行和请求头隔一行空格,存放POST方法向web服务器发送的数据,GET请求没有消息体。

响应数据的格式也分为三部分:(状态行、消息头:响应头、消息体:响应体)

  • 响应行:第一行,其中HTTP/1.1表示协议版本,200表示响应状态码,OK表示状态码描述。

  • 响应头: 第二行开始,格式为key: value形式,比如Date:(响应日期时间)、Content-Type:(响应页面的文本格式以及编码)、Cache-Control:(控制缓存,指示客户端应如何缓存,例如Max-Age=300表示可以最多缓存300s)。

  • 响应体: 最后一部分,存放响应数据。

响应行中的状态码分为五类:

信息响应 (1xx):服务器收到请求,需要请求者继续执行操作
成功响应 (2xx):成功,操作被成功接收并处理
重定向消息 (3xx):重定向,客户端需要再发起一个请求以完成整个处理
客户端错误响应 (4xx):请求包含语法错误或无法完成请求
服务端错误响应 (5xx):服务器在处理请求的过程中发生了错误

以下是更详细的HTTP状态码列表:

状态码状态码英文名称中文描述
100Continue继续。客户端应继续其请求
101Switching Protocols切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
   
200OK请求成功。一般用于GET与POST请求
201Created已创建。成功请求并创建了新的资源
202Accepted已接受。已经接受请求,但未处理完成
203Non-Authoritative Information非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204No Content无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205Reset Content重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206Partial Content部分内容。服务器成功处理了部分GET请求
   
300Multiple Choices多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301Moved Permanently永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302Found临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303See Other查看其它地址。与301类似。使用GET和POST请求查看
304Not Modified未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305Use Proxy使用代理。所请求的资源必须通过代理访问
306Unused已经被废弃的HTTP状态码
307Temporary Redirect临时重定向。与302类似。使用GET请求重定向
   
400Bad Request客户端请求的语法错误,服务器无法理解
401Unauthorized请求要求用户的身份认证
402Payment Required保留,将来使用
403Forbidden服务器理解请求客户端的请求,但是拒绝执行此请求
404Not Found服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
405Method Not Allowed客户端请求中的方法被禁止
406Not Acceptable服务器无法根据客户端请求的内容特性完成请求
407Proxy Authentication Required请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408Request Time-out服务器等待客户端发送的请求时间过长,超时
409Conflict服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突
410Gone客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411Length Required服务器无法处理客户端发送的不带Content-Length的请求信息
412Precondition Failed客户端请求信息的先决条件错误
413Request Entity Too Large由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414Request-URI Too Large请求的URI过长(URI通常为网址),服务器无法处理
415Unsupported Media Type服务器无法处理请求附带的媒体格式
416Requested range not satisfiable客户端请求的范围无效
417Expectation Failed服务器无法满足Expect的请求头信息
   
500Internal Server Error服务器内部错误,无法完成请求
501Not Implemented服务器不支持请求的功能,无法完成请求
502Bad Gateway作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
503Service Unavailable由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504Gateway Time-out充当网关或代理的服务器,未及时从远端服务器获取请求
505HTTP Version not supported服务器不支持请求的HTTP协议的版本,无法完成处理

4. TCP通信(Socket网络编程)

4.1 套接字(Socket):

套接字就是网络通信的数据收发的出入接口,通过套接字相互建立数据管道,执行数据的收发功能,可以理解为收发集装箱的马头、收发信件的邮局、收发包裹的快递站。套接字由操纵系统的Socket库中的socket程序组件创建。创建套接字在连接阶段之前,之后浏览器调用connect函数用来建立连接两台计算机之间的数据管道,通过这个出入口进行数据的收发。

总之,要TCP协议的三次握手建立连接需要先创建套接字作为收发数据的基础。客户端和服务器分别创建套接字,用于发送和接收握手消息,以建立最终的连接。这些套接字在握手过程中扮演了重要的角色,作为连接的双方端点,用于交换控制信息。

拓展,当两台计算机要建立数据通道的连接时,服务器会先创建套接字等待客户端(浏览器)创建套接字建立,当浏览器调用Socket库中的socket程序组件创建套接字后,调用connect函数指定描述符、服务器IP地址和端口号委托给协议栈发送建立连接请求(TCP协议中是通过三次握手建立连接)。这里的端口一般指的是动态/私有端口。描述符(通过它来识别应用程序使用哪个套接字收发数据,除此还可以用于标识计算机内部各种资源)。另外套接字之间是通过端口来识别对方的,与描述符加以区分,描述符是在文件系统和I/O操作级别的概念,而端口号是在网络通信层级上的概念。

TCP通信也叫Socket网络编程,只要代码基于Socket开发,底层就是基于了可靠传输的TCP通信。

4.2 TCP/IP协议的特点

  • 面向连接的协议

  • 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据。

  • 通过三次握手建立连接,连接成功形成数据传输通道。

  • 通过四次挥手断开连接

  • 基于IO流进行数据传输

  • 传输数据大小没有限制

  • 因为面向连接的协议,速度慢,但是是可靠的协议。

4.3 TCP协议的开发

  • 文件上传和下载

  • 邮件发送和接收

  • 远程登录

TCP协议相关的类

  • Socket一个该类的对象就代表一个客户端程序。

  • ServerSocket一个该类的对象就代表一个服务器端程序。

Socket类:

  • 构造器:

    Socket(String host, int port):创建一个流套接字,并将其连接到指定IP地址上的指定端口号。

  • 方法:

    OutputStream getOutputStream(); 获得字节输出流对象,包含在套接字建立的数据通信管道中的网络IO流

    InputStream getInputStream();获得字节输入流对象

  • 注意事项:

    只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。 如果连接成功,则表示三次握手通过。

ServerSocket类:

  • 构造器:public ServerSocket(int port):创建服务器套接字并将其绑定到指定的本地端口号。

  • 方法:

    public Socket accept():等待接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象。

客户端的开发流程:

1.客户端要请求于服务端的socket管道连接。 2.从socket通信管道中得到一个字节输出流 3.通过字节输出流给服务端写出数据。

服务端的开发流程:

1.注册端口。 2.接收客户端的Socket管道连接。 3.从socket通信管道中得到一个字节输入流。 4.从字节输入流中读取客户端发来的数据。

4.4 文件上传综合案例

目标:实现多用户(客户端)上传图片给服务端保存起来。

客户端:将本地文件上传到服务器 服务器:服务器不停止,能接收多个客户端上传的文件,并在上传完成后反馈给客户端

技术方案:循环+多线程(线程池),让服务器可以不断并发接收多个用户上传的文件,让每次文件上传之间不受"影响"。

首先是客户端的代码编写:

public class Client {
    public static void main(String[] args) throws IOException {
        /**
         * 客户端(浏览器):将本地文件上传到服务器
         * 服务器:接收上传的文件,并在上传完成后反馈给客户端
         * 循环+多线程,让服务器可以不断并发接收多个用户上传的文件,让每次文件上传之间不受"影响"
         */
        
        //1. 创建Socket对象,并连接服务器
        Socket socket = new Socket("127.0.0.1",7777);
​
        //2. 读取本地文件中的数据,用缓冲流包装原始流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("clientdir/yuli.png"));
        //3. 从socket套接字对象中获取网络输出流,用缓冲流包装原始流,将文件写到服务器中
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);//向socket套接字建立的数据通信管道(流)中写入数据到服务端
        }
​
        //写出本次输出到服务端的结束标记
        socket.shutdownOutput();
​
        //4. 接收回写的反馈信息
        //回显数据中包含中文,用转换流将原始字节输入流转换为字符输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = br.readLine();//回显数据只返回一行,可以用readline()方法只读取一行,不用结束标记,方便简洁
        System.out.println(line);
​
        //5. 释放资源,流包含在socket中
        socket.close();//套接字关闭数据通信管道,一方关闭两边断开。套接字未被销毁,只是关闭这个套接字将关闭套接字的输入流和输出流。如果此套接字有关联的通道,则该通道也将关闭。
//        socket = null;//可以置为null,以便垃圾回收器可以回收
    }
}

服务端代码编写:

public class Server {
    public static void main(String[] args) throws IOException {
        /**
         * 客户端(浏览器):将本地文件上传到服务器
         * 服务器:接收上传的文件,并在上传完成后反馈给客户端
         * 循环+多线程,让服务器可以不断并发接收多个用户上传的文件,让多个用户之间的上传不受"影响"
         */
​
        //1.创建对象并绑定对应客户端的端口
​
        //频繁创建线程并销毁非常浪费系统资源,用线程池优化
        //创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//1.核心线程数量
                16,//2.线程池总大小
                60,//3.空闲时间
                TimeUnit.SECONDS,//4.空闲时间单位
                new ArrayBlockingQueue<>(2),//5.阻塞队列
                Executors.defaultThreadFactory(),//6.线程工厂,让线程池如何创建线程对象
                new ThreadPoolExecutor.AbortPolicy()//7.任务拒绝策略抛出异常
        );
​
        ServerSocket ss = new ServerSocket(7777);
        //不关闭套接字,循环等待客户端来连接
        while (true) {
            //2.等待客户端连接,暂时没有客户来连接就会一直死等
            Socket socket = ss.accept();
​
            //3.开启一条线程,一个用户对应服务端的一条线程
            //new Thread(new MyRunnable(socket)).start();//不用每次请求都重新创建一个线程进行处理
            pool.submit(new MyRunnable(socket));//将连接到的Socket对象通过构造方法传给线程
        }
    }
}

在线程中的接收连接客户端发送过来的数据并保存:

public class MyRunnable implements Runnable{
​
    Socket socket;
    public MyRunnable(Socket socket) {
        this.socket = socket;
    }
​
    @Override
    public void run() {
        try {
            //1.读取数据并保存到本地文件中
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            String name = UUID.randomUUID().toString().replace("-", "");//生成随机且唯一的uuid标识符
            //用uuid命名上传到服务器的文件
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("serverdir/"+name+".png"));
            int len;
            byte[] bytes = new byte[1024];
            /**
             * 注意,read()方法会从数据连接通道中不断循环读取数据,读完数据之后仍然等待继续读取,需要一个结束标记才能停止循环读取
             */
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
​
            //2.回写数据
            //反馈信息给客户端,服务端为发送端,客户端为接收端
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("上传成功");
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket != null){
                //3.释放资源
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

成功上传到服务端文件夹下:

430b370b10e233bf12c96c1c84685cbe.png

 

 

4.5 BS-浏览器-服务器通信模拟

之前客户端和服务端都需要自己开发。也就是CS架构。接下来模拟一下BS架构。

目标:在浏览器中请求本程序,响应一个网页文字给浏览器显示。

客户端:浏览器通过路由发送请求,不需要我们开发。

服务端:响应消息数据给浏览器显示。

只需要注册服务端的端口,然后等待浏览器访问:

public class BSDemo {
    public static void main(String[] args) {
        try {
            // 1.注册端口
            ServerSocket ss = new ServerSocket(8080);
            // 2.创建一个循环接收多个客户端的请求。
            while(true){
                Socket socket = ss.accept();
                // 3.交给一个独立的线程来处理!
                new ServerThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

线程中的执行逻辑代码实现:

class ServerThread extends Thread{
    private Socket socket;
    public ServerThread(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        //因为文本类型传输限制,html页面和图片不能同时响应
        responseHtml();//响应html示例
//        responseImage();//响应图片示例
    }
​
    private void responseHtml(){
        try {
            // 响应消息数据给浏览器显示。
            // 浏览器是基于HTTP协议通信!响应格式必须满足HTTP协议数据格式的要求,浏览器
            // 才能够识别,否则响应消息浏览器根本不认识。
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println("HTTP/1.1 200 OK"); // 响应数据的响应头数据!
            ps.println("Content-Type:text/html;charset=UTF-8");//响应数据的类型。网页或者文本内容!
            ps.println(); // 状态行和消息头结束之后需要换行,下面是消息体,响应真实的数据
            
            ps.println("<span style='color:red;font-size:100px;'>SF90<span>"+"\n");
            
            ps.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void responseImage() {
        try {
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println("HTTP/1.1 200 OK"); // 响应数据的响应头数据!
            ps.println("Content-Type:image/jpeg\r\n");//响应数据的类型。网页或者文本内容!状态行和消息头结束之后需要换行,下面是消息体
​
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src/sf.jpg"));
            BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
            int len;
            byte[] buffer = new byte[1024];
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
​
            ps.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

响应html示例:

a8db1e7d99e7bbd3c658576e8e7ff27c.png

 

响应图片示例:

61854520d1a441259436bd30f9b1ffb5.png

 

5. 通信中的编程模型

5.1 通信中的编程模型

通信中的处理任务和IO操作的方式有同步阻塞和异步非阻塞。典型的同步阻塞例子是阻塞式IO,如上面的socket编程中的阻塞式读写操作;典型的异步非阻塞例子是异步IO,在网络编程中常用的异步框架如Node.js、Netty等。

同步阻塞和异步非阻塞是两种不同的编程模型,它们在处理任务和IO操作时的方式和行为有所不同。

-- 同步:当前线程要自己进行数据的读写操作。 -- 异步: 当前线程可以去做其他事情。 -- 阻塞: 在客户端套接字通道中数据没有的情况下,还是要继续等待着读。 -- 非阻塞:在客户端套接字通道中数据没有的情况下,会去做其他事情,一旦有了数据再来获取。

下面是它们的区别和适用场景:

同步阻塞(Synchronous Blocking):

  1. 在同步阻塞模型中,当一个任务或IO操作发起时,程序将立即阻塞(暂停向下执行)。在阻塞期间,线程将一直等待,无法执行其他任务,直到I/O操作完成并返回结果才能继续执行后续代码。一个连接一个线程。

  2. 典型的同步阻塞例子是阻塞式IO,如传统的socket编程中的阻塞式读写操作。

  3. 同步阻塞模型简单直观,但在高并发或IO密集型的场景下,阻塞会导致资源浪费和性能问题,对服务器资源要求比较高。

同步非阻塞(Synchronous Non-blocking):

  1. 在同步非阻塞模型中,当一个任务或IO操作发起时,程序不会暂停向下执行(阻塞)。线程不用等待其任务或IO操作的完成,以轮询的方式检查任务或IO操作的完成状态。如多路复用器(选择器)轮询客户端套接字通道,当有通道发来了数据才会开启线程处理。

  2. 适用于多任务轮询的情况,连接数目多(对多个数据源进行IO操作)且连接比较短(轻操作)的架构,如聊天服务器。编程比较复杂。

异步非阻塞(Asynchronous Non-blocking):

  1. 在异步非阻塞模型中,当一个任务或IO操作发起时,程序会继续执行后续代码,而不会等待操作的完成。任务或IO操作完成后会通过回调函数或事件处理机制来处理操作的完成和结果,以避免轮询等待。

  2. 典型的异步非阻塞例子是异步IO,在网络编程中常用的异步框架如Node.js、Netty等。

  3. 异步非阻塞模型适用于高并发、IO密集型的场景,可以更有效地利用系统资源和提高系统的吞吐量。如适用于连接数目多且连接比较长(重操作)的架构,适用于文件读写、消息队列,如相册服务器。但是编程比较复杂。

同步阻塞和同步非阻塞模型都需要调用方主动查询任务的完成状态,而异步非阻塞模型则通过回调或者事件通知来传递任务完成的信息。

同步阻塞式IO(BIO)同步非阻塞式IO(NIO)异步非阻塞(AIO)
阻塞IO(Blocking IO)非阻塞(Non Blocking)非阻塞(Non Blocking)
面向流(Stream)面向缓冲区(Buffer)异步任务调度器(Event Loop)
 选择器(Selector)回调函数(Callbacks)

选择使用同步阻塞或异步非阻塞模型,取决于应用程序的需求和性能目标。某些场景下,同步阻塞的简单性可能更加适合;而在高并发、需要快速响应的情况下,异步非阻塞模型可能更合适。

所以通信中的处理任务和IO操作的方式有同步阻塞和异步非阻塞。同步阻塞是传统的编程方式,在执行任务或IO操作时会阻塞程序的执行,而异步非阻塞则是通过回调函数或事件处理机制来实现任务或IO操作的异步执行,避免了阻塞和资源浪费。选择适当的模型取决于具体的应用场景和需求。如果需要高并发、高性能和资源利用效率,异步非阻塞通常是更好的选择,当然实现和编写异步代码可能会更加复杂和困难。

5.2 NIO三大核心原理

NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器),三者关系如示意图:

4d2459ee9f3cb8d6f132807c3494765d.png

 

5.2.1 Buffer缓冲区

缓冲区本质上是一块可写入数据,可读取数据的内存。这块内存被包装成NIO Buffer对象,并提供一组方法,以方便访问该块内存。相比对数组的操作,Buffer NIO更加容易操作和管理。不同类型缓冲区对应不同Buffer抽象类的子类对象。

  • 容量(capacity):作为内存块Buffer具有一定大小,称为容量,创建后不能更改。

  • 限制(limit):缓冲区限制的位置,限制开始之后的数据不能进行读写。

  • 位置(position):下一个可以读取或写入的数据的索引位置。

  • 位置标记(mark)和重置(reset):Buffer中的mark()方法指定特定的position,之后可以调用reset()方法恢复到这个位置进行读写。

0 <= mark <= position <= limit <= capacity

5.2.2 Channel通道

Java NIO的通道类似于BIO流,但不同的是:NIO通道可以从缓冲区中获取数据,也可以写数据到缓冲,而流的读写(输入和输出)通常是单向的。可以通过通道非阻塞读取和写入缓冲区,也支持异步读写数据。

常用的Channel实现类:

  • FileChannel:读取和写入,映射和操作文件的通道

  • DatagramChannel:UDP协议中用于读写网络数据的通道

  • SocketChannel:TCP协议中用于读写网络数据的通道

  • ServerSocketChannel:监听新进来的TCP连接,为每个连接创建一个SocketChannel。(类似于ServerSocket和Socket)

5.2.3 Selector选择器

Selector是Java NIO非阻塞IO的核心组件,是Channel对象的多路复用器,也就是说可以轮询检查一个或多个客户端NIO通道中哪些已经准备好进行读取或者写入。这样,一个单独的线程可以管理多个通道,也就实现了用一个线程处理多个客户端连接,不用为每个连接请求都创建一个线程,不用去维护多个线程,从而管理网络连接,提高效率。承上启下

创建Selector选择器:通过调用Selector.open()方法创建一个Selecrtor。

Selecotor selector = Selector.open();

在服务端向选择器中注册通道:SelectableChannel.register(Selector sel, int ops)

//1.NIO的通信基于通道,所以在服务端打开准备连接客户端的通道,接收连接请求
ServerSocketChanne1 ssChanne1 = serversocketChanne1.open();
//2.切换非阻塞模式,通道可以为阻塞和非阻塞
ssChanne1.configureBlocking(false);
//3.绑定连接套接字端口,接收客户端连接
ssChanne1.bind(new InetSocketAddress(7777));
//4.获取选择器,因为所有的通道都是由选择器来管理
Selector selector = selector.open();
//5.将通道注册到选择器上,并且指定对通道的“监听接收事件",监听有没有客户端发来新的连接请求
ssChanne1.register(selector, selectionKey.OP_ ACCEPT);//读:OP_READ, 写: OP_WRITE,...

5.3 NIO入门实例

以下是完整详细的NIO入门示例:

服务端:

public class Server {
    public static void main(String[] args) throws Exception {
        //1. 获取服务端套接字通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //2. 切换为非阻塞式
        ssChannel.configureBlocking(false);
        //3. 绑定服务端套接字通道绑定到指定端口
        ssChannel.bind(new InetSocketAddress(7777));
        //4.用给定的选择器注册服务端套接字通道。开始监听此通道有没有接收到新的客户端套接字通道的连接请求的事件。有则注册到选择器上
        Selector selector = Selector.open();
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);//此时可能已经注册了一些客户端套接字通道,注意只是注册,并没有连接
        //5. 使用轮询选择器中已经注册好(就绪)的客户端套接字通道
        while (selector.select() > 0){
            //6. 获取所有已经注册事件的选择器的迭代器
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
​
            //7. 遍历事件
            while (iter.hasNext()) {
                //提取遍历的当前的这个事件
                SelectionKey sk = iter.next();
                //8. 判断此事件是接入事件还是读取事件
                if (sk.isAcceptable()) {
                    //9. 如果是接入事件,接受来自客户端的套接字通道对服务端套接字通道的连接。连接是阻塞的,有事件才建立套接字的连接,无事件等待轮询
                    SocketChannel sChannel = ssChannel.accept();
                    //10.这个接入连接的套接字通道也应该是非阻塞的
                    sChannel.configureBlocking(false);
                    //11. 将此客户端套接字通道注册到选择器
                    sChannel.register(selector,SelectionKey.OP_READ);
                }
                else if (sk.isReadable()) {
                    //12. 如果是读事件,接受来自客户端的套接字通道对服务端套接字通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    //13. 读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);//声明缓冲区对象并设置大小
                    int len;//记录每次读取到数组中的字节或字符数据量。
                    while ((len = sChannel.read(buf)) > 0) {
                        buf.flip();//翻转缓冲区,将限制设置为当前位置,然后将位置设置为零。即恢复为缓冲区的第一个位置
                        String str = new String(buf.array(), 0, len);//读多少转换多少
                        System.out.println(str);
​
                        buf.clear();//清楚缓冲区之前的数据
                    }
                }
                //完成当前轮询中套接字通道产生的事件请求后,移出事件,
                iter.remove();
​
            }
        }
    }
}

客户端:

public class Client {
    public static void main(String[] args) throws IOException {
        //1. 获取客户端套接字通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("localhost",7777));
        //2. 将通道切换为非阻塞式
        sChannel.configureBlocking(false);
        //3. 初始化缓冲区对象并分配大小
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //4. 发送数据给服务端
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入昵称:");
        String name = scanner.nextLine();
        while (true) {
            System.out.println("输入发送数据:");
            String data = scanner.nextLine();
            buf.put((name+":"+data).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
    }
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Maxlec

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值