0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

探究Redis网络模型究竟有多强大(中)

jf_78858299 来源:蝉沐风的码场 作者:蝉沐风 2023-03-03 09:49 次阅读

3.2.1 创建socket

创建socket这一步和客户端没啥区别,不同的是这个socket我们称之为 等待连接socket(或监听socket)

3.2.2 绑定端口

bind()函数会将端口号写入上一步生成的监听socket中,这样一来,监听socket就完整保存了服务端的IP端口号

3.2.3 listen()的真正作用

listen(<Server描述符>, <最大连接数>);

很多小伙伴一定会对这个listen()有疑问,监听socket都已经创建完了,端口也已经绑定完了,为什么还要多调用一个listen()呢?

我们刚说过监听socket和客户端创建的socket没什么区别,问题就出在这个没什么区别上。

socket被创建出来的时候都默认是一个 主动socket ,也就说,内核会认为这个socket之后某个时候会调用connect()主动向别的设备发起连接。这个默认对客户端socket来说很合理,但是监听socket可不行,它只能等着客户端连接自己,因此我们需要调用listen()将监听socket从主动设置为被动,明确告诉内核:你要接受指向这个监听socket的连接请求!

此外,listen()的第2个参数也大有来头!监听socket真正接受的应该是已经完整完成3次握手的客户端,那么还没完成的怎么办?总得找个地方放着吧。于是内核为每一个监听socket都维护了两个队列:

  • 半连接队列(未完成连接的队列)

这里存放着暂未彻底完成3次握手的socket(为了防止半连接攻击,这里存放的其实是占用内存极小的request _sock,但是我们直接理解成socket就行了),这些socket的状态称为SYN_RCVD

  • 已完成连接队列

每个已完成TCP3次握手的客户端连接对应的socket就放在这里,这些socket的状态为ESTABLISHED

文字太多了,有点干,上个图!

图片

listen与3次握手

解释一下动图中的内容:

  1. 客户端调用connect()函数,开始3次握手,首先发送一个SYN X的报文(X是个数字,下同);
  2. 服务端收到来自客户端的SYN,然后在监听socket对应的半连接队列中创建一个新的socket,然后对客户端发回响应SYN Y,捎带手对客户端的报文给个ACK
  3. 直到客户端完成第3次握手,刚才新创建的socket就会被转移到已连接队列;
  4. 当进程调用accept()时,会将已连接队列头部的socket返回;如果已连接队列为空,那么进程将被睡眠,直到已连接队列中有新的socket,进程才会被唤醒,将这个socket返回

第4步就是阻塞的本质啊,朋友们!

3.3 答疑时间

Q1.队列中的对象是socket吗?

呃。。。乖,咱就把它当成socket就好了,这样容易理解,其实具体里边存放的数据结构是啥,我也很想知道,等我写完这篇文章,我研究完了告诉你。

Q2.accept()这个函数你还没讲是啥意思呢?

accept()函数是由服务端调用的,用于从已连接队列中返回一个socket描述符;如果socket为阻塞式的,那么如果已连接队列为空,accept()进程就会被睡眠。BIO恰好就是这个样子。

Q3.accept()为什么不直接把监听socket返回呢?

因为在队列中的socket经过3次握手过程的控制信息交换,socket的4元组的信息已经完整了,用做socket完全没问题。

监听socket就像一个客服,我们给客服打电话,然后客服找到解决问题的人,帮助我们和解决问题的人建立联系,如果直接把监听socket返回,而不使用连接socket,就没有socket继续等待连接了。

哦对了,accept()返回的socket也有个名字,叫 连接socket

3.4 BIO究竟阻塞在哪里

拿Server端的BIO来说明这个问题,阻塞在了serverSocket.accept()以及bufferedReader.readLine()这两个地方。有什么办法可以证明阻塞吗?

简单的很!你在serverSocket.accept(); 的下一行打个断点,然后debug模式运行BIOServerSocket,在没有客户端连接的情况下,这个断点绝不会触发!同样,在bufferedReader.readLine();下一行打个断点,在已连接的客户端发送数据之前,这个断点绝不会触发!

readLine()的阻塞还带来一个非常严重的问题,如果已经连接的客户端一直不发送消息,readLine()进程就会一直阻塞(处于睡眠状态),结果就是代码不会再次运行到accept(),这个ServerSocket没办法接受新的客户端连接。

解决这个问题的核心就是别让代码卡在readLine()就可以了,我们可以使用新的线程来readLine(),这样代码就不会阻塞在readLine()上了。

3.5 改造BIO

改造之后的BIO长这样,这下子服务端就可以随时接受客户端的连接了,至于啥时候能read到客户端的数据,那就让线程去处理这个事情吧。

public class BIOServerSocketWithThread {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(8099);
            System.out.println("启动服务:监听端口:8099");
            // 等待客户端的连接过来,如果没有连接过来,就会阻塞
            while (true) {
                // 表示阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
                Socket socket = serverSocket.accept(); //连接阻塞
                System.out.println("客户端:" + socket.getPort());
                // 表示获取客户端的请求报文
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            BufferedReader bufferedReader = new BufferedReader(
                                    new InputStreamReader(socket.getInputStream())
                            );
                            String clientStr = bufferedReader.readLine();
                            System.out.println("收到客户端发送的消息:" + clientStr);

                            BufferedWriter bufferedWriter = new BufferedWriter(
                                    new OutputStreamWriter(socket.getOutputStream())
                            );
                            bufferedWriter.write("ok\\n");
                            bufferedWriter.flush();
                        } catch (Exception e) {
                            //...
                        }

                    }
                }).start();
            }
        } catch (IOException e) {
            // 错误处理
        } finally {
            // 其他处理
        }
    }
}

事情的顺利进展不禁让我们飘飘然,我们居然是使用高阶的多线程技术解决了BIO的阻塞问题,虽然目前每个客户端都需要一个单独的线程来处理,但accept()总归不会被readLine()卡死了。

图片

BIO改造之后

所以我们改造完之后的程序是不是就是非阻塞IO了呢?

想多了。。。我们只是用了点奇技淫巧罢了,改造完的代码在系统调用层面该阻塞的地方还是阻塞,说白了,Java提供的API完全受限于操作系统提供的系统调用,在Java语言级别没能力改变底层BIO的事实!

Java没这个能力!

3.6 掀开BIO的遮羞布

接下来带大家看一下改造之后的BIO代码在底层都调用了哪一些系统调用,让我们在底层上对上文的内容加深一下理解。

给大家打个气,接下来的内容其实非常好理解,大家跟着文章一步步地走,一定能看得懂,如果自己动手操作一遍,那就更好了。

对了,我下来使用的JDK版本是JDK8。

straceLinux上的一个程序,该程序可以追踪并记录参数后边运行的进程对内核进行了哪些系统调用。

strace -ff -o out java BIOServerSocketWithThread

其中:

  • -o:

将系统调用的追踪信息输出到out文件中,不加这个参数,默认会输出到标准错误stderr

  • -ff

如果指定了-o选项,strace会追踪和程序相关的每一个进程的系统调用,并将信息输出到以进程id为后缀的out文件中。举个例子,比如BIOServerSocketWithThread程序运行过程中有一个ID为30792的进程,那么该进程的系统调用日志会输出到out.30792这个文件中。

我们运行strace命令之后,生成了很多个out文件。

图片

这么多进程怎么知道哪个是我们需要追踪的呢?我就挑了一个容量最大的文件进行查看,也就是out.30792,事实上,这个文件也恰好是我们需要的,截取一下里边的内容给大家看一下。

图片

可以看到图中的有非常多的行,说明我们写的这么几行代码其实默默调用了非常多的系统调用,抛开细枝末节,看一下上图中我重点标注的系统调用,是不是就是上文中我解释过的函数?我再详细解释一下每一步,大家联系上文,会对BIO的底层理解的更加通透。

  1. 生成监听socket,并返回socket描述符7,接下来对socket进行操作的函数都会有一个参数为7
  2. 8099端口绑定到监听socket,bind的第一个参数就是7,说明就是对监听socket进行的操作;
  3. listen()将监听socket(参数为7)设置为被动接受连接的socket,并且将队列的长度设置为50;
  4. 实际上就是System.out.println("启动服务:监听端口:8099");这一句的系统调用,只不过中文被编码了,所以我特意把:8099圈出来证明一下;

额外说两点:

其一:可以看到,这么一句简单的打印输出在底层实际调用了两次write系统调用,这就是为什么不推荐在生产环境下使用打印语句的原因,多少会影响系统性能;

其二:write()的第一个参数为1,也是文件描述符,表示的是标准输出stdout

  1. 系统调用阻塞在了poll()函数,怎么看出来的阻塞?out文件的每一行运行完毕都会有一个 = 返回值,而poll()目前没有返回值,因此阻塞了。实际上poll()系统调用对应的Java语句就是serverSocket.accept();

不对啊?为什么底层调用的不是accept()而是poll()?poll()应该是多路复用才是啊。在JDK4之前,底层确实直接调用的是accept(),但是之后的JDK对这一步进行了优化,除了调用accept(),还加上了poll()poll()的细节我们下文再说,这里可以起码证明了poll()函数依然是阻塞的,所以整个BIO的阻塞逻辑没有改变。

接下来我们起一个客户端对程序发起连接,直接用Linux上的nc程序即可,比较简单:

nc localhost 8099

发起连接之后(但并未主动发送信息),out.30792的内容发生了变化:

图片

  1. poll()函数结束阻塞,程序接着调用accept()函数返回一个连接socket,该socket的描述符为8
  2. 就是System.out.println("客户端:" + socket.getPort());的底层调用;
  3. 底层使用clone()创造了一个新进程去处理连接socket,该进程的pid为31168,因此JDK8的线程在底层其实就是轻量级进程;
  4. 回到poll()函数继续阻塞等待新客户端连接。

由于创建了一个新的进程,因此在目录下对多出一个out.31168的文件,我们看一下该文件的内容:

图片

发现子进程阻塞在了recvfrom()这个系统调用上,对应的Java源码就是bufferedReader.readLine();,直到客户端主动给服务端发送消息,阻塞才会结束。

3.7 BIO总结

到此为止,我们就通过底层的系统调用证明了BIO在accept()以及readLine()上的阻塞。最后用一张图来结束BIO之旅。

图片

BIO模型

BIO之所以是BIO,是因为系统底层调用是阻塞的,上图中的进程调用recv,其系统调用直到数据包准备好并且被复制到应用程序的缓冲区或者发生错误为止才会返回,在此整个期间,进程是被阻塞的,啥也干不了。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Socket
    +关注

    关注

    0

    文章

    179

    浏览量

    34491
  • 函数
    +关注

    关注

    3

    文章

    4117

    浏览量

    61545
  • Redis
    +关注

    关注

    0

    文章

    366

    浏览量

    10553
收藏 人收藏

    评论

    相关推荐

    小米5的那颗核心,究竟有多强

    小米5的那颗核心,究竟有多强自从小米在2011年崛起后,高通的骁龙系列处理器就逐渐成为了市面上旗舰手机的主流处理器,从蝎子核心到环蛇核心,再到现在的骁龙810,高通一直在进步。那么,这款骁龙810
    发表于 06-01 19:35

    Redis Stream应用案例

    今天介绍的主角——Redis Stream,本身就是起源于IRC中一个用户的idea。IRC的模型如下,在某个IRC频道的用户,既可以向所有的其他用户自由的发送消息,也可以接收其他所有用户发送
    发表于 06-26 17:15

    液晶PC与液晶电视究竟有什么区别?

    为什么要选择液晶?液晶PC与液晶电视究竟有什么区别?如何选择液晶PC与液晶电视?
    发表于 06-07 06:13

    请问一下RFID与NFC究竟有什么关系?

    RFID与NFC究竟有什么关系?
    发表于 06-15 07:06

    面向列的HBase存储结构究竟有什么样的不同之处呢?

    HBase是什么?HBase的存储结构究竟是怎样的呢?面向列的HBase存储结构究竟有什么样的不同之处呢?
    发表于 06-16 06:52

    请问一下芯片制造究竟有多难?

    请问一下芯片制造究竟有多难?
    发表于 06-18 06:53

    PCI-E4.0究竟有什么优势?

    PCI-E4.0究竟有什么优势?PCI-E究竟指的是什么呢?
    发表于 06-18 06:54

    内存时序究竟有多重要呢?究竟该如何去选择内存条呢?

    内存时序究竟有多重要呢?究竟该如何去选择内存条呢?DDR内存时序是高一些好还是低一些好?
    发表于 06-18 08:20

    定时器中断类型探究 精选资料分享

     一直在用的stm32定时器的中断都是TIM_IT_Update更新中断,也没问为什么,直到碰到有人使用TIM_IT_CC1断,才想到这定时器的中断类型究竟有什么区别,都怪当时学习stm32的时候
    发表于 08-13 06:28

    OpenPLC开源工业控制器究竟有何用处

    OpenPLC开源工业控制器有哪些优点?OpenPLC开源工业控制器有哪些功能?OpenPLC开源工业控制器究竟有何用处?
    发表于 09-02 07:42

    华为荣耀Magic今日发布:“未来”手机究竟有多强

    华为荣耀即将在12月16日发布最新的“未来”手机magic,关于这款手机的爆料在今日已经铺天盖地,今天,小编将为大家整理一下,给大家一个荣耀Magic的基本判断,看看这款旗舰究竟有多强力!
    发表于 12-16 09:34 3283次阅读

    ibm的2nm芯片究竟有多强 2nm芯片对续航的影响

    全球首颗2nm芯片的问世对半导体行业影响重大,IBM通过与AMD、三星及GlobalFoundries等多家公司的合作,最终抵达了2nm芯片制程的节点,推出了2nm的测试芯片。那么这颗芯片究竟有多强呢?它对续航的影响又有多大呢?
    的头像 发表于 06-23 09:35 1891次阅读

    Molex莫仕连接器的功能究竟有多强大?看他们的行业应用你就知道了!

    KOYUELEC光与电子:Molex莫仕连接器的功能究竟有多强大?看他们的行业应用你就知道了!
    的头像 发表于 12-31 12:30 7459次阅读

    探究Redis网络模型究竟有多强大(上)

    本文将从BIO开始介绍,经过NIO、多路复用,最终说回Redis的Reactor模型,力求详尽。本文与其他文章的不同点主要在于:
    的头像 发表于 03-03 09:46 341次阅读
    <b class='flag-5'>探究</b><b class='flag-5'>Redis</b><b class='flag-5'>网络</b><b class='flag-5'>模型</b><b class='flag-5'>究竟有多强大</b>(上)

    探究Redis网络模型究竟有多强大(下)

    接下来的非阻塞IO我们只抓主要矛盾,其余参考BIO即可。 如果你看过其他介绍非阻塞IO的文
    的头像 发表于 03-03 09:50 303次阅读
    <b class='flag-5'>探究</b><b class='flag-5'>Redis</b><b class='flag-5'>网络</b><b class='flag-5'>模型</b><b class='flag-5'>究竟有多强大</b>(下)