zoukankan      html  css  js  c++  java
  • IO学习笔记4

    二、 Socket编程

    常见的IO模型主要有以下分类:

    • 同步/异步
    • 阻塞/非阻塞

    这两个可以互相组合,如同步阻塞模型/同步非阻塞模型,但是没有异步阻塞模型。windows实现了异步模型,但是linux并没有实现,因此linux中的IO都是同步模型的。

    2.1 BIO

    BIO--即`BlockingIO,也叫同步阻塞IO。BIO的代码如下:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * @author shuai.zhao@going-link.com
     * @date 2021/6/1
     */
    public class SocketBIO {
        public static void main(String[] args) throws IOException {
            ServerSocket serverSocket = new ServerSocket(9000);
            System.out.println("step1: new ServerSocket(9000)");
    
            while (true) {
                Socket client = serverSocket.accept();
                System.out.println("client :" + client.getPort() + " is in");
                new Thread(new Runnable() {
                    public Socket client;
    
                    public Runnable setClient(Socket client) {
                        this.client = client;
                        return this;
                    }
                    
                    public void run() {
                        InputStream in = null;
                        try {
                            in = this.client.getInputStream();
                            BufferedReader br = new BufferedReader(new InputStreamReader(in));
                            while (true) {
                                String str = br.readLine();
                                if (str != null && !"".equals(str)) {
                                    System.out.println("str = " + str);
                                } else {
                                    break;
                                }
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }finally {
                            try {
                                in.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }.setClient(client)).start();
            }
        }
    }
    

    这就是一个最简单的BIO服务端,当服务启动后,使用nc localhost 9000连接当前服务端。

    zhaoshuai:io-study 乄 nc localhost 9000
    

    可以在控制台看到如下输出:

    client :52706 is in
    

    然后在终端随便输入内容,可以在控制台看到相应的输出。

    但是BIO的弊端就是它是阻塞的:

    • 服务端在调用serverSocket.accept()方法时是阻塞的。
    • 服务端为每一个客户端连接开启一个线程,客户端在调用br.readLine()读取数据时,也是阻塞的。

    阻塞点验证

     以下内容中可能会出现进程id变化的情况,是因为多次启动进程的情况,文章不是一口气写完的,自行替换pid

    通过strace方法可以看到详细的进程启动的服务调用,我们使用strace启动上面的java进程:

    1. 将上面的代码复制到linux服务器中:
    touch SocketBIO.java
    vi SocketBIO.java
    # 复制粘贴上面的代码,保存退出
    
    1. 编译代码

      /root/jdk/j2sdk1.4.2_18/bin/javac SocketBIO.java 
      
    2. 使用strace命令启动追踪java进程

      strace -ff -o out /root/jdk/j2sdk1.4.2_18/bin/java SocketBIO
      

    启动成功后另起一个shell连接可以看到如下目录:

    [root@node01 jdk4]# ll
    总用量 688
    -rw-r--r--. 1 root root 164417 6月   7 14:00 out.12509
    -rw-r--r--. 1 root root   9973 6月   7 14:01 out.12510
    -rw-r--r--. 1 root root   1329 6月   7 14:00 out.12511
    -rw-r--r--. 1 root root   1324 6月   7 14:00 out.12512
    -rw-r--r--. 1 root root   1068 6月   7 14:00 out.12513
    -rw-r--r--. 1 root root   1301 6月   7 14:00 out.12514
    -rw-r--r--. 1 root root  10366 6月   7 14:00 out.12515
    -rw-r--r--. 1 root root 207460 6月   7 14:01 out.12516
    -rw-r--r--. 1 root root   1112 6月   7 13:56 SocketBIO.class
    -rw-r--r--. 1 root root   1867 6月   7 13:55 SocketBIO.java
    

    使用strace追踪命令,会为进程中的每一个线程都创建一个out.pid的文件,记录此线程的系统调用

    此时执行netstat -natp|grep 9000查看socket的端口状态

    [root@node01 jdk4]# netstat -natp |grep 9000
    tcp6       0      0 :::9000                 :::*                    LISTEN      12509/java    
    

    可以看到如上内容,12509进程开启了9000端口,处理LISTEN状态,也就是操作系统此时正在监听9000端口。

    然后查看less out.12509也就是主线程的系统调用:

    .........
    socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 3
    setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
    bind(3, {sa_family=AF_INET6, sin6_port=htons(9000), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0)}, 24) = 0
    listen(3, 50)                           = 0
    write(1, "step1: new ServerSocket(9000)", 29) = 29
    write(1, "
    ", 1)                       = 1
    gettimeofday({tv_sec=1623045647, tv_usec=115119}, NULL) = 0
    gettimeofday({tv_sec=1623045647, tv_usec=115292}, NULL) = 0
    gettimeofday({tv_sec=1623045647, tv_usec=115330}, NULL) = 0
    gettimeofday({tv_sec=1623045647, tv_usec=115493}, NULL) = 0
    gettimeofday({tv_sec=1623045647, tv_usec=115525}, NULL) = 0
    gettimeofday({tv_sec=1623045647, tv_usec=115642}, NULL) = 0
    accept(3, 
    

    可以看到如上内容:

    • 首先通过socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 3创建了一个套接字,返回一个3的文件描述符。

      可以通过lsof -p 12509来查看进程打开的文件描述符:

      [root@node01 jdk4]# lsof -p 12509
      COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF     NODE NAME
      ......
      java    12509 root  mem    REG  253,0   2107692 16791640 /usr/lib/libc-2.17.so
      java    12509 root  mem    REG  253,0     17716 17503682 /usr/lib/libdl-2.17.so
      java    12509 root  mem    REG  253,0    133736 17503701 /usr/lib/libpthread-2.17.so
      java    12509 root  mem    REG  253,0     16384 50792961 /tmp/hsperfdata_root/12509
      java    12509 root  mem    REG  253,0    158768 16791632 /usr/lib/ld-2.17.so
      java    12509 root    0u   CHR  136,2       0t0        5 /dev/pts/2
      java    12509 root    1u   CHR  136,2       0t0        5 /dev/pts/2
      java    12509 root    2u   CHR  136,2       0t0        5 /dev/pts/2
      java    12509 root    3u  IPv6 210892       0t0      TCP *:cslistener (LISTEN)
      java    12509 root    4u  sock    0,7       0t0   210890 protocol: TCPv6
      

      可以看到文件描述符3是一个TCP连接。

    • 然后通过bind()函数将文件描述符3和9000端口进行绑定。

    • listne(3, 50)监听文件描述符3。

    • accetp(3, 阻塞等待客户端连接。

    然后在本地通过nc命令创建一个客户端连接(java代码写客户端也可以, 懒省事用nc)。

    [root@node01 jdk4]# nc localhost 9000
    

    然后新开一个窗口再次执行netstat -natp |grep 9000

    tcp6       0      0 :::9000                 :::*                    LISTEN      12784/java     
    tcp6       0      0 ::1:9000                ::1:44612               ESTABLISHED 12784/java     
    tcp6       0      0 ::1:44612               ::1:9000                ESTABLISHED 12792/nc   
    

    可以看到,本地创建打开了一个端口44612用于与9000建立一个TCP连接,然后在查看主线程的out文件:

    accept(3, {sa_family=AF_INET6, sin6_port=htons(44612), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 5
    gettimeofday({tv_sec=1623051290, tv_usec=728682}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=728721}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=728750}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=728822}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=728851}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=729000}, NULL) = 0
    write(1, "client 35727423244612 is in", 21) = 21
    write(1, "
    ", 1)                       = 1
    gettimeofday({tv_sec=1623051290, tv_usec=729976}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=730028}, NULL) = 0
    stat64("/root/io-study/bio/jdk4/SocketBIO$1.class", {st_mode=S_IFREG|0644, st_size=1400, ...}) = 0
    open("/root/io-study/bio/jdk4/SocketBIO$1.class", O_RDONLY|O_LARGEFILE) = 6
    fstat64(6, {st_mode=S_IFREG|0644, st_size=1400, ...}) = 0
    stat64("/root/io-study/bio/jdk4/SocketBIO$1.class", {st_mode=S_IFREG|0644, st_size=1400, ...}) = 0
    read(6, "312376272276.U
    26#	25$
    %&7'7(
    "..., 1400) = 1400
    close(6)                                = 0
    gettimeofday({tv_sec=1623051290, tv_usec=730648}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=730716}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=730747}, NULL) = 0
    gettimeofday({tv_sec=1623051290, tv_usec=730846}, NULL) = 0
    mmap2(NULL, 528384, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0xea915000
    mprotect(0xea915000, 4096, PROT_NONE)   = 0
    clone(child_stack=0xea995424, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xea995ba8, tls={entry_number=12, base_addr=0xea995b40, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}, child_tidptr=0xea995ba8) = 12793
    futex(0x86d9d34, FUTEX_WAIT_PRIVATE, 1, NULL) = 0
    futex(0x86d9be4, FUTEX_WAIT_PRIVATE, 2, NULL) = 0
    futex(0x86d9be4, FUTEX_WAKE_PRIVATE, 1) = 0
    sched_setscheduler(12793, SCHED_OTHER, [5]) = -1 EINVAL (无效的参数)
    accept(3, 
    
    • 从第一行可以看到accetp()函数,接收了一个客户端,返回了一个文件描述符5,这个文件描述符5就是客户端与服务端之间一个点对点的通道,可以通过lsof -p 12784查看:

      [root@node01 jdk4]# lsof -p 12784
      COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF     NODE NAME
      .....
      java    12784 root  mem    REG  253,0    158768 16791632 /usr/lib/ld-2.17.so
      java    12784 root    0u   CHR  136,2       0t0        5 /dev/pts/2
      java    12784 root    1u   CHR  136,2       0t0        5 /dev/pts/2
      java    12784 root    2u   CHR  136,2       0t0        5 /dev/pts/2
      java    12784 root    3u  IPv6 219987       0t0      TCP *:cslistener (LISTEN)
      java    12784 root    4u  sock    0,7       0t0   219985 protocol: TCPv6
      java    12784 root    5u  IPv6 219988       0t0      TCP localhost:cslistener->localhost:44612 (ESTABLISHED)
      
    • 在下面通过clone()函数为客户端创建了一个新线程。返回的12793也就是客户端子线程的线程id。然后ll查看out.12793文件。

      ...........
      mprotect(0xea924000, 12288, PROT_NONE)  = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732258}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732293}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732320}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732361}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732390}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732432}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732494}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732527}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732554}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732610}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732638}, NULL) = 0
      gettimeofday({tv_sec=1623051290, tv_usec=732691}, NULL) = 0
      recv(5, 
      

      可以看到系统调用recv(5,阻塞等待接受客户端数据。

    • 然后再次在accept()处阻塞。

    通过上面代码就可以了解BIO的整个系统调用流程,了解整体阻塞点。

    注意

    在jdk1.7以前,主线程是第一个线程,也就是进程号就是主线程。但是在jdk1.7以后主线程是第二个线程。因此查看系统调用时要看第二个(大小最大的文件)。

    [root@node01 jdk8]# ll
    总用量 260
    -rw-r--r--. 1 root root   9724 6月   7 17:42 out.12931
    -rw-r--r--. 1 root root 181394 6月   7 17:43 out.12932
    -rw-r--r--. 1 root root   1573 6月   7 17:43 out.12933
    -rw-r--r--. 1 root root    931 6月   7 17:43 out.12934
    -rw-r--r--. 1 root root   1055 6月   7 17:43 out.12935
    -rw-r--r--. 1 root root    975 6月   7 17:43 out.12936
    -rw-r--r--. 1 root root   4740 6月   7 17:43 out.12937
    -rw-r--r--. 1 root root   3747 6月   7 17:43 out.12938
    -rw-r--r--. 1 root root    931 6月   7 17:43 out.12939
    -rw-r--r--. 1 root root  12938 6月   7 17:43 out.12940
    -rw-r--r--. 1 root root   1641 6月   7 10:12 SocketBIO$1.class
    -rw-r--r--. 1 root root   1153 6月   7 10:12 SocketBIO.class
    

    在1.4中是通过accept()阻塞接受客户端的,在jdk1.7之后是通过poll函数,仍然是阻塞的:

    bind(5, {sa_family=AF_INET6, sin6_port=htons(9000), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
    listen(5, 50)                           = 0
    mprotect(0x7f9acc0d3000, 4096, PROT_READ|PROT_WRITE) = 0
    write(1, "step1: new ServerSocket(9000)", 29) = 29
    write(1, "
    ", 1)                       = 1
    lseek(3, 58905332, SEEK_SET)            = 58905332
    read(3, "PK34
    10240#344Ny271LV2416241625", 30) = 30
    lseek(3, 58905383, SEEK_SET)            = 58905383
    read(3, "3123762722760041345
    6127	233130	233131	233132	"..., 13985) = 13985
    poll([{fd=5, events=POLLIN|POLLERR}], 1, -1
    

    C10K问题

    上面写了BIO模型的实现以及细节。由于BIO的缺陷,引起一个C10K的问题,即10000个客户端。

    上面的BIO的方式会为每一个客户端创建一个线程,那么当有10K个客户端时,也就会有10K个线程。这样就会造成资源浪费。以及10K个线程同时运行,线程切换耗时增加,响应会很慢。

    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author shuai.zhao@going-link.com
     * @date 2021/6/15
     */
    public class C10KClient {
        private static List<SocketChannel> clients = new ArrayList<>();
    
        public static void main(String[] args) {
            InetSocketAddress server = new InetSocketAddress("127.0.0.1", 9000);
    
            long start = System.currentTimeMillis();
            try {
                for (int i = 10000; i < 65000; i++) {
    
                    SocketChannel client1 = SocketChannel.open();
    
                    client1.bind(new InetSocketAddress("127.0.0.1", i));
    
                    client1.connect(server);
    
                    clients.add(client1);
                }
            } catch (Exception ignore) {
                // 系统占用端口可能引发端口占用异常
            }
            System.out.println("connection time consuming:" + (System.currentTimeMillis() - start));
            System.out.println("clients = " + clients.size());
        }
    }
    
    

    创建一个C10K客户端,然后启动BIO服务端,再启动上面的客户端(ps:因为只是为了查看一下效率,我是使用本地启动的服务端和客户端)

    结果如下:

    服务端:
    client :14070 is in
    client :14071 is in
    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    	at java.lang.Thread.start0(Native Method)
    	at java.lang.Thread.start(Thread.java:717)
    	at com.gouxiazhi.io.SocketBIO.main(SocketBIO.java:53)
      .......
    客户端:
    connection time consuming:27909
    clients = 4122
    

    可以看到使用BIO的方式时,只连了四千多个链接,就因为无法创建新的链接而报错了(因为我是使用本地,如果在linux服务器上跑的话,使用root用户可以多创建一些链接及线程,但是仍然无法避免资源耗尽的风险)

  • 相关阅读:
    磁盘调度算法
    Maven 的 学习笔记
    文档结构
    变量
    进入SQL*Plus环境 (常用命令)
    PAT甲级 1050 String Subtraction (20分)(当读一行时(gets用不了))
    PAT甲级 1095 Cars on Campus (30分)(map + 排序)
    图书管理系统
    学生成绩管理系统
    磁盘调度算法
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/14889510.html
Copyright © 2011-2022 走看看