当前位置: 首页 > 工具软件 > Socket.IO C++ > 使用案例 >

网络IO Socket

姬裕
2023-12-01

1. 概念

网络IO的过程,就是操作系统接收到网卡的数据,缓存到一个buffer中,然后应用程序调用操作系统的函数,从对应的buffer中取出数据。

2. 常见IO模型

模拟客户端连接:

public static void main(String[] args) {

    try {

        Socket socket = new Socket("127.0.0.1", 8082);

        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

        dos.writeUTF("helloServer!");

        dos.flush();

        dos.close();

        socket.close();

    } catch (IOException e) {

        e.printStackTrace();

    }

}

2.1 BIO

无论是获取新的连接还是读取指定连接的数据,调用操作系统的函数都是阻塞的,如果要实现服务多个连接,就必须每个连接建立一个线程异步处理,否则,当建立起一个连接,但是客户端不发送数据,服务端就会被这个客户端占用,无法接受新的连接。

2.1.1模拟服务端连接

public static void main(String[] args) {

    try {

        ServerSocket serverSocket = new ServerSocket(8082);

        while(true){

            Socket socket = serverSocket.accept();//阻塞

           //服务端与客户端每建立一次连接就会开启一个线程处理

            new Thread(new Runnable() {

                @Override

                public void run() {

                    try {

                        DataInputStream dis = new DataInputStream(socket.getInputStream());//阻塞

                        System.out.println(dis.readUTF());

                        dis.close();

                        socket.close();

                    } catch (IOException e) {

                        e.printStackTrace();

                    }

                }

            }).start();

        }

    } catch (IOException e) {

        e.printStackTrace();

    }

}

2.1.2 优势

1.多线程,多连接,可以接收很多连接

2.1.3 劣势

  1. 线程内存消耗;
  2. cpu调度消耗;
  3. C10K问题;

2.2 NIO

解决了阻塞的问题,程序调用操作系统的函数,如果没有连接或数据,会立即返回,不会阻塞,避免了资源无效浪费。但是,它的问题在于,如果我有1万个连接,每次我需要挨个询问1万次,这个复杂度是O(n)的。每次询问都是一次系统调用,涉及到CPU的用户态内核态切换,成本很高。

2.2.1模拟服务端连接

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

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    serverSocketChannel.bind(new InetSocketAddress(8082));

    //OS false-NONBLOCKING,true-BLOCKING

    serverSocketChannel.configureBlocking(false);

    //客户端连接socket集合

    LinkedList<SocketChannel> clients = new LinkedList<>();

    while(true){

        Thread.sleep(1000);

        //serverSocketChannel.accept()不阻塞,linux底层没有客户端连接时返回-1,有客户端连接返回客户端的fd

        SocketChannel socketChannel = serverSocketChannel.accept();

        if(socketChannel == null){

            System.out.println("null...");

        }else{

            //OS false-NONBLOCKING,true-BLOCKING

            socketChannel.configureBlocking(false);

            System.out.println("clientport:" + socketChannel.socket().getPort());

            clients.add(socketChannel);

        }

        //遍历连接上的客户端socket读写数据

        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);

        clients.forEach(client->{

            try {

                int num = client.read(byteBuffer);

                if(num > 0){

                    byteBuffer.flip();

                    byte[] bytes = new byte[byteBuffer.limit()];

                    byteBuffer.get(bytes);

                    String result = new String(bytes);

                    System.out.println(result);

                    byteBuffer.clear();

                }

            } catch (IOException e) {

                e.printStackTrace();

            }

        });

    }

}

2.2.2优势

1.规避了C10K问题;

2.2.3劣势

1.存在很多无意义的系统调用,用户态和内核态切换消耗时间和资源;

2.3多路复用器

2.3.1 select

这是最初级的多路复用器,从NIO到多路复用器,其实就是一个从多次到批量的演进,多路复用器支持一次询问多个文件描述符(fd)(linux中,一切皆为文件,连接也是文件,有对应的文件描述符)。从多次到批量,就能节省大量的运行态切换成本。但是select的问题在于,批量有上限,是有限的批量。

2.3.2 poll

解决了select的上限问题,一次可以询问任意个数的fd,真正做到了批量。但是,即使减少了运行态切换的成本,针对每次传来的fd,操作系统依然需要逐个遍历,复杂度依然是O(n),只是每次操作的损耗降低了。

2.3.3 epoll

解决了POLLSelect存在的遍历问题,将复杂度降为O(1),操作系统提前维护好用户程序对应的fd,每次有数据到达,就把对应的fd放到一个数据结构中存起来,当用户程序需要读取数据时,直接把这些有状态的fd返回,用户程序一次性获取fd,逐个读取即可。用户只要调用一次,操作系统也不需要遍历。这是目前大部分场景下,最高效的模型。

2.3.4模拟服务端调用

//select poll epoll

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

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    serverSocketChannel.bind(new InetSocketAddress(8082));

    //OS false-NONBLOCKING,true-BLOCKING

    serverSocketChannel.configureBlocking(false);

    /*

    多路复用器 select poll epoll 优先选择:epoll 但是可以-D修正

    epoll: epoll_create -> fd7

     */

    Selector selector = Selector.open();

    /*

    channel注册到selector

    select,poll:jvm里开辟一个数组fd3放进去

    epoll:epoll_ctl(fd7,ADD,fd3,EPOLLIN

     */

    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

    while(true){

        /*

        调用多路复用器

        select,poll:内核的select(fd3) poll(fd3)

        epoll:内核的epoll_wait()

        阻塞,有时间设置一个超时

         */

        while(selector.select(500) > 0){

            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while(iterator.hasNext()){

                SelectionKey key = iterator.next();

                iterator.remove();//set 不移除会重复循环处理

                if(key.isAcceptable()){

                    /*

                    accept接受连接且返回新连接的fd,那新的fd怎么办?

                    select,poll:因为他们内核没有空间,那么jvm中保存和前边的fd3那个listen的一起

                    epoll:我们希望通过epoll_ctl把新的客户端fd注册到内核空间

                     */

                    System.out.println("a connection was accepted by a ServerSocketChannel.");

                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();

                    SocketChannel client = ssc.accept();//目的是调用accept接受客户端fd8

                    client.configureBlocking(false);

                    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8192);

                    /*

                    select,poll:jvm开辟一个数组fd8放进去

                    epoll:epoll_ctl(fd7,ADD,fd8,EPOLLIN

                     */

                    client.register(selector, SelectionKey.OP_READ, byteBuffer);

                    System.out.println("新客户端:" + client.getRemoteAddress());

                }else if(key.isConnectable()){

                    System.out.println("a connection was established with a remote server.");

                }else if(key.isReadable()){

                    System.out.println("a channel is ready for reading");

                    SocketChannel socketChannel = (SocketChannel) key.channel();

                    ByteBuffer byteBuffer = (ByteBuffer) key.attachment();

                    byteBuffer.clear();

                    int read = 0;

                    while(true){

                        //...

                    }

                }else if(key.isWritable()){

                    System.out.println("a channel is ready for writing");

                }

            }

        }

    }

}

2.4 AIO

以上由于IO还是需要应用程序自己读取,所以都属于同步IO模型,AIO是由操作系统内核来读取IO,再通知应用系统;

 类似资料: