天天看點

網絡問題排查實戰經典案例彙總

作者:IT運維技術圈

現在大部分軟硬體系統都是基于網絡的,有走區域網路(私網)的,有走外網(公網)的,會不可避免地出現很多與網絡相關的問題,特别是将産品部署到安全級别較高的客戶環境中,會出現各式各樣的複雜網絡問題。今天我們就來分享一下實際項目中遇到的多個網絡問題,以供參考!

網絡問題排查實戰經典案例彙總

1、Windows防火牆攔截了用戶端發來的TCP連接配接請求,導緻用戶端與伺服器建鍊失敗

這是一個Windows系統自帶的防火牆攔截程式網絡資料的例子。用戶端和伺服器程式運作在兩台Windows電腦上,用戶端需要連接配接到遠端的伺服器上擷取資料,但用戶端始終連接配接不上遠端的伺服器。用wireshark抓包看,用戶端給伺服器發送TCP三向交握的SYN包,伺服器始終沒有回應,導緻TCP連接配接始終無法建立。

我們先是ping了伺服器所在機器的IP位址,是能ping通的。又在伺服器所在電腦上使用netstat -a指令檢視到伺服器的9001端口是出于Listening監聽狀态的。

如下所示:

網絡問題排查實戰經典案例彙總

這就奇怪了,伺服器的ip能ping的通的,伺服器的端口也處于正常的監聽狀态,為啥始終沒法和伺服器建鍊呢?

後來想到我們在Widnows系統上第一次運作程式時,一般都會彈出類似下面的截圖:

網絡問題排查實戰經典案例彙總

一般情況下我們使用預設的選擇,沒有全部勾選,直接就點選下面的“允許通路”的按鈕了。視窗中提示Windows防火牆已經阻止了部分功能,應該是将公網網絡和專網網絡都勾選上的,估計是Windows防火牆将發給該伺服器程式的部分資料包攔截了,于是将伺服器程式所在系統的Windows防火牆關閉,然後用戶端可以正常連接配接了。

其實可以在伺服器側抓包,用戶端發來的用于三次握手的SYN包,伺服器所在機器的網卡應該收到了,隻是向應用層傳遞資料時資料被防火牆攔截了。

最終的解決辦法是允許該伺服器程式能通過防火牆進行通信,在控制台中點選系統和安全->Widnows Defender防火牆->允許應用或功能通過Windows防火牆,在打開的界面中找到伺服器程式:

網絡問題排查實戰經典案例彙總

将專用網絡和公用都勾上,點選确定就好了。即允許伺服器程式通過防火牆進行通信,防火牆就不會攔截發給伺服器的資料包了。

2、在Linux伺服器側抓包選錯網卡,導緻伺服器側的抓包資訊與用戶端的對不上

用戶端和伺服器通信的過程中出現了問題,導緻業務出現了異常,于是要在用戶端和服務端兩側抓包,對照兩邊的網絡資料包,看看到底是哪一側出問題了。

用戶端運作在Windows系統中,直接啟動WireShark就可以直擊抓包了。伺服器運作在遠端的Linux系統上,需要使用SSH工具遠端登入到Linux系統中,然後使用tcpdump指令進行抓包,然後再将抓封包件下載下傳到Windows系統中,然後使用WireShark打開檢視。

打開伺服器的抓封包件後,發現有問題,和用戶端抓的資料包對不上,伺服器側的抓封包件中顯示的伺服器IP位址,和終端側抓封包件中顯示的伺服器IP位址是不一緻的。伺服器側抓到的包中顯示的是伺服器IP是内網的IP,而終端側抓包顯示連接配接的伺服器IP是外網的IP,是以兩邊對不上的,後來想起來可能輸入tcpdump指令時選錯了網卡導緻的。

後經平台側的運維同僚确認,Linux伺服器上确實有兩張實體網卡,在Linux指令行中使用ifconfig指令就可以檢視到伺服器上的網卡資訊,一個是配置了内網的eth0網卡,一個是配置了外網IP的eth1網卡:

網絡問題排查實戰經典案例彙總

是以要修改之前輸入的tcpdump指令,指令中指定抓eth1網卡的資料包:

tcpdump -i eth1 -s 0 -w dvsserver.pcap

或者抓所有網卡的資料包:

tcpdump -i any -s 0 -w dvsserver.pcap

3、更新伺服器的端口改變了,導緻軟體無法進行線上更新

網絡問題排查實戰經典案例彙總

某日測試同僚在用戶端軟體上發起線上版本監測,結果始終連不上伺服器。使用wireshark抓包看到,軟體在發送TCP三向交握的SYN包後,遠端伺服器直接回了個RST包,強行将用戶端的連接配接請求給終止了。

首先,伺服器回包了,那伺服器肯定是能ping通的,于是使用telnet指令檢測更新伺服器的63000服務端口是否正常,結果該端口是連不上的。一般情況下直接回複RST可能是端口不存在引起的,經後來和更新伺服器開發确認,更新伺服器的端口已經變更了,不再是之前的60000端口号了。

其實這個問題中,還有兩點是有問題的:

(1)用戶端軟體側處理的有問題,不應該将更新伺服器的端口在代碼中固定為某個數字,應該使用登入時平台傳回的更新伺服器端口。

(2)平台變更端口後應該發郵件通知用戶端側,平台側應該對老的用戶端提供相容支援,老的版本已經釋出已經傳遞給客戶使用,平台要對老版本做相容,應該做個端口重定向,老版本使用60000端口發起連接配接時應該重定向到最新的端口上。

4、Linux伺服器系統中開啟了reuse和recycle選項,導緻用戶端會時不時連不上伺服器

網絡問題排查實戰經典案例彙總

有使用者回報軟體用戶端會時不時出現無法登入伺服器的問題。使用Wireshark抓包看,用戶端在發出三次握手的SYN包,始終收不到伺服器的ACK包,甚至觸發了用戶端的丢包重傳,即多次發送SYN包,伺服器都沒有回應,導緻用戶端和伺服器建TCP連接配接失敗。

後來在平台側也進行了抓包,發現伺服器确實收到了用戶端發來的SYN包,但就是沒有回ACK應答包。經排查得知,伺服器的Linux系統的TCP/IP協定棧開啟了reuse和recycle選項,這和協定棧的timestamp時間戳政策會沖突,如果短時間内多次收到SYN包,平台側TCP/IP協定棧會直接将請求拒絕掉,不給SYN包發送端任何回應。

伺服器側這兩個選項一般都不能開啟,特别是tcp_tw_recycle選項開啟後,可能會導緻部分連接配接請求不響應,導緻連接配接失敗。在伺服器側,可以通過指令直接将這兩個選項關閉掉:

echo 0 > /proc/sys/net/ipv4/tcp_tw_reuse              echo 0 > /proc/sys/net/ipv4/tcp_tw_recycle           

關于這兩個選項的說明如下:

(1)tcp_tw_reuse:主要用于端口複用,用在用戶端側,将其設定為1表示允許将TIME-WAIT sockets重新用于新的TCP連接配接,預設為0,表示關閉。(2)tcp_tw_recycle:将其設定為1表示開啟TCP連接配接中TIME-WAIT sockets的快速回收,預設為0,表示關閉。

5、Windows系統中使用雙網卡時,可能需要添加政策路由

在一台測試用的Windows PC機上,配置了兩張網卡,一張是連接配接外網的網卡,用于上外網;一張是連接配接内網的網卡,用于連區域網路

如下:

而Windows作業系統隻允許設定一個預設網關,是以隻能有一個網卡配置預設網關,一般是連接配接外網的網卡配置預設網關,因為外網的IP位址是不固定的。内網的網卡則不配置網關,對于内網的IP位址是相對固定的,比如以192.168開頭的、以172.16開頭的、以10.開頭的。當要通路這些位址時,可以通過添加政策路由的方式指定通路這些開頭的IP位址從指定的區域網路的網關出去。

隻要是有網卡配置了預設網關,都會在路由表中添加一條走預設網關的預設路由,如下所示:

網絡問題排查實戰經典案例彙總

通過指令去添加政策路由時,也會向系統中添加對應的路由條目,添加政策路由的指令如下:

route add 172.16.0.0 mask 255.255.0.0 172.16.125.88(内網的網關)           

這條添加路由指令的含義是:所有通路以172.16開頭的IP位址,都從網關172.16.125.88出去。

當我們發起對一個IP位址的通路時(也可能通過域名去通路,會先将域名解析為IP位址,然後用IP位址去通路),系統在查找路由時,會優先比對系統的非預設路由,即會比對添加的政策路由,當比對不上時才會去使用預設路由。

是以,通路外網的位址時會走連接配接外網的網卡出去,通路以192.168開頭的、以172.16開頭的等内網位址時,會走政策路由中指定的連接配接内網的網關出去。

6、連接配接線路中的網絡裝置将用戶端與其之間的連接配接單方面關閉掉,導緻後續登入伺服器時出現異常

在公司區域網路的測試環境中,用戶端自動重連伺服器出現問題。根據列印日志發現,用戶端和伺服器之間的TCP長連接配接因為網絡問題出現斷鍊,用戶端在收到斷鍊通知後,會去自動重連伺服器,但始終都連接配接不上。

根據列印日志看到,伺服器傳回的錯誤碼是使用者已登入。這個就奇怪了,明明是用戶端收到與伺服器的連接配接斷開的通知後去重連的,為啥伺服器側還回報我們的賬号還處于登入狀态呢?既然連接配接斷了,伺服器應該也能感覺到的,賬戶不太可能還出于連接配接狀态的!

于是使用SSH遠端登入到伺服器上,使用netstat檢視伺服器目前的TCP連接配接清單,在清單中看到了用戶端的IP,用戶端居然和伺服器還處于連接配接狀态。

于是找公司大牛幫忙排查分析一下,他查下來懷疑可能是用戶端與伺服器之間的路由器單方面将路由器與用戶端之間的鍊路給斷開了,但路由器與伺服器之間的鍊路還保持着,還沒斷開。該路由器是好多年前購買的老式華為路由器,可能是路由器有問題,估計是因為用戶端與伺服器長時間沒有資料互動,路由器認為用戶端與其的鍊路失去活性了,強行将其與用戶端之間的鍊路釋放了。

網絡問題排查實戰經典案例彙總

用戶端與伺服器之間使用websocket網絡庫(libwebsockets開源庫)進行通信的,libwebsockets庫支援開啟心跳機制、設定心跳參數的。為了解決連接配接鍊路上長時間不跑資料導緻鍊路被釋放問題,在初始化libwebsockets庫時,設定一下心跳參數就可以了。

libwebsockets庫中設定心跳參數的結構體如下所示:

/**              * struct lws_context_creation_info - parameters to create context with              *              * This is also used to create vhosts.... if LWS_SERVER_OPTION_EXPLICIT_VHOSTS              * is not given, then for backwards compatibility one vhost is created at              * context-creation time using the info from this struct.              *              * If LWS_SERVER_OPTION_EXPLICIT_VHOSTS is given, then no vhosts are created              * at the same time as the context, they are expected to be created afterwards.              *              * @port: VHOST: Port to listen on... you can use CONTEXT_PORT_NO_LISTEN to              * suppress listening on any port, that's what you want if you are              * not running a websocket server at all but just using it as a              * client              * @iface: VHOST: to bind the listen socket to all interfaces, or the              * interface name, eg, "eth2"              * If options specifies LWS_SERVER_OPTION_UNIX_SOCK, this member is              * the pathname of a UNIX domain socket. you can use the UNIX domain              * sockets in abstract namespace, by prepending an @ symbole to the              * socket name.              * @protocols: VHOST: Array of structures listing supported protocols and a protocol-              * specific callback for each one. The list is ended with an              * entry that has a callback pointer.              * It's not const because we write the owning_server member              * @extensions: VHOST: or array of lws_extension structs listing the              * extensions this context supports. If you configured with              * --without-extensions, you should give here.              * @token_limits: CONTEXT: or struct lws_token_limits pointer which is initialized              * with a token length limit for each possible WSI_TOKEN_***              * @ssl_cert_filepath: VHOST: If libwebsockets was compiled to use ssl, and you want              * to listen using SSL, set to the filepath to fetch the              * server cert from, otherwise for unencrypted              * @ssl_private_key_filepath: VHOST: filepath to private key if wanting SSL mode;              * if this is set to but sll_cert_filepath is set, the              * OPENSSL_CONTEXT_REQUIRES_PRIVATE_KEY callback is called              * to allow setting of the private key directly via openSSL              * library calls              * @ssl_ca_filepath: VHOST: CA certificate filepath or               * @ssl_cipher_list: VHOST: List of valid ciphers to use (eg,              * "RC4-MD5:RC4-SHA:AES128-SHA:AES256-SHA:HIGH:!DSS:!a"              * or you can leave it as to get "DEFAULT"              * @http_proxy_address: VHOST: If non-, attempts to proxy via the given address.              * If proxy auth is required, use format              * "username:password@server:port"              * @http_proxy_port: VHOST: If http_proxy_address was non-, uses this port at              * the address              * @gid: CONTEXT: group id to change to after setting listen socket, or -1.              * @uid: CONTEXT: user id to change to after setting listen socket, or -1.              * @options: VHOST + CONTEXT: 0, or LWS_SERVER_OPTION_... bitfields              * @user: CONTEXT: optional user pointer that can be recovered via the context              * pointer using lws_context_user              * @ka_time: CONTEXT: 0 for no keepalive, otherwise apply this keepalive timeout to              * all libwebsocket sockets, client or server              * @ka_probes: CONTEXT: if ka_time was nonzero, after the timeout expires how many              * times to try to get a response from the peer before giving up              * and killing the connection              * @ka_interval: CONTEXT: if ka_time was nonzero, how long to wait before each ka_probes              * attempt              * @provided_client_ssl_ctx: CONTEXT: If non-, swap out libwebsockets ssl              * implementation for the one provided by provided_ssl_ctx.              * Libwebsockets no longer is responsible for freeing the context              * if this option is selected.              * @max_http_header_data: CONTEXT: The max amount of header payload that can be handled              * in an http request (unrecognized header payload is dropped)              * @max_http_header_pool: CONTEXT: The max number of connections with http headers that              * can be processed simultaneously (the corresponding memory is              * allocated for the lifetime of the context). If the pool is              * busy new incoming connections must wait for accept until one              * becomes free.              * @count_threads: CONTEXT: how many contexts to create in an array, 0 = 1              * @fd_limit_per_thread: CONTEXT: nonzero means restrict each service thread to this              * many fds, 0 means the default which is divide the process fd              * limit by the number of threads.              * @timeout_secs: VHOST: various processes involving network roundtrips in the              * library are protected from hanging forever by timeouts. If              * nonzero, this member lets you set the timeout used in seconds.              * Otherwise a default timeout is used.              * @ecdh_curve: VHOST: if , defaults to initializing server with "prime256v1"              * @vhost_name: VHOST: name of vhost, must match external DNS name used to              * access the site, like "warmcat.com" as it's used to match              * Host: header and / or SNI name for SSL.              * @plugin_dirs: CONTEXT: , or -terminated array of directories to              * scan for lws protocol plugins at context creation time              * @pvo: VHOST: pointer to optional linked list of per-vhost              * options made accessible to protocols              * @keepalive_timeout: VHOST: (default = 0 = 60s) seconds to allow remote              * client to hold on to an idle HTTP/1.1 connection              * @log_filepath: VHOST: filepath to append logs to... this is opened before              * any dropping of initial privileges              * @mounts: VHOST: optional linked list of mounts for this vhost              * @server_string: CONTEXT: string used in HTTP headers to identify server              * software, if , "libwebsockets".              */              struct lws_context_creation_info {              int port; /* VH */              const char *iface; /* VH */              const struct lws_protocols *protocols; /* VH */              const struct lws_extension *extensions; /* VH */              const struct lws_token_limits *token_limits; /* context */              const char *ssl_private_key_password; /* VH */              const char *ssl_cert_filepath; /* VH */              const char *ssl_private_key_filepath; /* VH */              const char *ssl_ca_filepath; /* VH */              const char *ssl_cipher_list; /* VH */              const char *http_proxy_address; /* VH */              unsigned int http_proxy_port; /* VH */              int gid; /* context */              int uid; /* context */              unsigned int options; /* VH + context */              void *user; /* context */              int ka_time; /* context */              int ka_probes; /* context */              int ka_interval; /* context */              #ifdef LWS_OPENSSL_SUPPORT              SSL_CTX *provided_client_ssl_ctx; /* context */              #else /* maintain structure layout either way */              void *provided_client_ssl_ctx;              #endif              short max_http_header_data; /* context */              short max_http_header_pool; /* context */              unsigned int count_threads; /* context */              unsigned int fd_limit_per_thread; /* context */              unsigned int timeout_secs; /* VH */              const char *ecdh_curve; /* VH */              const char *vhost_name; /* VH */              const char * const *plugin_dirs; /* context */              const struct lws_protocol_vhost_options *pvo; /* VH */              int keepalive_timeout; /* VH */              const char *log_filepath; /* VH */              const struct lws_http_mount *mounts; /* VH */              const char *server_string; /* context */              /* Add new things just above here ---^              * This is part of the ABI, don't needlessly break compatibility              *              * The below is to ensure later library versions with new              * members added above will see 0 (default) even if the app              * was not built against the newer headers.              */              void *_unused[8];              };           

上述結構體中的ka_time、ka_interval和ka_probes三個字段,是心跳參數,這三個參數的含義是:

ka_time:兩個心跳包之間的時間間隔;

ka_interval:給對端發送心跳包之後,收不到對端ACK确認逾時時間;

ka_probes:心跳包探測次數。

我們在調用lws_create_context接口初始化libwebsockets庫時,可以指定這三個參數

static lws_context* CreateContext              {              lws_set_log_level( 0xFF,  );              lws_context* plcContext = ;              lws_context_creation_info tCreateinfo;              memset(&tCreateinfo, 0, sizeof tCreateinfo);              tCreateinfo.port = CONTEXT_PORT_NO_LISTEN;              tCreateinfo.protocols = protocols;              tCreateinfo.ka_time = 10; // 心跳包廂的時間間隔              tCreateinfo.ka_interval = 10; // 發出心跳包後沒有收到ACK确認包時重發心跳包的逾時時間              tCreateinfo.ka_probes = 3; // 心跳探測次數,對于windows作業系統,此設定是無效的,Windows系統時固定為10次,不可修改              tCreateinfo.options = LWS_SERVER_OPTION_DISABLE_IPV6;              plcContext = lws_create_context(&tCreateinfo);              return plcContext;              }           

跟進libwebsockets庫的開源代碼中,函數lws_create_context的内部,最終調用的是lws_plat_set_socket_options接口,該接口内部最終是給對應的socket套接字設定心跳參數的,如下:

LWS_VISIBLE int              lws_plat_set_socket_options(struct lws_vhost *vhost, lws_sockfd_type fd)              {              int optval = 1;              int optlen = sizeof(optval);              u_long optl = 1;              DWORD dwBytesRet;              struct tcp_keepalive alive;              int protonbr;              #ifndef _WIN32_WCE              struct protoent *tcp_proto;              #endif              if (vhost->ka_time) {              /* enable keepalive on this socket */              // 先調用setsockopt打開發送心跳包(設定)選項              optval = 1;              if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE,              (const char *)&optval, optlen) < 0)              return 1;              alive.onoff = TRUE;              alive.keepalivetime = vhost->ka_time*1000;              alive.keepaliveinterval = vhost->ka_interval*1000;              if (WSAIoctl(fd, SIO_KEEPALIVE_VALS, &alive, sizeof(alive),              , 0, &dwBytesRet, , ))              return 1;              }              /* Disable Nagle */              optval = 1;              #ifndef _WIN32_WCE              tcp_proto = getprotobyname("TCP");              if (!tcp_proto) {              lwsl_err("getprotobyname failed with error %d\n", LWS_ERRNO);              return 1;              }              protonbr = tcp_proto->p_proto;              #else              protonbr = 6;              #endif              setsockopt(fd, protonbr, TCP_NODELAY, (const char *)&optval, optlen);              /* We are nonblocking... */              ioctlsocket(fd, FIONBIO, &optl);              return 0;              }           
是以libwebsockets庫的心跳設定,使用的還是TCPIP協定棧的心跳,不是應用層自己實作的心跳機制。

關于TCPIP協定棧的三個心跳參數的詳細說明如下:

(1)keepalivetime:心跳正常時,本端發送一個心跳包給對端,收到對端心跳包的回應,間隔keepalivetime時間後,發下一包心跳包,windows預設的心跳包發送間隔是2小時。

(2)keepaliveinterval:心跳異常時,本端發送心跳包後沒收到對端的回應,間隔keepaliveinterval時間後,發送下一個心跳包(繼續探測)。如果多次沒有收到對端的回應,當探測次數達到上限(keep-alive probes)時,則協定棧認為連接配接出問題。

(3)keep-alive probes:windows系統中 ,心跳包探測次數keep-alive probes是不可改變的,協定棧固定為10次。

7、在複雜網絡環境中主從伺服器切換時遇到的多個網絡異常問題

網絡問題排查實戰經典案例彙總

主伺服器和從伺服器共用一個IP,當主伺服器出問題時,切換到從伺服器上,然後伺服器以多點傳播的方式将搶IP的資料包發出去,這個資料包始終沒有發出來,導緻搶IP操作失敗。通過排查得知,多點傳播資料包會被客戶網絡環境中的一台華為路由器攔截,可能是這台華為路由器有問題,但客戶要求我們從我們伺服器這一側去修改,後來将多點傳播改成單點傳播才解決問題。

客戶的網絡裝置上配置了很多安全規則,其中一個規則是将IP-MAC位址綁定,如果裝置的IP和MAC位址對不上,裝置發出來的資料包就會被網絡裝置認為是不安全的資料,會直接被攔截。在從伺服器拿到主伺服器的IP之後,IP對應的MAC位址就變了,正好就觸發了這個IP-MAC綁定規則,導緻資料包被攔截。後來的解決辦法是将主從伺服器公用的IP作為特例進行放行,即對這個IP不進行攔截。

8、Linux系統的TCP/IP協定棧重定向選項被關閉,無法響應網關發來的ICMP重定向消息,導緻收發資料時出現嚴重的丢包問題

網絡問題排查實戰經典案例彙總

給客戶部署的系統中,有台裝置放置于某個網絡節點下,給該裝置配置了該節點下的預設網關,結果聯調下來發現,所有的其他節點下的其他裝置都沒問題,就這台裝置有問題,這台裝置發出來的資料有嚴重的丢包問題。

現場人員和客戶一起做了對比測試,把客戶之前購買的别的廠商的裝置放置在該網絡節點下,别的廠商的裝置都沒有丢包問題,就我們公司的裝置有問題。期間,我們給客戶調撥了一個我們幾年前研發的一款老式裝置,放置在該節點下也沒問題,就目前使用的新式裝置有問題。

這個問題折騰的比較久,始終沒有查出來問題,後來找公司的頂級專家來排查,才查出來問題。這台裝置發出去的資料,預設情況下都要通過其配置的預設網關發出去,抓包發現,預設網關會給裝置發了ICMP重定向消息,該消息中攜帶一個IP位址,該消息是用來告訴裝置,要發送資料都從這個IP發出去。

一般情況下,協定棧在收到這個ICMP重定向消息後,會向系統路由表中添加一條路由,這樣要發送的資料會使用這條路由中的IP發送出去。通過大量的抓包分析之後,找到了問題的症結,是因為裝置内置的Linux系統的TCP/IP協定棧的重定向選項都被關閉導緻的,在linux指令行使用指令sysctrl -a | grep redirects可以檢視到:

網絡問題排查實戰經典案例彙總

我們硬體裝置中的使用的Linux系統是經過裁剪後部署進去的,之前在系統裁剪時,出于安全考慮,将系統的TCP/IP網絡協定棧中所有重定向選項都關閉了,是以此案例中預設網關發過來的ICMP重定向消息被丢棄了,導緻發出的資料還是發到預設網關上,但從預設網關出去的資料會有明顯的丢包問題(客戶網絡環境故意這麼處理的,不讓資料從預設網關出去),是以出現了最開始出現的問題。

此問題的臨時解決辦法是手動将這些重定向選項打開,後續進行Linux系統裁剪時要将這些重定向選項打開。

原作者:dvlinker

源連結:https://blog.csdn.net/chenlycly/article/details/124643918

編輯:IT運維技術圈

小編有話說

➤推薦服務:

向下滑動檢視更多

點選【IT面試精選】檢視全網最權威的一線大廠面試真題及面試經驗,每天更新哦!

點選【IT路邊社】檢視實時更新的IT新聞資訊

點選【2022網際網路大事件盤點】檢視2022網際網路最全大事件盤點

回複【加群】群滿啦!~添加波哥微信拉您進群!

點選【安全加強】擷取最新安全加強腳本

點選【一鍵iptables腳本】擷取iptables自動設定腳本

繼續閱讀