当前位置:网站首页>Overview of AOSP ~ WiFi architecture

Overview of AOSP ~ WiFi architecture

2022-06-25 02:47:00 Nanke is cute

Android WiFi Architectural Overview

In this paper, Android Source project (AOSP) in WiFi Functional software architecture and each module ( Executable file 、 Dynamic link library ) The interface between .

SDK API

Android SDK For developers WiFi Programming interface (API) , It is very convenient to use .

Related packages :
android.net.wifi( Write App Just import The package , You can use WiFi Related functions )

Main related categories :

  • WifiManager WIFI Programming entry ,WIFI Most of the functionality of is provided in the form of methods of this class
  • WifiInfo Used to describe WIFI The state of the connection
  • ScanResult Used to describe a AP, Such as SSID, Signal strength , Safety mode, etc

Overview

 Insert picture description here
WifiManager It's all about managing Wifi Basic of connection API, Can pass :
android.content.Context.getSystemService(Context.WIFI_SERVICE)
Get an example of it .

Specifically IPC(Inter-Process communication)

App & system_server(WifiManager & WifiService)

if Binder Is the connection App and system_server The bridge , that WifiManager and WiFiService The two ends of the bridge .

framework Code and wifi dependent package be located :
frameworks/base/wifi/java (WIFI Some related packages )
frameworks/base/services/java ( Various Service,WIFI Related packages are :com.android.server.wifi )

frameworks In the code , and wifi The relationships of several related classes are as follows :

  • WifiService Inherited from IWifiManager.Stub;
  • IWifiManager.Stub And inherit from Binder, At the same time IWifiManager Interface ;
  • WifiManager.Stu.proxy It has also been realized. IWifiManager Interface ;

Pictured :
 Insert picture description here
among ,IWifiManager, IWifiManager.Stub, IWifiManager.Stub.Proxy All by IWifiManger.aidl Generate ;
aidl Automatically generate related java Code , Simplify the use of Binder Realization RPC The process of .
IWifiManager.Stub.Proxy,WifiManager,BinberProxy For clients (App process );
and IWifiManager.Stub,WifiService,Binder For the server (SystemServer process ).

App And system_server adopt Binder signal communication , but Binder In itself, it only realizes IPC, It's like socket The ability to communicate . and App Terminal WifiManager and system_server Terminal WifiService And Binder And so on RPC(remote procedure call).

WifiManager Only the system is app Provided interface .Context.getSystemService(Context.WIFI_SERVICE)
The actual object type returned is IWifiManager.Stub.Proxy.

IWifiManager.Stub.Proxy An example of is located at App A proxy on the end , Proxy image IWifiManager.Stub.Proxy
take WifiManager The parameters of the method are serialized to Parcel, Re menstruation Binder Send to system_server process .

system_server Internal WifiService closed App From the WifiManager call , Complete the actual work .
such , The actual work of communicating with the lower layer is done by App Transferred to system_server process (WifiService object ).

WifiStateMachine

in addition , You can see WifiStateMachine yes wifi Functional hub , The control flow of several different modes flows down through it .
When WIFI be in STA Pattern ( or P2P Pattern ) when , adopt WifiNative And wpa_supplicant Interaction .
WifiNative Several definitions Native Method :

    public native static boolean setMaxTxPower(int txpower, boolean sapRunning);

    public native static boolean loadDriver();

    public native static boolean isDriverLoaded();

    public native static boolean unloadDriver();

    public native static boolean startSupplicant(boolean p2pSupported, int firstScanDelay);

    /* Sends a kill signal to supplicant. To be used when we have lost connection or when the supplicant is hung */
    public native static boolean killSupplicant(boolean p2pSupported);

    private native boolean connectToSupplicantNative();

    private native void closeSupplicantConnectionNative();

    /** * Wait for the supplicant to send an event, returning the event string. * @return the event string sent by the supplicant. */
    private native String waitForEventNative();

    private native boolean doBooleanCommandNative(String command);

    private native int doIntCommandNative(String command);

    private native String doStringCommandNative(String command);

When WIFI be in AP Pattern . adopt NetworkManagementService And netd Interaction , Specifically through LocalSocket(Framework Packaged UNIX Domain socket) And netd Process of communication .
such as ,NetworkManagementService.startAccessPoint Method :

    @Override
    public void startAccessPoint(
            WifiConfiguration wifiConfig, String wlanIface) {
    
        mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
        try {
    
            wifiFirmwareReload(wlanIface, "AP");
            if (wifiConfig == null) {
    
                mConnector.execute("softap", "set", wlanIface); //  towards  netd  Send control command 
            } else {
    
                mConnector.execute("softap", "set", wlanIface, wifiConfig.SSID,
                                   wifiConfig.hiddenSSID ? "hidden" : "broadcast",
                                   "1", getSecurityType(wifiConfig),
                                   new SensitiveArg(wifiConfig.preSharedKey));
            }
            mConnector.execute("softap", "startap");
        } catch (NativeDaemonConnectorException e) {
    
            throw e.rethrowAsParcelableException();
        }
    }

WifiNative

functionally ,WifiNative yes system_server and wpa_supplicant Dialogue window , In fact, it mainly depends on wpa_supplicant The dynamic library compiled by the project libwpa_client.so.

WifiNative Several native The concrete implementation of the method is in :
frameworks/base/core/jni/android_net_wifi_WifiNative.cpp

WifiNative Of native Method implementation :

static JNINativeMethod gWifiMethods[] = {
    
    /* name, signature, funcPtr */
    {
     "loadDriver", "()Z",  (void *)android_net_wifi_loadDriver },
    {
     "isDriverLoaded", "()Z",  (void *)android_net_wifi_isDriverLoaded },
    {
     "unloadDriver", "()Z",  (void *)android_net_wifi_unloadDriver },
    {
     "startSupplicant", "(ZI)Z",  (void *)android_net_wifi_startSupplicant },
    {
     "killSupplicant", "(Z)Z",  (void *)android_net_wifi_killSupplicant },
    {
     "connectToSupplicantNative", "()Z", (void *)android_net_wifi_connectToSupplicant },
    {
     "closeSupplicantConnectionNative", "()V", (void *)android_net_wifi_closeSupplicantConnection },
    {
     "waitForEventNative", "()Ljava/lang/String;", (void*)android_net_wifi_waitForEvent },
    {
     "doBooleanCommandNative", "(Ljava/lang/String;)Z", (void*)android_net_wifi_doBooleanCommand },
    {
     "doIntCommandNative", "(Ljava/lang/String;)I", (void*)android_net_wifi_doIntCommand },
    {
     "doStringCommandNative", "(Ljava/lang/String;)Ljava/lang/String;", (void*) android_net_wifi_doStringCommand },
    {
     "setMaxTxPower", "(IZ)Z",  (void *)android_net_wifi_setMaxTxPower },
};

android_net_wifi_WifiNative.cpp Not much practical work done , Most of them call directly wifi.h wifi_maxtxpower.h Defined function :
wifi.h The concrete realization of is hardware/libhardware_legacy/wifi/wifi.c ( Will be compiled as libhardware_legacy.so)
wifi_maxtxpower.h The concrete realization of is hardware/qcom/wlan/libmaxtxpower/wifi_maxtxpower.c ( Will be compiled as libmaxtxpower.so)
therefore native Code dependency libhardware_legacy.so modular 、libmaxtxpower.so modular .

WIFI HAL
Realization SystemServer And wpa_supplicant(hostapd) communication , namely Wifi HAL.
Wifi HAL Encapsulates the UNIX Domain socket,SystemServer adopt UNIX Domain socket And wpa_supplicant(hostapd)
signal communication ;SystemServer Messages sent and wpa_supplicant The response messages are all for ASCII character string .

Wifi HAL The code is mainly distributed in :
hardware/libhardware_legacy/wifi
hardware/qcom/wlan/libmaxtxpower

Are compiled as :
hardware/libhardware_legacy/wifi -> libhardware_legacy.so
hardware/qcom/wlan/libmaxtxpower -> libmaxtxpower.so

wifi.h Defined WIFI HAL The interface of , The specific functions are :

//  Drive related :
int wifi_load_driver();
int wifi_unload_driver();
int is_wifi_driver_loaded();

// supplicant relevant :
int wifi_start_supplicant(int p2pSupported, int first_scan_delay);
int wifi_stop_supplicant(int p2pSupported);
int wifi_connect_to_supplicant();
void wifi_close_supplicant_connection();

//  wait for WIFI What happened , This function blocks the current call , Until there is wifi Event time , Returns a representation of wifi The character of the event 
int wifi_wait_for_event(char *buf, size_t len);

//  towards wifi Drive sends a life ,( Most functions send commands to the lower layer through this function )
int wifi_command(const char *command, char *reply, size_t *reply_len);

//  Initiate one dhcp request 
int do_dhcp_request(int *ipaddr, int *gateway, int *mask,
                   int *dns1, int *dns2, int *server, int *lease);

//  Return to one do_dhcp_request() Error string for 
const char *get_dhcp_error_string();

#define WIFI_GET_FW_PATH_STA 0
#define WIFI_GET_FW_PATH_AP 1
#define WIFI_GET_FW_PATH_P2P 2

//  Return an invitation firmware route 
const char *wifi_get_fw_path(int fw_type);

//  by wlan Drive change firmware route 
int wifi_change_fw_path(const char *fwpath);

#define WIFI_ENTROPY_FILE "/data/misc/wifi/entropy.bin"

int ensure_entropy_file_exists();

wifi_maxtxpower.h Only one function is defined :

int set_max_tx_power(int power, int sap_running);

android_net_wifi_WifiNative.cpp These functions are called .
wifi.c Called wpa_ctrl.h Some functions defined , and wpa_ctrl.h The function in
stay external/wpa_supplicant_8 Project implementation , And compiled as libwpa_client.so,
See external/wpa_supplicant_8/Android.mk.

wpa_supplicant(hostapd)

Code is located :
external/wpa_supplicant_8
The project contains two interrelated open source projects wpa_supplicant and hostapd, They will generate two executables :
wpa_supplicant and hostapd, Respectively STA Patterns and AP Mode daemon .

besides , Also generated for testing wpa_cli,hostapd_cli,
as well as WIFI HAL Rely on the wpa_client.so, The details can be found in Android.mk Find .

wpa_supplicant Source code structure and hostapd similar , I'll just introduce wpa_supplicant.

wpa_ctrl.h Defines and wpa_supplicant( or hostapd) Interface for process communication :

struct wpa_ctrl * wpa_ctrl_open(const char *ctrl_path);

// Close a control interface to wpa_supplicant/hostapd
void wpa_ctrl_close(struct wpa_ctrl *ctrl);

// Send a command to wpa_supplicant/hostapd
int wpa_ctrl_request(struct wpa_ctrl *ctrl, const char *cmd, size_t cmd_len,
             char *reply, size_t *reply_len,
             void (*msg_cb)(char *msg, size_t len));

// Register as an event monitor for the control interface
int wpa_ctrl_attach(struct wpa_ctrl *ctrl);

// Unregister event monitor from the control interface
int wpa_ctrl_detach(struct wpa_ctrl *ctrl);

// Receive a pending control interface message
int wpa_ctrl_recv(struct wpa_ctrl *ctrl, char *reply, size_t *reply_len);

// Check whether there are pending event messages
int wpa_ctrl_pending(struct wpa_ctrl *ctrl);

// Get file descriptor used by the control interface
int wpa_ctrl_get_fd(struct wpa_ctrl *ctrl);

char * wpa_ctrl_get_remote_ifname(struct wpa_ctrl *ctrl);

wpa_ctrl_open Create a UNIX Domain socket And wpa_supplicant( or hostapd) Processes are connected ,
wpa_ctrl_close Used to turn off wpa_ctrl_open Created connection ,
wpa_ctrl_request Used to direct to wpa_supplicant/hostapd Send control command , And block ,
until wpa_supplicant/hostapd Return the command response . Control commands and responses are ASCII character string .

wpa_ctrl.h In addition to declaring these functions , Also defined wpa_supplicant/hostapd Some news from , There is no detailed list of .

Source code wpa_supplicant_global_ctrl_iface_receive
Be responsible for dispatching control commands from the upper level , And then call the specific processing function :

 "ATTACH" -> wpa_supplicant_ctrl_iface_attach
 "DETACH" -> wpa_supplicant_ctrl_iface_detach
  else -> wpa_supplicant_global_ctrl_iface_process:
    "IFNAME="(prefix) //  Most control commands are in the form of IFNAME= start 
        -> wpas_global_ctrl_iface_ifname
            -> wpa_supplicant_ctrl_iface_process # "IFNAME=" Processing of the first command 
    wpas_global_ctrl_iface_redir 
        -> wpas_global_ctrl_iface_redir_p2p
            -> wpa_supplicant_ctrl_iface_process # "IFNAME=" Processing of the first command 
        -> wpas_global_ctrl_iface_redir_wfd
            -> wpa_supplicant_ctrl_iface_process # "IFNAME=" Processing of the first command 
    "PING" -> "PONG"
    "INTERFACE_ADD" -> wpa_supplicant_global_iface_add
    "INTERFACE_REMOVE" -> wpa_supplicant_global_iface_remove
    "INTERFACE_LIST" -> wpa_supplicant_global_iface_list
    "INTERFACES" -> wpa_supplicant_global_iface_interfaces
    "TERMINATE" -> wpa_supplicant_terminate_proc
    "SUSPEND" -> wpas_notify_suspend
    "RESUME" -> wpas_notify_resume
    "SET" -> wpas_global_ctrl_iface_set
    "SAVE_CONFIG" -> wpas_global_ctrl_iface_save_config
    "STATUS" -> wpas_global_ctrl_iface_status

The function is in wpa_supplicant In the catalog
ctrl_iface_unix.c and ctrl_iface_udp.c It can be realized in , It can be accessed from the android.config Switch
(android.config By Android.mk contain , Specific see Android.mk)

wpa_supplicant Communicate with the kernel
wpa_supplicant The running model of is single process and single thread Reactor(IO multiplexing).wpa_supplicant adopt NETLINK socket Communicate with the kernel .

wpa_supplicant The project supports multiple driver programming interfaces , stay Android It uses nl80211;

nl80211 It's new 802.11netlink Interface common header , And cfg80211 Together form Wireless-Extensions alternatives .
cfg80211 yes Linux 802.11 To configure API, nl80211 Used for configuration cfg80211 equipment , It is also used for kernel to user space communication .

The actual use nl80211 when , Just include the header file in the program

wireless module(in kernel)
Code is located :
kernel/net/wireless
nl80211.c Medium nl80211_init Use genl_register_family_with_ops Registered the response application
struct genl_ops nl80211_ops[], This array defines the response NETLINK Function of message .

and nl80211_init stay cfg80211_init Inside is called ,cfg80211_init Be being subsys_initcall Registered
Subsystem initialization program , Compiled into cfg80211.ko.

nl80211_ops[] excerpts :

static struct genl_ops nl80211_ops[] = {
    
    // ...
    {
    
        .cmd = NL80211_CMD_TRIGGER_SCAN,
        .doit = nl80211_trigger_scan,
        .policy = nl80211_policy,
        .flags = GENL_ADMIN_PERM,
        .internal_flags = NL80211_FLAG_NEED_NETDEV_UP |
                  NL80211_FLAG_NEED_RTNL,
    },
    {
    
        .cmd = NL80211_CMD_GET_SCAN,
        .policy = nl80211_policy,
        .dumpit = nl80211_dump_scan,
    },
    {
    
        .cmd = NL80211_CMD_START_SCHED_SCAN,
        .doit = nl80211_start_sched_scan,
        .policy = nl80211_policy,
        .flags = GENL_ADMIN_PERM,
        .internal_flags = NL80211_FLAG_NEED_NETDEV_UP |
                  NL80211_FLAG_NEED_RTNL,
    },
    {
    
        .cmd = NL80211_CMD_STOP_SCHED_SCAN,
        .doit = nl80211_stop_sched_scan,
        .policy = nl80211_policy,
        .flags = GENL_ADMIN_PERM,
        .internal_flags = NL80211_FLAG_NEED_NETDEV_UP |
                  NL80211_FLAG_NEED_RTNL,
    },

    // ...
};

wlan driver

Code is located :
vendor/qcom/opensource/wlan/prima

Module initialization (module_init), Module exit (module_exit):
CORE/HDD/src/wlan_hdd_main.c

Model :
Multithreading + queue

Create kernel thread :
CORE/VOSS/src/vos_sched.c Of vos_sched_open()

Thread task (vos_sched.c):

  • VosMcThread() - The VOSS Main Controller thread
  • VosWdThread() - The VOSS Watchdog thread
  • VosTXThread() - The VOSS Main Tx thread
  • VosRXThread() - The VOSS Main Rx thread

Thread environment (context) (vos_sched.h):

typedef struct _VosSchedContext
{
    
  /* Place holder to the VOSS Context */ 
   v_PVOID_t           pVContext; 

  /* WDA Message queue on the Main thread*/
   VosMqType           wdaMcMq;

   /* PE Message queue on the Main thread*/
   VosMqType           peMcMq;

   /* SME Message queue on the Main thread*/
   VosMqType           smeMcMq;

   /* TL Message queue on the Main thread */
   VosMqType           tlMcMq;

   /* SYS Message queue on the Main thread */
   VosMqType           sysMcMq;

  /* WDI Message queue on the Main thread*/
   VosMqType           wdiMcMq;

   /* WDI Message queue on the Tx Thread*/
   VosMqType           wdiTxMq;

   /* WDI Message queue on the Rx Thread*/
   VosMqType           wdiRxMq;

   /* TL Message queue on the Tx thread */
   VosMqType           tlTxMq;

   /* TL Message queue on the Rx thread */
   VosMqType           tlRxMq;

   /* SYS Message queue on the Tx thread */
   VosMqType           sysTxMq;

   VosMqType           sysRxMq;

    // ...

   struct task_struct* McThread;

   /* TX Thread handle */

   struct task_struct*   TxThread;

   /* RX Thread handle */
   struct task_struct*   RxThread;

    // ...
} VosSchedContext, *pVosSchedContext;

Qualcomm data :
80-Y0513-1_G_QCA_WCN36x0_Software_Architecture.pdf
chapter: Android WLAN Host Software Architecture

SCAN Process tracking

App

WifiManager wifiManager = (WifiManager)Context.getService(Contex.WIFI_SERVICE);
wifiManager.startScan();

//wifiManager --> IWifiManager.Stub.Proxy

IWifiManager.Stub.Proxy implements android.net.wifi.IWifiManager

wifiManager.startScan() 
-> IWifiManager.Stub.Proxy.startScan(WorkSource=null);
-> BinderProxy.transact(Stub.TRANSACTION_startScan, _data, _reply, 0);

systerm_server

wifi –> WifiService

WifiService extends IWifiManager.Stub

IWifiManager.Stub extends android.os.Binder 
    implements android.net.wifi.IWifiManager

-> IWifiManager.Stub.onTransact(int code, Parcel data, Parcel reply, int flags);
    case TRANSACTION_startScan:
-> WifiService.startScan(WorkSource workSource);
-> WifiStateMachine.startScan(int callingUid, WorkSource workSource);
-> StateMachine.sendMessage(CMD_START_SCAN, callingUid, 0, workSource);
    case CMD_START_SCAN:
->  handleScanRequest(WifiNative.SCAN_WITHOUT_CONNECTION_SETUP, message);
->  startScanNative(type, freqs)
-> WifiNative.scan(type, freqs)
->  doBooleanCommand("SCAN ..."); // AF_UNIX socket, send to wpa_supplicant

wpa_supplicant

external/wpa_supplicant_8/wpa_supplicant$ grep -nr "\"SCAN " .
./ChangeLog:197:      - "SCAN freq=<freq list>" can be used to specify which channels are
./ChangeLog:199:      - "SCAN passive=1" can be used to request a passive scan (no Probe
./ChangeLog:201:      - "SCAN use_id" can be used to request a scan id to be returned and
./ChangeLog:203:      - "SCAN only_new=1" can be used to request the driver/cfg80211 to
./ctrl_iface.c:6986:    } else if (os_strncmp(buf, "SCAN ", 5) == 0) {
    
./src/drivers/driver_test.c:1289:   ret = os_snprintf(pos, end - pos, "SCAN " MACSTR,
./src/drivers/driver_test.c:1994:   } else if (os_strncmp(buf, "SCAN ", 5) == 0) {
    
./ctrl_iface.c:6986:    } else if (os_strncmp(buf, "SCAN ", 5) == 0) {
    

refer to ./ctrl_iface.c:6986

    } else if (os_strcmp(buf, "SCAN") == 0) {
    
        wpas_ctrl_scan(wpa_s, NULL, reply, reply_size, &reply_len);
    } else if (os_strncmp(buf, "SCAN ", 5) == 0) {
    
        wpas_ctrl_scan(wpa_s, buf + 5, reply, reply_size, &reply_len);

wpas_ctrl_scan -> wpa_supplicant_req_scan

    int res = eloop_deplete_timeout(sec, usec, wpa_supplicant_scan, wpa_s,
                    NULL);
    if (res == 1) {
    
        wpa_dbg(wpa_s, MSG_DEBUG, "Rescheduling scan request: %d.%06d sec",
            sec, usec);
    }

wpa_supplicant_scan -> wpa_supplicant_trigger_scan

-> radio_add_work(wpa_s, 0, "scan", 0, wpas_trigger_scan_cb, ctx)

wpas_trigger_scan_cb -> wpa_drv_scan

static inline int wpa_drv_scan(struct wpa_supplicant *wpa_s,
                   struct wpa_driver_scan_params *params)
{
    
    if (wpa_s->driver->scan2) // callback
        return wpa_s->driver->scan2(wpa_s->drv_priv, params);
    return -1;
}

grep scan2 callback

external/wpa_supplicant_8/wpa_supplicant$ grep -nr "scan2\s*=" .
./src/drivers/driver_wext.c:2401:   .scan2 = wpa_driver_wext_scan,
./src/drivers/driver_privsep.c:726: .scan2 = wpa_driver_privsep_scan,
./src/drivers/driver_test.c:2677:   .scan2 = wpa_driver_test_scan,
./src/drivers/driver_bsd.c:1618:    .scan2          = wpa_driver_bsd_scan,
./src/drivers/driver_nl80211.c:12612:   .scan2 = driver_nl80211_scan2,
./src/drivers/driver_ndis.c:3217:   wpa_driver_ndis_ops.scan2 = wpa_driver_ndis_scan;

refer to ./src/drivers/driver_nl80211.c:12612
driver_nl80211_scan2 -> wpa_driver_nl80211_scan

    msg = nl80211_scan_common(drv, NL80211_CMD_TRIGGER_SCAN, params,
                  bss->wdev_id_set ? &bss->wdev_id : NULL);
    if (!msg)
        return -1;

use NL80211_CMD_TRIGGER_SCAN talk with kernel(cfg80211.ko)

kernel

grep NL80211_CMD_TRIGGER_SCAN in kernel source:

kernel$ cgrep NL80211_CMD_TRIGGER_SCAN
./net/wireless/nl80211.c:9053:      .cmd = NL80211_CMD_TRIGGER_SCAN,
./net/wireless/nl80211.c:9605:                NL80211_CMD_TRIGGER_SCAN) < 0) {
    
./include/uapi/linux/nl80211.h:255: *   option to specify additional IEs in NL80211_CMD_TRIGGER_SCAN,
./include/uapi/linux/nl80211.h:260: * @NL80211_CMD_TRIGGER_SCAN: trigger a new scan with the given parameters
./include/uapi/linux/nl80211.h:759: NL80211_CMD_TRIGGER_SCAN,
./include/uapi/linux/nl80211.h:1362: *  This attribute is used with %NL80211_CMD_TRIGGER_SCAN and
./include/uapi/linux/nl80211.h:3863: * of NL80211_CMD_TRIGGER_SCAN and NL80211_CMD_START_SCHED_SCAN

refer to net/wireless/nl80211.c:9053

static struct genl_ops nl80211_ops[] = {
    

// ... ...

    {
    
        .cmd = NL80211_CMD_TRIGGER_SCAN,
        .doit = nl80211_trigger_scan,
        .policy = nl80211_policy,
        .flags = GENL_ADMIN_PERM,
        .internal_flags = NL80211_FLAG_NEED_WDEV_UP |
                  NL80211_FLAG_NEED_RTNL,
    },

// ... ...
};

nl80211_trigger_scan -> rdev_scan(rdev, request);

static inline int rdev_scan(struct cfg80211_registered_device *rdev,
                struct cfg80211_scan_request *request)
{
    
    int ret;
    trace_rdev_scan(&rdev->wiphy, request);
    ret = rdev->ops->scan(&rdev->wiphy, request); // callback
    trace_rdev_return_int(&rdev->wiphy, ret);
    return ret;
}
原网站

版权声明
本文为[Nanke is cute]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/176/202206242332264907.html