网络编程(一):服务器模型、Java I/O模型、Reactor事件处理模型、I/O复用

news2024/10/11 2:18:59

文章目录

  • 一、Socket和TCP/IP协议族的关系
  • 二、服务器模型
    • 1.C/S模型(Client/Server Model)
    • 2.P2P模型(Peer-to-Peer Model)
  • 三、Java的I/O演进
    • 1.BIO(阻塞)
      • (1)工作流程
      • (2)代码实现
    • 2.NIO(多路复用/轮询)
      • (1)工作流程
      • (2)代码实现
    • 3.NIO2.0——AIO(异步/事件驱动)
  • 四、并发事件处理模式
    • 1.Reactor模式(NIO)
      • (1)单Reactor单线程模型
      • (2)单Reactor多线程模型(Redis)
      • (3)主从Reactor多线程模型(Netty)
    • 2.Proactor模式(AIO)
  • 五、I/O复用

一、Socket和TCP/IP协议族的关系

Socket和TCP/IP协议族是网络编程中的两个重要概念,它们之间存在密切的关系。

首先,TCP/IP协议族是一组用于互联网通信的网络协议的集合。它由多个协议组成,其中最核心的协议是TCP(Transmission Control Protocol)和IP(Internet Protocol)。TCP协议提供可靠的数据传输和连接管理,而IP协议则负责将数据包从源地址传输到目的地址。

Socket是一种抽象概念,它提供了应用程序与网络之间的接口。通过Socket,应用程序可以通过网络与其他应用程序进行通信。在编程语言中,Socket通常被封装成库或API,以提供方便的网络编程接口。

TCP/IP协议族中的TCP协议使用Socket来实现端到端的数据传输。在TCP/IP网络中,每个主机都有一个唯一的IP地址,而每个运行TCP协议的应用程序都使用一个Socket来标识自己。一个Socket由IP地址和端口号组成,用于唯一标识网络中的一个应用程序。

当应用程序使用Socket进行网络通信时,它可以创建一个Socket对象,并指定目标主机的IP地址和端口号。通过Socket对象,应用程序可以使用TCP协议建立与目标主机的连接,并进行数据的发送和接收。TCP协议负责将数据分割成小的数据包,并通过IP协议将这些数据包从源主机传输到目的主机。

总结来说,Socket是应用程序与网络之间的接口,而TCP/IP协议族则是网络通信的基础协议。通过Socket和TCP协议,应用程序可以在TCP/IP网络中进行可靠的数据传输和通信。

二、服务器模型

服务器模型是指在网络中进行通信和资源共享时所采用的不同架构模式。常见的服务器模型包括C/S模型(Client/Server Model)和P2P模型(Peer-to-Peer Model)。

1.C/S模型(Client/Server Model)

C/S模型是一种常见的服务器模型,其中客户端(Client)和服务器(Server)之间存在明确的角色和功能分工。

  • 客户端:客户端是发起请求的一方,它向服务器发送请求并接收服务器的响应。客户端通常是一台终端设备,如个人计算机、智能手机等。
  • 服务器:服务器是提供服务的一方,它接收客户端的请求并提供相应的服务或资源。服务器通常是一台高性能的计算机或设备,具备处理请求和提供服务的能力。

在C/S模型中,客户端和服务器之间通过网络进行通信。客户端发起请求,服务器接收请求并处理,然后将响应发送回客户端。这种模型可以实现中心化的控制和管理,服务器负责处理和存储数据,客户端主要负责用户界面的展示和交互。

2.P2P模型(Peer-to-Peer Model)

P2P模型是一种去中心化的服务器模型,其中参与通信的设备之间平等地协作,没有明确的客户端和服务器的区别。

  • 对等节点:在P2P模型中,所有参与通信的设备都是对等节点,它们既是服务的提供者,也是服务的请求者。每个节点都可以与其他节点直接通信,共享资源或提供服务。

在P2P模型中,设备之间通过直接连接进行通信,而不依赖于中央服务器。每个设备既可以发起请求,也可以响应其他设备的请求,实现了资源和服务的共享。P2P模型常用于文件共享、实时通信等场景,例如BitTorrent协议就是一种典型的P2P协议。

总结来说,C/S模型是一种中心化的服务器模型,客户端和服务器之间存在明确的角色和功能分工;而P2P模型是一种去中心化的服务器模型,参与通信的设备平等地协作,共享资源和服务。选择适合的服务器模型取决于具体的应用需求和网络架构。

三、Java的I/O演进

1.BIO(阻塞)

网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

BIO,即Blocking IO,阻塞型I/O。阻塞体现在两个地方,连接线程的阻塞和读写的阻塞。

(1)工作流程

服务端启动ServerSocket;
客户端启动 Socket 对服务器进行通信,服务端对每个客户端建立一个线程与之通讯(可以使用线程池进行优化);
客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待(即阻塞);
如果有响应,客户端线程会等待请求结束后,再继续执行。

在这里插入图片描述

(2)代码实现

  • 服务端:
package bio;

import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class BIOServer {
    //创建一个线程池,用于处理客户端连接后的工作
    public static ThreadPoolExecutor pool=new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    public static void main(String[] args) throws IOException{
        ServerSocket serverSocket=new ServerSocket(8888);
        while(true){
            //1 等待客户端连接是阻塞的
            Socket socket=serverSocket.accept();
            System.out.println("客户端连接上了");
            //2 连接上以后向线程池提交一个任务用于处理连接
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    while(true){
                        try{
                            //读写也是阻塞的
                            //创建输出流,server向client输出
                            PrintStream printStream = new PrintStream(socket.getOutputStream());
                            printStream.println("message from server 8888");
                            printStream.close();
                            socket.close();
                        }catch(IOException e){
                            e.printStackTrace();
                        }
                        
                    }
                }
            });
        }
    }
}

  • 客户端:
package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class BIOClient {
    public static void main(String[] args) throws IOException{
        Socket socket = new Socket("127.0.0.1", 8888);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println("This message comes from server:"+bufferedReader.readLine());
        bufferedReader.close();
        socket.close();
    }
}

在这里插入图片描述

  • 缺点:
    • accept()等待客户端连接是阻塞的,有时候需要进行无谓的等待,效率低下,浪费资源。
    • 引入线程池进行优化提升了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃。
    • 读写操作仍然是阻塞的,如果客户端半天没有操作,也会浪费资源,因此效率不高。

2.NIO(多路复用/轮询)

NIO,即non-blocking lO,非阻塞型IO。

  • 非阻塞——减少线程资源的浪费:
    BIO提供非阻塞读写模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。可以做到 用一个线程来处理多个操作,体现了一种多路复用的思想。 而不是像BIO那样,一个连接过来就得分配一个线程,造成资源的浪费。

  • 处理数据的方式:
    BIO 以流的方式处理数据,而 NIO 以缓冲区(也被叫做块)的方式处理数据,块 IO 效率比流 IO 效率高很多。BIO 基于字符流或者字节流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区或者从缓冲区写入到通道。

  • 复用:
    Selector(选择器)用于监听多个通道的事件(比如连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

(1)工作流程

Channel(通道),Buffer(缓冲区), Selector(选择器)为NIO的三大核心组件。

  • Channel(通道):
    相比于BIO流的读写,Channel的读写是双向的,既可以从通道中读取数据,又可以写数据到通道。通道可以非阻塞读取和写入通道/缓冲区,也支持异步地读写。

  • Buffer(缓冲区):
    在客户端和Channel之间,增加Buffer缓冲区的支持,更加容易操作和管理。

  • Selector(选择器):
    用来 轮询 检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率。

在这里插入图片描述

(2)代码实现

代码来自:here

  • 服务端:
package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 创建一个Selector对象,
        Selector selector = Selector.open();

        // 绑定端口6666, 在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        // 把serverSocketChannel注册到selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 循环等待用户连接
        while (true){
            if (selector.select(1000) == 0){ //等待(阻塞)一秒, 没有事件发生
//            if (selector.selectNow() == 0){ // 也可以设置成非阻塞的
                System.out.println("服务器等待了一秒,无连接");
                continue;
            }

            // 如果返回的>0 , 说明客户端有了动作,就获取相关的selectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回关注事件的集合

            // 遍历selectionKeys
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()){
                // 获取到selectionKey
                SelectionKey key = keyIterator.next();
                //根据key对应的通道获取事件并做相应处理
                if (key.isAcceptable()){
                    //如果是OP_ACCEPT, 表示有新的客户端产生
                    //给该客户端生成SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //将socketChannnel设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel注册到selector上, 设置事件为OP_READ,同时给socketChannel关联一个buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if (key.isReadable()){
                    // 发生了OP_READ
                    SocketChannel channel=(SocketChannel)key.channel();
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客户端"+new String(buffer.array()));
                }

                // 手动从集合中移除当前的selectionKey, 防止多线程情况下的重复操作
                keyIterator.remove();

            }


        }

    }
}

  • 客户端:
package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {

    public static void main(String[] args) throws IOException {
        // 获取一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞
        socketChannel.configureBlocking(false);
        //设置服务器端ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        if (!socketChannel.connect(inetSocketAddress)){

            while (!socketChannel.finishConnect()){
                //如果没有连接成功,客户端是非阻塞的,可以做其它工作
                System.out.println("等待连接...");
            }
        }

        // 如果连接成功,就发送数据
        String str = "hello world";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        // 发送数据 , 将buffer中的数据写入到channel中
        socketChannel.write(buffer);
        System.in.read();

    }

}

在这里插入图片描述

3.NIO2.0——AIO(异步/事件驱动)

AIO,即Asynchronous I/O,异步非阻塞IO。AIO提供的最大的特点是具备异步功能,采用“订阅-通知”模式,即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。

在这里插入图片描述

下面是一段简单的代码示例:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class NIO2AsyncFileIOExample {

    public static void main(String[] args) {
        try {
            // 通过路径获取文件通道
            Path path = Paths.get("test.txt");
            AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
                    path, StandardOpenOption.READ, StandardOpenOption.WRITE);

            // 分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 异步读取文件
            Future<Integer> readResult = fileChannel.read(buffer, 0);
            while (!readResult.isDone()) {
                // 在等待异步读取完成时可以进行其他操作
                System.out.println("Waiting for read operation to complete...");
            }

            // 打印读取结果
            buffer.flip();
            System.out.println("Read data: ");
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            System.out.println();

            // 异步写入数据
            String newData = "Hello, NIO 2.0!";
            buffer.clear();
            buffer.put(newData.getBytes());
            buffer.flip();
            Future<Integer> writeResult = fileChannel.write(buffer, 0);
            while (!writeResult.isDone()) {
                // 在等待异步写入完成时可以进行其他操作
                System.out.println("Waiting for write operation to complete...");
            }
            System.out.println("Data written to file.");

            // 关闭文件通道
            fileChannel.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、并发事件处理模式

1.Reactor模式(NIO)

使用 Java NIO 构建的 IO 程序,它的工作模式是:主动轮训 IO 事件,IO 事件发生后程序的线程主动处理 IO 工作,这种模式也叫做 Reactor 模式。

(1)单Reactor单线程模型

只有一个线程来执行所有的任务,效率低下,并且也有可靠性问题。

在这里插入图片描述

(2)单Reactor多线程模型(Redis)

相比于上一个模型,增加了线程池的支持,从一定程度上提升了并发效率,但是引入线程池可能会涉及到数据同步问题。Redis底层就是基于这种模型。

在这里插入图片描述

(3)主从Reactor多线程模型(Netty)

在上一个模型的基础上,一个Reactor变成了两个,主Reactor创建连接,从Reactor分发读写任务,能支持更高的并发量。Netty是基于这种模型。
在这里插入图片描述

2.Proactor模式(AIO)

使用 Java AIO 构建的 IO 程序,它的工作模式是:将 IO 事件的处理托管给操作系统,操作系统完成 IO 工作之后会通知程序的线程去处理后面的工作,这种模式也叫做 Proactor 模式。

现在AIO和Proactor使用还不怎么广泛。

五、I/O复用

select、poll、epoll

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/993472.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MATLAB实现函数拟合

目录 一.理论知识 1.拟合与插值的区别 2.几何意义 3.误差分析 二.操作实现 1.数据准备 2.使用cftool——拟合工具箱 三.函数拟合典例 四.代码扩展 一.理论知识 1.拟合与插值的区别 通俗的说&#xff0c;插值的本质是根据现有离散点的信息创建出更多的离散点&#xf…

HashMap解决哈希冲突

要了解 Hash冲突&#xff0c;那首先我们要先了解 Hash 算法和 Hash 表。 Hash算法 Hash 算法&#xff0c;就是把任意长度的输入&#xff0c;通过散列算法&#xff0c;变成固定长度的输出&#xff0c;这个输出结果是散列值。 Hash表 Hash 表又叫做“散列表”&#xff0c;它是通…

7.Xaml Image控件

1.运行图片 2.运行源码 a.xaml源码 <!--Source="/th.gif" 图像源--><!--Stretch="Fill" 填充模式--><Image x:Name

Qemu 架构 硬件模拟器

Qemu 架构 硬件模拟器 Qemu 是纯软件实现的虚拟化模拟器&#xff0c; 几乎可以模拟任何硬件设备&#xff0c; 我们最熟悉的就是能够模拟一台能够独立运行操作系统的虚拟机&#xff0c; 虚拟机认为自己和硬件打交道&#xff0c; 但其实是和 Qemu 模拟出来的硬件打交道&#xff…

单元测试界的高富帅,Pytest框架 (二) 前后置方法和 fixture 机制

前言 上一篇文章入门篇咱们介绍了pytest的基本使用&#xff0c;这一篇文章专门给大家讲解pytest中关于用例执行的前后置步骤处理,pytest中用例执行的前后置处理既可以通过测试夹具(fixtrue)来实现&#xff0c;也可以通过xunit 风格的前后置方法来实现。接下来我们一起看看如何…

利用procrank和lsof定位出客户软件内存OOM的问题

最近遇到一些事情&#xff0c;觉得挺憋屈的&#xff0c;可是再憋屈总得往前走吧&#xff01;打工人&#xff0c;不好办啊&#xff01;事情是这样的&#xff0c;笔者在芯片原厂负责SDK和行业解决方案输出的&#xff0c;可以理解成整体SDK turnkey方案。但是有些客户多少还要改一…

单例模式(饿汉式单例 VS 懒汉式单例)

所谓的单例模式就是保证某个类在程序中只有一个对象 一、如何控制只产生一个对象&#xff1f; 1.构造方法私有化&#xff08;保证对象的产生个数&#xff09; 创建类的对象&#xff0c;要通过构造方法产生对象 构造方法若是public权限&#xff0c;对于类的外部&#xff0c;可…

企业架构LNMP学习笔记31

负载均衡服务器的高可用备用服务器配置&#xff1a; 负载均衡服务器的配置比单台服务器的配置要高很多。硬件上要上一个台阶。 所有的请求流量都要经过负载均衡服务器&#xff0c;负载均衡服务器压力很大&#xff0c;防止她宕机&#xff0c;导致后端web服务器都不可用&#xf…

ROS2下使用TurtleBot3-->SLAM导航(仿真)RVIZ加载不出机器人模型

一、问题描述 在使用台式机进行仿真时&#xff0c;大部分例程很顺利&#xff0c;但在SLAM导航时&#xff0c;在RVIZ中却一直加载不出机器人模型&#xff0c;点击Navigation2 Goal选择目标点进行导航时&#xff0c;无响应。 启动后在RVIZ2和终端看到一个错误 按照官网的指令试…

探索 Wall-E 的寻路算法

几年前,Yandex 组织了一场名为“机器人快递员”的竞赛,并提供了诱人的奖品:一张参加专业人士封闭式自动驾驶会议的门票。该竞赛类似于一场游戏,参与者的任务是在地图上找到最佳路线并使用机器人快递员优化送货。 当我深入研究这个主题时,我发现尽管路线查找问题已经解决,…

ms17-010(永恒之蓝)漏洞复现

目录 前言 一、了解渗透测试流程 二、使用nmap工具对win7进行扫描 2.1 2.2 2.3 2.4 2.5 三、尝试ms17-010漏洞利用 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 四、结果展示 4.1 4.2 4.3 4.4 4.5 总结 前言 ms17-010&#xff08;永恒之蓝&am…

mysql八股1

参考MySQL八股文连环45问&#xff08;背诵版&#xff09; - 知乎 (zhihu.com) 基础 1.范式 第一范式&#xff1a;强调的是列的原子性 第二范式&#xff1a;要求实体的属性完全依赖于主关键字。所谓完全 依赖是指不能存在仅依赖主关键字一部分的属性。&#xff08;就是主键不…

Linux防火墙(iptables)

一、linux的防火墙组成 linux的防火墙由netfilter和iptables组成。用户空间的iptables制定防火墙规则&#xff0c;内核空间的netfilter实现防火墙功能。 netfilter&#xff08;内核空间&#xff09;位于Linux内核中的包过滤防火墙功能体系&#xff0c;称为Linux防火墙的“内核…

Vulnhub:Kira: CTF靶机

kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.238 信息收集 端口扫描 nmap -A -sC -v -sV -T5 -p- --scripthttp-enum 192.168.111.238访问80端口 点击upload发现是个上传点 点击language发现存在文件包含 上传php图片马再利用文件包含反弹shell http://1…

CIO40: 数字化落地最佳实践(16000字)

1-数字化规划 第一步&#xff1a;数字化规划。CIO要根据企业的发展现况及未来战略方向、根据企业董事长的布局和理念、根据企业各业务板块领导人的格局和理念&#xff0c;做出一份董事长和领导层高度认同的数字化规划&#xff0c;而且需要用PPT进行完整和专业的表达出来&#x…

基于SpringBoot的在线教育平台系统

基于SpringBootVue的线教育平台系统&#xff0c;前后端分离 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 【主要功能】 角色&#xff1a;管理员、学生、老师 …

When后接什么动词?

1、when doing用法 When从句的主语和主句的主语是同一个或同一些人时&#xff0c;从句的谓语动词为be动词doing形式&#xff0c;可以把从句的主语和be动词省略&#xff0c;即用when doing sth是指当做……事情时&#xff0c; 如&#xff1a; when doing the work,we should b…

(以太网交换机)BCM53575B0KFSBG、BCM56870A0KFSBG、BCM56760B0KFSBG采用BGA封装的集成电路

一、BCM53575B0KFSBG 1G/2.5G/10G/25G TSN 连接交换机 应用 中小企业 2.5G/10G 交换机 使用时间敏感网络&#xff08;TSN&#xff09;的工业以太网/汽车应用 联网 (TSN) 的工业以太网/汽车应用 用于基站、闪存阵列、专业级音频/视频和机箱背板的嵌入式连接交换机 &#xff08;…

自然语言处理NLP:一文了解NLP自然语言处理技术,NLP在生活中的应用,图导加深了解,NLP语料库,NLP开源工具

目录 1.自然语言处理NLP 1.1 NLP中英对照&#xff08;双份&#xff09; 1.2 相关文章 2.NLP语料库 2.1 NLP语料库列举 2.2 相关文章 3.NLP开源工具 3.1 NLP开源工具列举 3.2 相关文章 4.自然语言文本分类模型代码 5.图导NLP 6. NLP在生活中的应用 1.自然语言处理NLP …

Kafka中Consumer源码解读

Consumer源码解读 本课程的核心技术点如下&#xff1a; 1、consumer初始化 2、如何选举Consumer Leader 3、Consumer Leader是如何制定分区方案 4、Consumer如何拉取数据 5、Consumer的自动偏移量提交 Consumer初始化 从KafkaConsumer的构造方法出发&#xff0c;我们跟踪到…