Java Socket网络编程深入理解:OS->BIO->NIO->Netty

本文深入探讨了Java网络编程的演进历程,从操作系统基础知识出发,详细讲解了BIO、NIO以及NIO+多路复用器(如epoll)的原理和优缺点。最后介绍了Netty框架如何利用NIO+多路复用器实现高性能、高可靠的网络IO程序。
摘要由CSDN通过智能技术生成

Socket编程深入理解:OS->BIO->NIO->Netty

本文假设读者具有一定的socket编程基础并且已经了解并会使用基本的socket编程。本文旨在从我的角度梳理从BIO到NIO再到多路复用器等的发展过程。有兴趣的同学强烈推荐这个视频教程:https://link.zhihu.com/?target=https%3A//links.jianshu.com/go%3Fto%3Dhttps%253A%252F%252Fwww.bilibili.com%252Fvideo%252FBV1RV411r72B%253Fp%253D1
首先从操作系统讲起:

一、操作系统基本知识

在这里插入图片描述

上图是一个基本的计算机基本工作图,注意下列几点:

  • 计算机主要有处理器和内存两个部分,对于内存空间,计算机将它分为了内核空间和用户空间。内核空间是计算机指定的一块空间,这个空间只能由操作系统访问,用户程序是无法访问的,同时CPU中的一些特定的指令和寄存器也是只有操作系统可以访问的,这是第一点。
  • 一个CPU同一时间只能执行一个程序,要么操作系统、要么App进程。计算机通过时钟中断来达到计算机完成操作系统和各个用户程序间的切换,例如当前CPU正在执行APP1,当下一个中断到来后CPU会先将App1在CPU中的寄存器值写回App1内存空间,同时清空缓存,然后执行操作系统之前注册的回调函数进行进程调度,然后开始执行下一个App进程。如果内存中进程或线程非常多的话,那么这个分时时间片就非常小,系统就会进行这种反复的进程切换或线程切换,消耗资源。因此我们希望进程和线程尽可能少。
  • 内核程序即操作系统,不仅负责了用户程序的调度,同时也负责了与外部设备的交互,如网卡、鼠标等。如果用户程序需要与外设交互,则需要调用内核程序的功能,这叫做系统调用,和函数调用是不同的。系统调用会使CPU执行内核程序,用户程序使通过系统的软中断实现的,在中断中执行内核的程序,因此也需要CPU保护用户程序的现场数据,消耗资源。这表明App每触发一次系统调用,就会进行一次现场数据保护。
  • 值得注意的一点是:在linux系统中这个内核程序就对应了Linux系统,Linux系统并没用直接采用机器指令实现,而是通过C语言进行编程实现的。因此用户程序的系统调用实际调用的就是Linux中对应的C语言程序。

二、BIO编程

BIO又称Block IO,即阻塞IO编程。在jdk1.4之前JVM虚拟机只提供了BIO这种网络实现。

在jdk中,BIO的实现主要通过ServerSocket和Socket来实现的,以服务端ServerSocket为例:

  • 首先创建该对象,并且绑定监听端口, ServerSocket ss=new ServerSocket(9999); 对应系统调用:socket(…)=3;listen(3,50)
  • 然后最后开始等待客户端连接 Socket s = ss.accept(); 对应系统调用:accept(3, 此时注意,由于还没有客户端连接,因此系统调用方法会一直等待在这个地方,导致了阻塞,因此BIO是一种阻塞的IO实现。
  • 阻塞不仅体现在等待连接,同时如果连接上了系统通过 InputStream is = s.getInputStream();来读取二进制数据也是堵塞的,因为对应系统调用 recv(),该函数在没有数据过来是也会一直堵塞在这里直到有数据进来。

对于客户端而言,从Socket连接中读取服务器数据也是一种阻塞方法。

优点:

  • 显然,操作简单,理解容易。

缺点:

  • 首先由于BIO是阻塞的,因此对于服务器每建立一个连接就必须新开一个线程,这样当并发高的时候线程过多,就导致CPU大量的时间用在了线程切换上,浪费资源。
  • 效率低下,当客户端连接后没有发送数据的话,服务器的每个线程都会陷入一种空等待状态,占用整个进程资源。

三、NIO编程

基于上述BIO的确定,jdk在1.4之后引入了NIO编程,又叫New IO,即新的IO,这种IO提供了非阻塞和阻塞两种方式作为选择,一般采用非阻塞方式。注意,NIO的非阻塞其实产生的系统调用和BIO是一样的,但是在系统函数socket创建套接字文件描述符fd时存在一个参数供以选择,叫做sock_NONBLOCK,即在这个地方就可以选择该Socket调用是非阻塞还是阻塞的。因此,NIO在jdk层面体现为New IO,因为它是阻塞或非阻塞的,在操作系统的层面可以体现为非阻塞IO。

NIO特点如下:

  • 与BIO不同,NIO不采用byte数组作为数据传输渠道,而是采用一种新的容器Buffer(缓冲区),同时两个核心API也分别变成了SocketChannel和ServerSoketChannel,操作上其实和之前的BIO很相似

  • 相较于BIO,NIO非阻塞变成的最根本不同在于,为操作系统socket fd指定为非阻塞后,其accept方法和recv方法如果没能立刻建立连接或接收到数据它不会阻塞,而是返回一个-1或null,因此用户程序不需要阻塞在哪儿,而是可以干自己的事情,然后时不时来确定有没有连接或数据即可。在Java中通过channel.configureBlocking(false);指定是否阻塞。

  • 一个正常的NIO程序的服务器端编程流程如下:

    • 1、创建ServerSoketChannel对象,指定端口号和非阻塞属性
    • 2、建立while循环,在循环中首先通过系统调用accpet,如果返回非空,那么说明存在新的连接,获得客户端连接通道socketChannel,加入到通道集合中。
    • 3、对所有的客户端通道集合进行读取遍历,如果有数据则进行处理。
  • 从上诉可以看到,NIO是一种同步非阻塞的方法,同步和非同步的概念首次出现在多线程的sync中,在并发场景中,由于多个线程需要操作同一个共享资源,可能就会出现错误,因此每个线程都要相互之间获得对方的信息,借助该资源的锁机制让每个线程可以在使用该资源时先判断是否有其它线程正在使用该资源,达到串行化同步的效果,总结起来就是每个线程取用该资源时先判断该资源是否可用,可以用我在用。这也完美体现在NIO中,对于accept,我先判断是否有连接可用,可用我才用,不能我就等待着或者先去干其它事情,对应了BIO和NIO。因此BIO和NIO都是一种同步方法。

优点:

  • 非阻塞可以使用一个线程处理多个连接的消息,可以解决线程过多的问题。
  • 非阻塞不需要阻塞等待,白白消耗CPU资源,效率更高。

缺点:

  • 加入当前存在了10000个客户端连接,并且只有3个连接发送消息到服务器,但是NIO程序还是需要产生10000次recv的系统调用来进行遍历,也需要产生10000次线程保护,这样是及其消耗CPU资源的事情。
    • 解决办法:一次性将10000个客户端连接通道的fd给到内核,让内核自身进行遍历来返回结果。

四、NIO+多路复用器编程

针对单独使用NIO需要反复系统调用recv的缺点,采用的解决方案就是采用系统调用select一次性把所有fd丢给内核去调用,从而实现多路客户端连接服用同一次系统调用,因此叫做多路复用器。对于多路服用器,linux操作系统提供了三种解决方案,分别是select、poll 和epoll,其中select和poll基本是一类,只是select有限制一次遍历的大小而poll没有,但epoll是一种更好的实现方案,当前用的最多的也是epoll,很多框架都是优先选用epoll。

在这里插入图片描述

首先介绍select:

  • 在JDK层面,JDK封装提供了一个类叫做selector,作为监视器对象,以服务端为例,工作流程如下:

    • 服务端先创建channel通道,绑定端口,然后设置非组塞。 channel = ServerSocketChannel.open();
    • 然后创建selector选择器对象,在操作系统中对应了多路复用器对象select。 selector = Selector.open();
    • 然后channel需要不断的监听端口,接收客户端连接,这时channel在selector中注册自己需要监听的事件,通过语句channel.register(selector, SelectionKey.OP_ACCEPT);
    • 然后建立大循环,在循环中首先通过selector.select方法检测是否出现了对应的事件,如果出现了连接事件,则通过selector的selectionKeys获得对应的客户端通道,然后为该通道继续注册读监听事件,socket.register(selector,SelectionKey.OP_READ);监听每个通道的数据读取。
  • 上述步骤的逻辑很简单,假如JVM选择了操作系统中的select实现的话,当服务器channel注册监听连接事件或客户端通道注册监听读取事件时,JVM会开辟一片内存空间用于存储在多路复用器中组测的各种fd(socket+关注的事件)。然后当用户程序调用selector.select时会JVM会产生系统调用select将之前注册的所有fd丢给内核去遍历,然后如果某个fd出现事件,他会返回该fd给JVM。

  • 注意该select方法其实是一种阻塞方法,如果没有事件的话会一直阻塞,所以一般设置一个超时时间。selector.select(2000)==0

  • 最后需要注意的一点就是NIO+多路复用器其实也是一种同步的方法,因为复用器只会提供给你每个通道是否有连接或者可读的状态,还是需要自己去读取,即自己读取之前还是通过selector去判断是否可读的,可读在读,不可读就算了,因此是同步方法。

然后就是重点epoll:

首先在JDK的操作层面上两者的操作是没有区别的,但是在系统调用上是完全不同的。在上述select的解决方法中存在两个问题,

  • 每次系统select调用都需要传输所有的注册过的通道文件描述符fd
  • 每次内核都需要去完整的遍历所有的fds

因此epoll给出了另外一种解决方案,解决方法如下:

  • 如果采用epoll,在通道通过selector组测事件监听时,会产生系统调用epoll_ctl(),该函数在内核中开辟一个内存空间1,将对应通道的fd存放到内核空间中,从而方便之后的内核遍历,不需要直接传输。
  • 在内核中对于那些放进来的通道fds,如果出现了连接或数据读取等事件,内核会通过中断自动的将这些出现事件的fds转移到另一块内存空间2,因此当用户程序调用selector.select(2000)==0方法时,对应了epoll的epoll.wait(),该方法只需要在内存空间2中直接读取即可,基本实现了时间复杂度O(1),效率极高。

在这里插入图片描述

基于NIO+多路复用器的聊天程序

总结:其实对于多路复用器的操作就两三步:1、建立多路复用器selector 2、向selector注册事件 3、通过select遍历读取有事件的通道

服务器代码:

public class NioChatServer {
   
    ServerSocketChannel channel;
    Selector selector;
    public void start() throws IOException {
   
        //1、得到服务器套接字socket,
        // 调用内核函数得到系统给的socket文件描述符fd,即对象
        channel = ServerSocketChannel.open();
        //2、得到选择器对象用于中间监视,
        // 调用系统内核的select\poll\epoll函数,以epoll为例,调用epoll_create()函数为例,得到多路复用器对象,
        // 同时在内核中开辟一个空间用于存储在多路复用器中组测的各种fd(socket+关注的事件)
        selector = Selector.open(); //得到一个连接器
        //3、为socket绑定端口
        //调用内核函数bind绑定端口
        channel.bind(new InetSocketAddress(9999));
        //4、设置非阻塞
        //在内核函数中,accept提供了非阻塞的输入参数来指定,在这个地方配置了那么socket在调用accept方法的话就会调用非阻塞方式
        channel.configureBlocking(false);
        //5、为socket在多路复用器中注册对应事件
        // 如果采用epoll,在内核中对应了epoll_ctl()函数,会将socket的fd存放到内核空间中,方便内核遍历,
        // 如果用的select或者poll,JVM会自己开辟一块内存空间,然后把对应的 fd 放进去
        channel.register(selector, SelectionKey.OP_ACCEPT);
        //6、开始判断有没有事件到来
        while(true){
   
            //如果虚拟机采用复用器 select。则调用select方法,并传入所有所有注册过的fds,返回有状态的fd
            //如果采用epoll,则直接调用epoll.wait()方法进行读取,这是一种阻塞方法,可以设置超时事件,返回待操作fd的数目
            if(selector.select(2000)==0){
   //阻塞检测两秒
                System.out.println("服务器在干自己的事");
            }
            //7、得到所有事件的集合然后遍历
            //直接得到之前从内核中得到的所有fd,记得遍历完了要删除,不然虚拟机是不会帮忙删除的
            Set<SelectionKey> sele
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值