• 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用户可以多创建一些链接及线程,但是仍然无法避免资源耗尽的风险)

  • 相关阅读:
    一个简单的knockout.js 和easyui的绑定
    knockoutjs + easyui.treegrid 可编辑的自定义绑定插件
    Knockout自定义绑定my97datepicker
    去除小数后多余的0
    Windows Azure Web Site (15) 取消Azure Web Site默认的IIS ARR
    Azure ARM (1) UI初探
    Azure Redis Cache (3) 创建和使用P级别的Redis Cache
    Windows Azure HandBook (7) 基于Azure Web App的企业官网改造
    Windows Azure Storage (23) 计算Azure VHD实际使用容量
    Windows Azure Virtual Network (11) 创建VNet-to-VNet的连接
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/14889510.html
Copyright © 2020-2023  润新知