Netty--从内核角度出发讲解BIO

一、Socket的连接过程、TCP的一些参数
  • 前置知识
    用到的命令
    netstat -natp 查看网络连接和占用的端口
    tcpdump -nn -i eth0 port 9090开监听抓取数据包
    lsof -p <进程号> 查看某个进程已经打开的文件状态

  • Socket服务端代码

package com.haizhang.netty.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;


public class SocketIOPropertites {
    //server socket listen property: 这些配置不是JVM层级的,是关联到内核的TCP协议栈的一些选项参数。
    private static final int RECEIVE_BUFFER = 10;
    private static final int SO_TIMEOUT = 0;  // 服务端的超时时间
    private static final boolean REUSE_ADDR = false;
    private static final int BACK_LOG = 2; // 多少个连接可以被积压
    //client socket listen property on server endpoint:
    private static final boolean CLI_KEEPALIVE = false;
    private static final boolean CLI_OOB = false;
    private static final int CLI_REC_BUF = 20;
    private static final boolean CLI_REUSE_ADDR = false;
    private static final int CLI_SEND_BUF = 20;
    private static final boolean CLI_LINGER = true;
    private static final int CLI_LINGER_N = 0;
    private static final int CLI_TIMEOUT = 0;  // 客户端的超时时间
    private static final boolean CLI_NO_DELAY = false;
    /*

        StandardSocketOptions.TCP_NODELAY
        StandardSocketOptions.SO_KEEPALIVE
        StandardSocketOptions.SO_LINGER
        StandardSocketOptions.SO_RCVBUF
        StandardSocketOptions.SO_SNDBUF
        StandardSocketOptions.SO_REUSEADDR
     */
    public static void main(String[] args) {
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(9090), BACK_LOG);
            server.setReceiveBufferSize(RECEIVE_BUFFER);
            server.setReuseAddress(REUSE_ADDR);
            server.setSoTimeout(SO_TIMEOUT);

        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("server up use 9090!");
        try {
            while (true) {
                // System.in.read();  //分水岭:
                final Socket client = server.accept();  //阻塞1,没有 -1  一直卡着不动  accept(4,
                System.out.println("client port: " + client.getPort());
                client.setKeepAlive(CLI_KEEPALIVE);
                client.setOOBInline(CLI_OOB);
                client.setReceiveBufferSize(CLI_REC_BUF);
                client.setReuseAddress(CLI_REUSE_ADDR);
                client.setSendBufferSize(CLI_SEND_BUF);
                client.setSoLinger(CLI_LINGER, CLI_LINGER_N);
                client.setSoTimeout(CLI_TIMEOUT);
                client.setTcpNoDelay(CLI_NO_DELAY);
                //client.read   //阻塞   没有  -1 0
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            InputStream in = client.getInputStream(); //阻塞2
                            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                            char[] data = new char[1024];
                            while (true) {

                                int num = reader.read(data);

                                if (num > 0) {
                                    System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));
                                } else if (num == 0) {
                                    System.out.println("client readed nothing!");
                                    continue;
                                } else {
                                    System.out.println("client readed -1...");
                                    System.in.read();
                                    client.close();
                                    break;
                                }
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端代码

package com.bjmashibing.system.io;

import java.io.*;
import java.net.Socket;

public class SocketClient {

    public static void main(String[] args) {
        try {
            Socket client = new Socket("192.168.150.11",9090);

            client.setSendBufferSize(20);
            client.setTcpNoDelay(true);  // 如果数据量比较小,会不会积攒起来再发,默认是true
            client.setOOBInLine(true);
            OutputStream out = client.getOutputStream();

            InputStream in = System.in;
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));

            while(true){
                String line = reader.readLine();
                if(line != null ){
                    byte[] bb = line.getBytes();
                    for (byte b : bb) {
                        out.write(b);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

下面详细跟踪建立连接的过程
启动服务端
在这里插入图片描述
开启服务端后,出现了一个对于 9090 的 listen 状态。
TCP 三次握手是走 listen 的,建立连接之后,后面走文件描述符,那就是另外一个环节了,我们后面再讲。
在这里插入图片描述
使用jps得到服务端的进程id号:7932
在这里插入图片描述
使用lsof -p 7932查看7932端口的文件描述符的分配情况。

在这里插入图片描述

参数详解:
COMMAND:进程的名称
PID:进程标识符
USER:进程所有者
FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等
TYPE:文件类型,如DIR、REG等
DEVICE:指定磁盘的名称
SIZE:文件的大小
NODE:索引节点(文件在磁盘上的标识)
NAME:打开文件的确切名称
FD列中的文件描述符:
cwd值表示应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改,txt类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的/sbin/init程序。其次数值表示应用程序的文件描述符,这是打开该文件时返回的一个整数。如上的最后一行文件/dev/initctl,其文件描述符为 10。
u 表示该文件被打开并处于读取/写入模式,而不是只读 ? 或只写 (w) 模式。同时还有大写 的W 表示该应用程序具有对整个文件的写锁。该文件描述符用于确保每次只能打开一个应用程序实例。初始打开每个应用程序时,都具有三个文件描述符,从0到2,分别表示标准输入、输出和错误流。所以大多数应用程序所打开的文件的FD都是从3开始。
Type列:
文件和目录分别称为REG和DIR。
CHR表示字符;(fopen,打开文件)
BLK表示块设备;
UNIX、FIFO和IPv4,分别表示UNIX 域套接字、先进先出(FIFO)队列和网际协议(IP)套接字。
FIFO表示先进先出;(popen,pipe)
inet表示网际协议(IP)套接字tcp/udp;(socket)
netlink表示netlink

启动客户端
客户端启动,进入代码的阻塞等待用户输入逻辑
在这里插入图片描述
开启监听之后,我们使用nc命令,连接本地的9090端口

[root@node1 ~]# nc localhost 9090 

nc命令介绍https://blog.csdn.net/u012486730/article/details/82019996

在服务端抓到了三次握手的包
在这里插入图片描述
在服务端看到建立了连接,虽然连接还未被使用。
在这里插入图片描述
在客户端进行用户输入之后(服务端也有的阻塞的逻辑,需要回车才能接收client的数据)

在这里插入图片描述
继续查看服务端抓包监听在这里插入图片描述
查看服务端的连接状态:双方开辟了资源。即便你程序不要我,我也在内核里有资源用来接收或者等待一类的。

netstat -antp  | head -n 2 ;netstat -antp | grep 9090 

在这里插入图片描述
服务端输入回车之后
接受到了客户端发过来的数据
在这里插入图片描述
刚才的socket连接已经被分配给7932了
在这里插入图片描述
lsof 得到了新的文件描述符 6
总结一下
TCP:面向连接的,可靠的传输协议
在这里插入图片描述
Socket:是一个四元组。ip:port ip:port四元组的任何一个元的不同,都可以区分不同的连接。

面试题 1:服务端80端口接收客户端连接之后,是否需要为客户端的连接分配一个随机端口号?
答:不需要。

面试题 2:现在,有一个客户端,有一个服务端,
客户端的ip地址是AIP,程序使用端口号CPORT想要建立连接。
服务端的IP地址是XIP,端口号是XPORT。
现在假设某一个客户端A开了很多连接占满了自己的65535个端口号,那客户端A是否还能与另一个服务端建立建立连接?
答:可以,因为只要能保证四元组唯一即可

在这里插入图片描述
注:一台服务器是可以与超过65535个客户端保持长连接的,调优到超过百万连接都没问题,只要四元组唯一就可以了。客户端来了之后,服务端是不需要单独给它开辟一个端口号的。
在这里插入图片描述
下面这个图可以说明,无论再多的连接,服务端始终是使用的同一个ip:端口
在这里插入图片描述
那么,我们常见的报错“端口号被占用”是什么原因?

我们常见的报错“端口号被占用”实际上是在启动SocketSocket的时候,而不是Socket,两者不是一个概念。如果两个服务使用了相同的端口号,这时如果来了一个数据包,内核无法区分是哪一个服务在LISTEN,不知道要发给哪一个服务了,如下图例子
在这里插入图片描述
在这里插入图片描述
每一个独立的进程只要维护它自己的文件描述符唯一即可。
在这里插入图片描述
keepalive
三个不同层级的 keepalive

  • TCP协议中规定,如果双方建立的连接(虚无的,并不是物理的连接),如果双方很久都不说话,你能确定对方还活着吗?不能,因为可能突然断电。所以规定了这么一种机制,哪怕是周期性的消耗一些网络资源,也要及时把无效的连接踢掉,节省内存。
  • HTTP级别
  • 负载均衡keepalived

网络IO的变化 演进模型(BIO)
一句话概括BIO?
BIO就是,客户端来一个连接,抛出一个线程,来一个连接,抛出一个线程…
BIO存在两个方面的阻塞,第一个方面就是ServerSocket在开启监听端口后accept客户端等待连接时,会阻塞当前线程。第二个就是,当ServerSocket接受到客户端连接时,后续要阻塞的等待客户端读数据的操作。
几个维度
同步、异步、阻塞、非阻塞
用到的命令:
strace -ff -o out /usr/java TestSocket
用来追踪Java程序和内核进行了哪些交互(进行了哪些系统调用)

strace -e strace=内核函数 -p pid 可以指定strace只追踪某个内核调用事件。参考strace用法介绍

详细追踪 BIO 的连接过程

public class TestServer {
    //server socket listen property: 这些配置不是JVM层级的,是关联到内核的TCP协议栈的一些选项参数。
    private static final int RECEIVE_BUFFER = 10;
    private static final int SO_TIMEOUT = 0;  // 服务端的超时时间
    private static final boolean REUSE_ADDR = false;
    private static final int BACK_LOG = 2; // 多少个连接可以被积压
    //client socket listen property on server endpoint:
    private static final boolean CLI_KEEPALIVE = false;
    private static final boolean CLI_OOB = false;
    private static final int CLI_REC_BUF = 20;
    private static final boolean CLI_REUSE_ADDR = false;
    private static final int CLI_SEND_BUF = 20;
    private static final boolean CLI_LINGER = true;
    private static final int CLI_LINGER_N = 0;
    private static final int CLI_TIMEOUT = 0;  // 客户端的超时时间
    private static final boolean CLI_NO_DELAY = false;
    /*

        StandardSocketOptions.TCP_NODELAY
        StandardSocketOptions.SO_KEEPALIVE
        StandardSocketOptions.SO_LINGER
        StandardSocketOptions.SO_RCVBUF
        StandardSocketOptions.SO_SNDBUF
        StandardSocketOptions.SO_REUSEADDR
     */
    public static void main(String[] args) {
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(8090), BACK_LOG);
            server.setReceiveBufferSize(RECEIVE_BUFFER);
            server.setReuseAddress(REUSE_ADDR);
            server.setSoTimeout(SO_TIMEOUT);

        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("server up use 8090!");
        try {
            while (true) {
                // System.in.read();  //分水岭:
                final Socket client = server.accept();  //阻塞1,没有 -1  一直卡着不动  accept(4,
                System.out.println("client port: " + client.getPort());
                client.setKeepAlive(CLI_KEEPALIVE);
                client.setOOBInline(CLI_OOB);
                client.setReceiveBufferSize(CLI_REC_BUF);
                client.setReuseAddress(CLI_REUSE_ADDR);
                client.setSendBufferSize(CLI_SEND_BUF);
                client.setSoLinger(CLI_LINGER, CLI_LINGER_N);
                client.setSoTimeout(CLI_TIMEOUT);
                client.setTcpNoDelay(CLI_NO_DELAY);
                //client.read   //阻塞   没有  -1 0
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            InputStream in = client.getInputStream(); //阻塞2
                            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                            while (true) {
                                System.out.println(reader.readLine());
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的代码使用jdk1.4跑起来
在这里插入图片描述

我们需要安装低版本的jdk才可以看清楚Socket连接时发生的详细细节,这里需要安装j2sdk-1_4_2_18-linux-i586.bin
参考https://blog.csdn.net/developerof/article/details/38455399

注意,如果是64位的机器,会报异常 bad ELF interpreter问题 ,解决办法:
情况一:64位系统中安装了32位程序解决办法
是因为64位系统中安装了32位程序
解决方法:

yum install glibc.i686

情况二:解决交叉编译环境错误

# arm-linux-gcc hello.c -o tt
/home/gl/usr/local/arm/4.3.2/bin/arm-linux-gcc: /home/gl/usr/local/arm/4.3.2/bin/arm-none-linux-gnueabi-gcc: /lib/ld-linux.so.2: bad ELF interpreter: 没有那个文件或目录
/home/gl/usr/local/arm/4.3.2/bin/arm-linux-gcc:行3: /home/gl/usr/local/arm/4.3.2/bin/arm-none-linux-gnueabi-gcc: 成功
[root@austgl gl]# yum install ld-linux.so.2


在服务端用jps找到进程的id号是8384
在这里插入图片描述

在服务端使用tail监控out.8384文件的输出(8384是main线程的输出,其他的out可能是一些垃圾回收线程等其他线程的输出)
(这里注意一下一共有8个线程,待会儿建立连接之后再看)
在这里插入图片描述
可以看到JVM用到了内核系统调用的accept,main线程正在阻塞
在这里插入图片描述
注意上面的accept后面紧跟着一个数字3,linux内核为每个进程提供了一个文件描述符表,而文件描述符都有自己的编号,0表示标准输入、1表示标准输出、2表示标准错误输出。故此这里打开一个Socket端口,会调用linux内核生成一个文件描述符,并产生一个编号为3的文件描述符编号。

紧着这,在一个客户端上建立一个连接
在这里插入图片描述
在服务端我们看到,刚才阻塞accept(3,的位置继续执行。34178是客户端连接进来的随机端口号,192.1618.150.12是来自于客户端的ip地址
在这里插入图片描述
当我们客户端连上的时候,会生成一个线程,那么线程究竟在操作系统中是怎么生成的呢?可以关注到accept(3,执行后,内核实际上调用了clone:
clone是linux的一个系统调用。Java当中的一个线程,就是操作系统的一个子线程。下图我们看到,(客户端连接进来之后),服务端调用clone函数,开启了一个线程号为8447的新线程。flags里面记录的是子线程共享的文件系统、打开的文件等父线程的系统资源
在这里插入图片描述
在clone之前,还会有一个系统调用指令mmap2,它负责将文件和设备映射到内存。mmap2系统调用使得使用32位off_t的应用程序能够映射大文件(最多2^44字节)。

参考https://man7.org/linux/man-pages/man2/mmap2.2.html

查看用strace输出的out文件,也可以证明8447这个新线程的存在。
在这里插入图片描述
在服务端可以看到,多了一个文件描述符5,表示的是从node01(服务端机器名称)到node02(客户端机器名称)的已连通的状态(socket四元组)
在这里插入图片描述
服务端 8447.out 正在recv阻塞接收
在这里插入图片描述

想学好Linux,去学习文档中这些man帮助手册,有时候比网络上的博客文章更准确(也可以 man man 查看帮助文档本身的帮助文档)
使用man 2 socket,你会发现所谓socket系统调用,其实就是调用了一个有返回值(文件描述符)的函数(用于LISTEN)
在这里插入图片描述

稍稍总结一下
BIO 模型的整个连接过程
无论哪种IO模型,application想要和外界通信,都要进行上面所展示的一系列的(3步)系统调用,都是不可缺少的。
之后服务端进入阻塞状态accept(3,等待客户端的连接。此次阻塞被成功地连接之后,又进入一的新的阻塞,等待新的客户端连接。
一旦连接成功之后,会为这个连接抛出去一个新的线程,新的线程中又进入一个阻塞状态recv(5,等待接收消息。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值