网络IO(一)

286 阅读4分钟

前言

最近在系统的学习redis,redis的高性能除了基于内存、对于不同数据类型会根据数据量的多少而使用不同的数据结构……,还有redis使用了多路复用器epoll,除了redis外nginx、tomcat也都使用到了多路复用器。

在说多路复用器之前先从CPU中断说起

CPU中断

在Linux信号驱动的情况下,网卡接收到其他主机发来的数据,会将接收到的数据存储在内存中,然后向CPU发送一个信号,表示网络中有数据传输过来了需要处理,在计算机中硬件的信号优先级比用户程序的信号优先级高,硬件的信号都需要立即处理(否则可能会有丢数据的情况发生),CPU感应到了刺激,就会停止当前所执行的用户程序(假设是单核CPU),去执行硬件中断程序,执行完后再执行用户程序。

这也是用户态到内核态的切换过程,用户态:CPU在执行用户程序的代码;内核态:CPU在执行操作系统内核代码,比如:磁盘IO、声卡、显卡等操作……。

一个程序的运行必定是需要计算机硬件参与的。那么用户程序具体是怎么访问计算机硬件的呢?

在用户程序代码中会有像"int x80"这样的代码调用,这里的"int"就是interrupt中断的意思,CPU在执行到这行代码时,就会去中断向量表中查找"80"对应的系统调用函数并执行它,执行完后再继续执行用户程序代码。(系统调用函数:可以对操作系统内核执行一些动作的函数)。

hint:操作系统向下管理着计算机硬件。

BIO(Blocking IO)

代码

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


public class SocketBIO {

    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090);

        System.out.println("step1: new ServerSocket(9090)");

        while (true){

            Socket client = server.accept();//阻塞1

            System.out.println("step2: client port: " + client.getPort());

            new Thread(new Runnable() {

                @Override
                public void run() {
                    InputStream inputStream = null;
                    try {
                        inputStream = client.getInputStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                        while (true){
                            String s = reader.readLine();//阻塞2
                            System.out.println("step3: read: " + s);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            
        }
    }
}

在调用accept()和readLine()时都会发生线程阻塞,如果有一个客户端连接进来后,没有发送任何数据,当前线程就会阻塞在readLine() 不往下执行,所以就需要创建一个线程来执行readLine()。

在执行上面代码的过程中,会调用系统调用函数,可以将这个类放在Linux虚拟机上查看运行过程,来更好的理解。使用strace -ff -o out java SocketBIO启动程序并追踪程序运行时所有线程的系统调用,以out开头的文件在当前目录下输出 第一个136620大小的out.2300就是main线程的out文件,使用vi out.2300

可以看到在执行new ServerSocket(9090)时,调用了socketbindlisten系统调用函数。因为没有客户端连接进来,在执行server.accept()就发生了线程阻塞,系统调用也卡在了accept上,程序在等待客户端连接。

使用jpsnetstat -natp输出 也显示程序处于listen状态。使用nc localhost 9090连接程序监听的端口9090后,out.2300 accept接收到了fd(文件描述符为5的客户端),并使用clone函数创建了一个2386的线程,main线程继续调用accept等待客户端的连接。

接着查看新创建的线程的out.2386 阻塞在了recv上,等待数据发送过来

缺点

这样BIO的缺点也就显而易见了,每来一个客户端连接都要创建一个线程,需要消耗大量资源在线程切换上

每个新技术的产生都是为了解决上一个的缺点,所以NIO就产生了

hint:每连接一个客户端进来就会在内核中开辟一个socket空间

NIO(Non-Blocking IO)

顾名思义就是非阻塞IO

代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

public class SocketNIO {

    public static void main(String[] args) {
        LinkedList<SocketChannel> clients = new LinkedList<>();

        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();//服务端绑定端口,开启监听
            ssc.bind(new InetSocketAddress(9090));
            ssc.configureBlocking(false);//OS的 nonblocking 只有服务端监听非阻塞

            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SocketChannel client = ssc.accept();//接收客户端连接,不会阻塞,没有客户端连接 返回值 null 在linux中 -1。
                //如果来客户端的连接, accept返回的是这个客户端的fd
                // NONBLOCKING就是代码能往下走了,只不过有不同的情况

                if (client == null) {
                    System.out.println("client is null");
                }else {
                    client.configureBlocking(false);//nonblocking 读取客户端发送的数据非阻塞
                    int port = client.socket().getPort();
                    System.out.println("client port:" + port);
                    clients.add(client);
                }

                ByteBuffer buffer = ByteBuffer.allocateDirect(4096);

                //遍历已经连接进来的客户端能不能读写数据
                for (SocketChannel cli : clients) {
                    int read = cli.read(buffer);//不会阻塞,>0(有数据)-1 0 
                    if (read > 0) {
                        buffer.flip();
                        byte[] aaa = new byte[buffer.limit()];
                        buffer.get(aaa);
                        String b = new String(aaa);
                        System.out.println(cli.socket().getPort() + ":" + b);
                        buffer.clear();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用java SocketNIO启动程序,就会一直打印"client is null",继续使用strace -ff -o out java SocketNIO追踪程序中所有线程的系统调用,将日志输出到out开头的文件中 关键是fcntl

优点

规避了多线程的问题

缺点

如果连接了大量的客户端,就需要一直频繁的调用recv,一直频繁的在用户态和内核态间切换,消耗大量资源在切换上,那么为了避免频繁的用户态到内核态切换,能不能内核提供一些函数可以一次监听多个客户端呢,多路复用器就登场了

hint:recv对应一个客户端,即监听一个客户端