阅读 21

网络IO学习笔记

  1. socket是什么?

    四元组,cip:cport + sip:sport

    唯一确定一组连接,服务端不需要为每个客户端连接都分配一个端口

    在linux上就是一个fd,可用netstat查看连接状态

  2. 建立连接的过程?

    三次握手建立连接 - 确保双方确认连接

    四次挥手断开连接 - 确保双方断开连接

  3. TCP的拥塞控制?

    通过滑动窗口控制,每次ack会返回一个可用窗口大小,别给我多发,多发我受不了。

  4. BIO中,new serveSocket(9080,20),accept(),read(),内核调了什么方法?

    socket = fd3,

    bind(fd3,8090),

    listen(fd3),

    accept(fd3, -> fd5,//blocking

    recv(fd5, //blocking 可通过多线程方式异步执行

    BIO是同步阻塞模型

    accept阻塞

    recv阻塞

  5. NIO

    同步非阻塞模型

    不阻塞,accept,recv

    jdk:new io

    os:noblocking io

    操作系统提供了不阻塞的accept,recv

    NIO代码:

    public class SocketNIO {
       
        //  what   why  how
        public static void main(String[] args) throws Exception {
       
            LinkedList<SocketChannel> clients = new LinkedList<>();
       
            ServerSocketChannel ss = ServerSocketChannel.open();  //服务端开启监听:接受客户端
            ss.bind(new InetSocketAddress(9090));
            ss.configureBlocking(false); //重点  OS  NONBLOCKING!!!  //只让接受客户端  不阻塞
       
            while (true) {
                //接受客户端的连接
                Thread.sleep(1000);
                SocketChannel client = ss.accept(); //不会阻塞,没有客户端连接,OS层面返回-1,java层面返回NULL,
                //accept  调用内核了:1,没有客户端连接进来,返回值?在BIO 的时候一直卡着,但是在NIO ,不卡着,返回-1,NULL
                //如果来客户端的连接,accept 返回的是这个客户端的fd  5,client  object
                //NONBLOCKING 就是代码能往下走了,只不过有不同的情况
       
                if (client == null) {
                 //   System.out.println("null.....");
                } else {
                    client.configureBlocking(false); //重点  socket(服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept 得到  连接的socket>,连接socket<连接后的数据读写使用的> )
                    int port = client.socket().getPort();
                    System.out.println("client..port: " + port);
                    clients.add(client);
                }
       
                ByteBuffer buffer = ByteBuffer.allocateDirect(4096);  //可以在堆里   堆外
       
                //遍历已经链接进来的客户端能不能读写数据
                for (SocketChannel c : clients) {   //串行化!!!!  多线程!!
                    int num = c.read(buffer);  // >0  -1  0   //不会阻塞
                    if (num > 0) {
                        buffer.flip();
                        byte[] aaa = new byte[buffer.limit()];
                        buffer.get(aaa);
       
                        String b = new String(aaa);
                        System.out.println(c.socket().getPort() + " : " + b);
                        buffer.clear();
                    }
                }
            }
        }
       
    }
    复制代码

    挨个遍历accept的client,去R/W,每次遍历涉及到系统调用

    问题:遍历成本在用户态和内核态的切换

  6. c10k问题

    每循环一次,O(n)的复杂度 -> recv调用,实际并不是每一个连接都有读事件发送,很多系统调用是无意义的,浪费系统资源

  7. 阻塞,非阻塞,同步,异步

    阻塞:BLOCKING

    非阻塞:NOBLOCKING

    同步:应用程序自己完成:R/W

    异步:kernel帮你完成:R/W,程序无感知,传入一个buffer,然后读buffer,不需要调用read

  8. IO多路复用实现 (同步非阻塞)

    select(1024 fds)

    poll(没有1024 fds限制)

    触发了一次系统调用,select方法把所有IO连接遍历一边,返回一个int值,表示有多少个IO事件发生。

    问题:每次调用select函数,需要重传fds;都需要遍历全量的fds的一个复杂度

    如何解决?在内核中开辟一块控件存fds,就不需要每次都传递了,直接将有IO事件发生的fds返回,就不需要遍历了。epoll来了

    epoll

    epoll有3个核心函数(epoll_create -> fd3, epoll_ctl(fd3,ADD,fd5,EPOLLIN), epoll_wait -> 读取有事件的io)

    epoll_create:返回值是一个fd, 在内核开辟一个空间 epfd -> 红黑树,保存需要监听的fds,还会开辟一个链表空间,用于保存有事件发生的fds(如:accept,read)

    epoll_ctl 维护红黑树上监听的fd集合

    epoll_wait 获取所有事件的fds链表

    在epoll之前IO中断的callback,kernel只是完成了将网卡发来的数据,走内核网络协议栈(2-数据链路层,3-网络层,4-传输层)最终关联到fd的buffer,

    所以某一时间app询问内核是否有fd可R/W,会有状态返回。

    epoll出现之后,如果kernel支持epoll,则kernel会在将网卡发来的数据最终关联到fd的buffer后,延伸一个操作,在epoll_create创建的红黑树上找到对应的fd,然后copy这个fd到内核空间的一个链表中,epoll_wait直接取这个链表的fds,就可以获取到有IO事件发生的fds。这个操作都是由内核完成的。

    相比于select/poll,省去了遍历fds的过程,直接能返回有事件发生的fds。

    public class SocketMultiplexingSingleThreadv1 {
       
        //马老师的坦克 一 二期
        private ServerSocketChannel server = null;
        private Selector selector = null;   //linux 多路复用器(select poll    epoll kqueue) nginx  event{}
        int port = 9090;
       
        public void initServer() {
            try {
                server = ServerSocketChannel.open();
                server.configureBlocking(false);
                server.bind(new InetSocketAddress(port));
       
       
                //如果在epoll模型下,open--》  epoll_create -> fd3
                selector = Selector.open();  //  select  poll  *epoll  优先选择:epoll  但是可以 -D修正
       
                //server 约等于 listen状态的 fd4
                /*
                register
                如果:
                select,poll:jvm里开辟一个数组 fd4 放进去
                epoll:  epoll_ctl(fd3,ADD,fd4,EPOLLIN
                 */
                server.register(selector, SelectionKey.OP_ACCEPT);
       
       
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
       
        public void start() {
            initServer();
            System.out.println("服务器启动了。。。。。");
            try {
                while (true) {  //死循环
       
                    Set<SelectionKey> keys = selector.keys();
                    System.out.println(keys.size()+"   size");
       
       
                    //1,调用多路复用器(select,poll  or  epoll  (epoll_wait))
                    /*
                    select()是啥意思:
                    1,select,poll  其实  内核的select(fd4)  poll(fd4)
                    2,epoll:  其实 内核的 epoll_wait()
                    *, 参数可以带时间:没有时间,0  :  阻塞,有时间设置一个超时
                    selector.wakeup()  结果返回0
       
                    懒加载:
                    其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用
       
                     */
                    while (selector.select() > 0) {
                        Set<SelectionKey> selectionKeys = selector.selectedKeys();  //返回的有状态的fd集合
                        Iterator<SelectionKey> iter = selectionKeys.iterator();
                        //so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!!!!!!
                        //  NIO  自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
                        //幕兰,是不是很省力?
                        //我前边可以强调过,socket:  listen   通信 R/W
                        while (iter.hasNext()) {
                            SelectionKey key = iter.next();
                            iter.remove(); //set  不移除会重复循环处理
                            if (key.isAcceptable()) {
                                //看代码的时候,这里是重点,如果要去接受一个新的连接
                                //语义上,accept接受连接且返回新连接的FD对吧?
                                //那新的FD怎么办?
                                //select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
                                //epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
                                acceptHandler(key);
                            } else if (key.isReadable()) {
                                readHandler(key);  //连read 还有 write都处理了
                                //在当前线程,这个方法可能会阻塞  ,如果阻塞了十年,其他的IO早就没电了。。。
                                //所以,为什么提出了 IO THREADS
                                //redis  是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
                                //tomcat 8,9  异步的处理方式  IO  和   处理上  解耦
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
       
        public void acceptHandler(SelectionKey key) {
            try {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端  fd7
                client.configureBlocking(false);
       
                ByteBuffer buffer = ByteBuffer.allocate(8192);  //前边讲过了
       
                // 0.0  我类个去
                //你看,调用了register
                /*
                select,poll:jvm里开辟一个数组 fd7 放进去
                epoll:  epoll_ctl(fd3,ADD,fd7,EPOLLIN
                 */
                client.register(selector, SelectionKey.OP_READ, buffer);
                System.out.println("-------------------------------------------");
                System.out.println("新客户端:" + client.getRemoteAddress());
                System.out.println("-------------------------------------------");
       
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
       
        public void readHandler(SelectionKey key) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            int read = 0;
            try {
                while (true) {
                    read = client.read(buffer);
                    if (read > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            client.write(buffer);
                        }
                        buffer.clear();
                    } else if (read == 0) {
                        break;
                    } else {
                        client.close();
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
       
            }
        }
       
        public static void main(String[] args) {
            SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
            service.start();
        }
    }
    复制代码