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

原文地址:https://www.cnblogs.com/Zs-book1/p/14889510.html