自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux 內(nèi)核網(wǎng)絡(luò)之 tcp 三次握手

系統(tǒng) Linux
今天我們一起來(lái)了解一下Linux 內(nèi)核網(wǎng)絡(luò)之 tcp 三次握手的相關(guān)流程。

三次握手流程如下圖:

服務(wù)器端監(jiān)聽(tīng)

在client端向server端進(jìn)行連接前,server處于監(jiān)聽(tīng)狀態(tài)。流程如下:

int reqsk_queue_alloc(struct request_sock_queue *queue,
unsigned int nr_table_entries)
{
size_t lopt_size = sizeof(struct listen_sock);
struct listen_sock *lopt;
//計(jì)算半連接隊(duì)列的長(zhǎng)度
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = max_t(u32, nr_table_entries, 8);
..
//為半連接隊(duì)列申請(qǐng)內(nèi)存
lopt_size += nr_table_entries * sizeof(struct request_sock );
if (lopt_size > PAGE_SIZE)
/ 如果申請(qǐng)內(nèi)存大于1頁(yè),則申請(qǐng)?zhí)摂M地址連續(xù)的空間 /
lopt = __vmalloc(lopt_size,GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO,PAGE_KERNEL);
else
/ 申請(qǐng)內(nèi)存在1頁(yè)內(nèi),則申請(qǐng)物理地址連續(xù)的空間 */
lopt = kzalloc(lopt_size, GFP_KERNEL);
//全連接隊(duì)列頭初始化
queue->rskq_accept_head = NULL;
// 半連接隊(duì)列的最大長(zhǎng)度
lopt->nr_table_entries = nr_table_entries;
...
//半連接隊(duì)列設(shè)置
queue->listen_opt = lopt;
return 0;
}

服務(wù)器端在監(jiān)聽(tīng)時(shí)初始化半連接 hash 表,然后掛到接收隊(duì)列中,等待客戶端連接。

另外 queue->rskq_accept_head為全連接隊(duì)列,是一個(gè)以鏈表的形式進(jìn)行管理全連接。

關(guān)于半連接和全連接的結(jié)構(gòu)如下:

client 發(fā)送 SYN 報(bào)文

client 向 server 進(jìn)行 connect 時(shí),最終會(huì)調(diào)用 tcp_v4_connect 向server發(fā)送 SYN 報(bào)文。

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
//設(shè)置 socket 狀態(tài)為 TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
...
//函數(shù)用來(lái)根據(jù) sk 中的信息,構(gòu)建一個(gè)完成的 syn 報(bào)文,并將它發(fā)送出去。
err = tcp_connect(sk);
...
}
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
tcp_connect_init(sk);
//申請(qǐng) skb 并構(gòu)造為一個(gè) SYN 包
buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
...
//添加到發(fā)送隊(duì)列 sk_write_queue 上
__skb_queue_tail(&sk->sk_write_queue, buff);
...
//發(fā)送syn報(bào)文
tcp_transmit_skb(sk, buff, 1, GFP_KERNEL);
...
/* Timer for repeating the SYN until an answer. */
//啟動(dòng)重傳定時(shí)器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}

在 tcp_connect 中申請(qǐng)并構(gòu)造一個(gè)SYN 包,然后將其發(fā)出。同時(shí)還啟動(dòng)了一個(gè)重傳定時(shí)器,該定時(shí)器的作用就是等到一定時(shí)間后若收不到服務(wù)器的反饋時(shí)進(jìn)行重傳。

server 端接收 SYN 并發(fā)送 SYN+ACK

所有到server端的tcp數(shù)據(jù)包都會(huì)經(jīng)過(guò)網(wǎng)卡、軟中斷,最終到 tcp_v4_rcv。

在 tcp_v4_rcv 中根據(jù)報(bào)文報(bào)文頭中的信息,從監(jiān)聽(tīng)的hash 表 listening_hash 中找到對(duì)應(yīng)監(jiān)聽(tīng)的 socket 結(jié)構(gòu)。然后進(jìn)入 tcp_v4_do_rcv

進(jìn)行握手處理。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
//服務(wù)器收到第一步握手 SYN 或者第三步 ACK 都會(huì)走到這里
if (sk->sk_state == TCP_LISTEN) {
//從半連接表syn_table中取出節(jié)點(diǎn)
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
...
}
}

由于當(dāng)前 socket 是 listen 狀態(tài),首先會(huì)到 tcp_v4_hnd_req 去查看半連接隊(duì)列。服務(wù)器第一次響應(yīng) SYN 的時(shí)候,半連接隊(duì)列里是空的,所以相當(dāng)于什么也沒(méi)干就返回了。

static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
// 從半連接隊(duì)里中查詢
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr);
return sk;
}
struct request_sock *inet_csk_search_req(const struct sock *sk,
struct request_sock ***prevp,
const __be16 rport, const __be32 raddr,
const __be32 laddr)
{
for (prev = &lopt->syn_table[inet_synq_hash(raddr, rport, lopt->hash_rnd,
lopt->nr_table_entries)];
(req = *prev) != NULL;
prev = &req->dl_next) {
...
}
}
return req;
}

在 tcp_rcv_state_process 中根據(jù)各種狀態(tài)做不同的處理。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
...
case TCP_LISTEN:
...
//判斷是否為 SYN 包
if(th->syn) {
//調(diào)用 tcp_v4_conn_request
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
...
}
goto discard;
case TCP_SYN_SENT:
...
return 0;
}

由于對(duì)方發(fā)來(lái)的 SYN 報(bào)文, 調(diào)用 tcp_v4_conn_request 進(jìn)行處理。

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
...
//查看半連接隊(duì)列是否已滿
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
//在全連接隊(duì)列滿的情況下,如果有 young_ack,那么直接丟
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
//分配 request_sock 內(nèi)核對(duì)象,該request_sock->sk 此時(shí)還為空
req = reqsk_alloc(&tcp_request_sock_ops);
if (!req)
goto drop;
...
tcp_rsk(req)->snt_isn = isn;
// 發(fā)送 syn+ack 包
if (tcp_v4_send_synack(sk, req, dst))
goto drop_and_free;
if (want_cookie) {
reqsk_free(req);
} else {
//添加到半連接隊(duì)列,并開(kāi)啟計(jì)時(shí)器,
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
}
return 0;
}

inet_csk_reqsk_queue_is_full 如果返回 true 就表示半連接隊(duì)列滿了,另外 sysctl_tcp_syncookies 判斷是否打開(kāi)了內(nèi)核參數(shù) tcp_syncookies,如果未打開(kāi)則返回 false。

如果半連接隊(duì)列滿了,而且 ipv4.tcp_syncookies 參數(shù)設(shè)置為 0,那么來(lái)自客戶端的握手包將 goto drop,也就是數(shù)據(jù)包直接丟棄,此時(shí)客戶端感知不到報(bào)文被 server 丟棄,依靠重傳定時(shí)器重傳。

SYN Flood 攻擊就是通過(guò)消耗光服務(wù)器上的半連接隊(duì)列來(lái)使得正常的用戶連接請(qǐng)求無(wú)法被響應(yīng)。不過(guò)在現(xiàn)在的 Linux 內(nèi)核里只要打開(kāi) tcp_syncookies,半連接隊(duì)列滿了仍然也還可以保證正常握手的進(jìn)行。

sk_acceptq_is_full 來(lái)判斷全連接隊(duì)列是否滿了, inet_csk_reqsk_queue_young 判斷的是有沒(méi)有 young_ack(未處理完的半連接請(qǐng)求)。

若全連接隊(duì)列滿的情況下,且同時(shí)有 young_ack ,那么內(nèi)核同樣直接丟掉該 SYN 握手包。

young_ack 是半連接隊(duì)列里保持著的一個(gè)計(jì)數(shù)器。記錄的是剛有SYN到達(dá),沒(méi)有被SYN_ACK重傳定時(shí)器重傳過(guò) SYN_ACK,同時(shí)也沒(méi)有完成過(guò)三次握手的 sock 數(shù)量。

從上面可以看到,若隊(duì)列已滿,server 端直接丟棄報(bào)文,并不通知客戶端。這時(shí)候客戶端只能通過(guò)發(fā)送 SYN 包時(shí)開(kāi)啟的重傳定時(shí)器超時(shí)進(jìn)行重傳。

server 構(gòu)造 synack 報(bào)文進(jìn)行回應(yīng)。

若啟用 syncookies,則是根據(jù)需要來(lái)判斷三次握手的,因此無(wú)需保存連接請(qǐng)求,直接將其釋放。

若未開(kāi)啟 syncookies,則需將連接請(qǐng)求塊保存到其父?jìng)鬏斂刂茐K中的半連接散列表中,并設(shè)置啟動(dòng)連接定時(shí)器。計(jì)時(shí)器的作用是如果某個(gè)時(shí)間之內(nèi)還收不到客戶端的第三次握手的話,服務(wù)器會(huì)重傳 synack 包。

request_sock 內(nèi)核對(duì)象加入到半連接表中,如下圖

客戶端響應(yīng) SYNACK

client 收到 synack 包后,最終會(huì)走到 tcp_rcv_state_process中,此時(shí)socket的狀態(tài)為 TCP_SYN_SENT

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
...
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard;
case TCP_LISTEN:
...
case TCP_SYN_SENT:
//處理 synack 包,返回值大于0表示需給對(duì)方發(fā)送RST段,該TCP段的釋放由tcp_rcv_state_process調(diào)用者處理
queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
if (queued >= 0)
return queued;
/* Do step6 onward by hand. */
//在處理完接收的段后,還需要處理緊急數(shù)據(jù),然后釋放該段,最后檢測(cè)是否有數(shù)據(jù)需要發(fā)送。
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk, tp);
return 0;
}
...
return 0;
}

關(guān)于 synack 包的處理邏輯為 tcp_rcv_synsent_state_process。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
...
if (th->ack) {
...
//若收到ack+rst段,則調(diào)用tcp_reset設(shè)置ECONNREFUSED錯(cuò)誤碼,同時(shí)通知等待該套接口的進(jìn)程,然后關(guān)閉套接口
if (th->rst) {
tcp_reset(sk);
goto discard;
}
//在SYS_SENT狀態(tài)下接收的段必須存在SYN標(biāo)志,否則說(shuō)明接收到的段無(wú)效,然后跳到discard_and_undo丟棄該段
if (!th->syn)
goto discard_and_undo;
TCP_ECN_rcv_synack(tp, th);
// 初始化與窗口有關(guān)的成員變量
tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
tcp_ack(sk, skb, FLAG_SLOWPATH);// 刪除發(fā)送隊(duì)列和重傳定時(shí)器
/* Ok.. it's good. Set up sequence numbers and
? move to established.
*/
tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
...
//設(shè)置已完成連接狀態(tài)
tcp_set_state(sk, TCP_ESTABLISHED);
...
//初始化擁塞控制
tcp_init_congestion_control(sk);
...
//若啟用了連接?;睿瑒t啟用連接?;疃〞r(shí)器
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
if (!tp->rx_opt.snd_wscale)
__tcp_fast_path_on(tp, tp->snd_wnd);
else
tp->pred_flags = 0;
//若不處于SOCK_DEAD,則喚醒等待該套接口的進(jìn)程,同時(shí)向套接口的異步等待隊(duì)列上的進(jìn)程發(fā)送信號(hào),通知他們?cè)撎捉涌诳梢暂敵鰯?shù)據(jù)了
if (!sock_flag(sk, SOCK_DEAD)) {
/* 指向sock_def_wakeup,會(huì)喚醒調(diào)用connect()的進(jìn)程,完成連接的建立 /
sk->sk_state_change(sk);
/ 如果使用了異步通知,則發(fā)送SIGIO通知進(jìn)程可寫(xiě) */
sk_wake_async(sk, 0, POLL_OUT);
}
...
discard:
__kfree_skb(skb);
return 0;
} else {
// 發(fā)送ack段,同時(shí)更新窗口。
tcp_send_ack(sk);
}
return -1;
}
...
}

處理 TCP 段中存在 ACK 標(biāo)志的情況。

啟動(dòng)連接?;疃〞r(shí)器。

喚醒調(diào)用connect阻塞的進(jìn)程,然后發(fā)送 ACK 報(bào)文。

void tcp_send_ack(struct sock sk)
{
/ If we have been reset, we may not send again. */
//發(fā)送ack時(shí),tcp必須不在TCP_CLOSE狀態(tài)
if (sk->sk_state != TCP_CLOSE) {
...
//為ack分配一個(gè)SKB,若分配失敗則在啟動(dòng)延時(shí)確認(rèn)定時(shí)器后返回
buff = alloc_skb(MAX_TCP_HEADER, GFP_ATOMIC);
...
/* Send it off, this clears delayed acks for us. */
//設(shè)置序號(hào)和發(fā)送時(shí)間,調(diào)用tcp_transmit_skb將ack段發(fā)送出去
TCP_SKB_CB(buff)->seq = TCP_SKB_CB(buff)->end_seq = tcp_acceptable_seq(sk, tp);
TCP_SKB_CB(buff)->when = tcp_time_stamp;
tcp_transmit_skb(sk, buff, 0, GFP_ATOMIC);
}
}

client 收到對(duì)端發(fā)送的 synack 包后,清除了 connect 時(shí)設(shè)置的重傳定時(shí)器,把 socket 狀態(tài)設(shè)置為 TCP_ESTABLISHED,同時(shí)喚醒調(diào)用 connect 而阻塞的進(jìn)程,開(kāi)啟?;疃〞r(shí)器并發(fā)送第三次握手的 ack 確認(rèn)包。

server 端處理 ACK 包

關(guān)于 server 處理 ack 報(bào)文的過(guò)程如下:

|-> tcp_v4_do_rcv

. |-> tcp_v4_hnd_req

. . |-> inet_csk_search_req // 從半連接中取出連接請(qǐng)求塊request_sock

. . |-> tcp_check_req

. . . |-> syn_recv_sock => tcp_v4_syn_recv_sock

. . . . |-> tcp_create_openreq_child

. . . . . |-> inet_csk_clone // 生成一個(gè)sock結(jié)構(gòu),設(shè)置 TCP_SYN_RECV 狀態(tài)

. . . |->

inet_csk_reqsk_queue_unlink // 把連接請(qǐng)求塊request_sock從半連接隊(duì)列中刪除

. . . |-> inet_csk_reqsk_queue_add //把request_sock和生成的sock進(jìn)行關(guān)聯(lián),并掛到icsk_accept_queue 全連接隊(duì)列中

. |-> tcp_child_process

. . |-> tcp_rcv_state_process //設(shè)置狀態(tài) TCP_ESTABLISHED

. . |-> sk_data_ready => sock_def_readable //喚醒阻塞在accept上的進(jìn)程

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
//服務(wù)器收到第一步握手 SYN 或者第三步 ACK 都會(huì)走到這里
if (sk->sk_state == TCP_LISTEN) {
//從半連接表syn_table中取出連接請(qǐng)求塊request_sock,同時(shí)生成一個(gè)新的sock結(jié)構(gòu)
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
// 新生成的sock和監(jiān)聽(tīng)的不一樣
if (nsk != sk) {
//設(shè)置狀態(tài) TCP_ESTABLISHED并喚醒阻塞在accept上的進(jìn)程
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
}
...
}
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
...
// 從半連接hash表中獲取連接請(qǐng)求塊request_sock
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
return tcp_check_req(sk, skb, req, prev);
...
}
inet_csk_search_req

在半連接隊(duì)列里進(jìn)行查找并返回一個(gè)半連接 request_sock 對(duì)象。然后進(jìn)入到 tcp_check_req 中

struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb,
struct request_sock *req,
struct request_sock **prev)
{
...
//創(chuàng)建子 socket ,調(diào)用 tcp_v4_syn_recv_sock
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb,
req, NULL); //tcp_v4_syn_recv_sock
//清理半連接隊(duì)列
inet_csk_reqsk_queue_unlink(sk, req, prev);
inet_csk_reqsk_queue_removed(sk, req);
//把request_sock和生成的sock進(jìn)行關(guān)聯(lián),并把request_sock添加到全連接隊(duì)列
inet_csk_reqsk_queue_add(sk, req, child);
return child;
...
}

創(chuàng)建子 socket 并初始化,然后把新生成newsk 加入到 ehash hash 表中, 以后當(dāng)有報(bào)文到來(lái)時(shí),從該 hash 表中找對(duì)應(yīng)的sock結(jié)構(gòu),也就找到了對(duì)應(yīng)的進(jìn)程。

struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
...
//判斷接收隊(duì)列是不是滿了,若滿,丟棄
if (sk_acceptq_is_full(sk))
goto exit_overflow;
...
//創(chuàng)建 sock && 初始化
newsk = tcp_create_openreq_child(sk, req, skb);
...
// 把 newsk 加入到 已完成鏈接的ehash hash表中
__inet_hash(&tcp_hashinfo, newsk, 0);
__inet_inherit_port(&tcp_hashinfo, sk, newsk);
...
}

設(shè)置 TCP_ESTABLISHED 狀態(tài)

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len)
{
...
if (th->ack) {
...
switch(sk->sk_state) {
case TCP_SYN_RECV:
//設(shè)置狀態(tài) TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
...
}

第三次握手的主要功能就是從半連接 hash表中摘除連接請(qǐng)求塊 request_sock,然后生成一個(gè) sock 與之進(jìn)行關(guān)聯(lián),然后再把 request_sock 添加到全連接隊(duì)列。

server 從 accept 中被喚醒

server 調(diào)用 accept 時(shí)由于 icsk_accept_queue 隊(duì)列沒(méi)有為空,進(jìn)程被阻塞等待。

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
...
if (sk->sk_state != TCP_LISTEN) /* socket必須處于監(jiān)聽(tīng)狀態(tài) *
goto out_err;
/* Find already established connection /
// 發(fā)現(xiàn)沒(méi)有ESTABLISHED狀態(tài)的連接請(qǐng)求塊
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
/ 等待超時(shí)時(shí)間,如果是非阻塞則為0 */
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep /
error = -EAGAIN;
if (!timeo) / 如果是非阻塞的,則直接退出 /
goto out_err;
/ 阻塞等待,直到有全連接。如果用戶有設(shè)置等待超時(shí)時(shí)間,超時(shí)后會(huì)退出 /
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
/ 獲取新連接的sock,釋放連接控制塊 */
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
BUG_TRAP(newsk->sk_state != TCP_SYN_RECV);
out:
release_sock(sk);
return newsk;
out_err:
newsk = NULL;
*err = error;
goto out;
}

上三次握手完成后,server 被喚醒,此時(shí)全連接隊(duì)列 icsk_accept_queue 不空,server 調(diào)用 reqsk_queue_get_child() 從全連接隊(duì)列中獲取一個(gè)新的sock。

static inline struct sock *reqsk_queue_get_child(struct request_sock_queue *queue,
struct sock parent)
{
/ 從全連接隊(duì)列中,取出第一個(gè)ESTABLISHED狀態(tài)的連接請(qǐng)求塊 */
struct request_sock *req = reqsk_queue_remove(queue);
struct sock child = req->sk; / 一個(gè)已建立的連接 */
BUG_TRAP(child != NULL);
/* 當(dāng)前backlog隊(duì)列的全連接數(shù)減一 */
sk_acceptq_removed(parent);
__reqsk_free(req);
return child;
}

accept 的作用就是從已經(jīng)建立好的全連接隊(duì)列中取出一個(gè)返回已完成連接的 sock 返回給用戶進(jìn)程。

責(zé)任編輯:華軒 來(lái)源: 今日頭條
相關(guān)推薦

2021-03-08 18:08:08

TCP Connect 協(xié)議

2024-10-09 20:54:16

2023-09-07 16:46:54

TCP數(shù)據(jù)傳遞

2023-10-24 15:22:09

TCPUDP

2022-10-10 07:34:36

TCP三次握手區(qū)塊鏈

2020-12-08 06:34:16

TCP握手SYN 報(bào)文

2015-10-13 09:42:52

TCP網(wǎng)絡(luò)協(xié)議

2019-06-12 11:26:37

TCP三次握手四次揮手

2024-01-12 08:23:11

TCPACK服務(wù)器

2022-07-25 07:07:35

TCP客戶端服務(wù)器

2019-12-12 10:36:43

TCPSYNIP

2020-08-27 07:41:28

TCP協(xié)議數(shù)據(jù)

2022-07-07 09:00:17

TCP 連接HTTP 協(xié)議

2023-09-02 21:57:52

網(wǎng)絡(luò)TCP協(xié)議

2018-07-05 14:25:01

TCP握手原理

2018-10-15 08:06:33

TCP握手原理

2021-07-03 17:47:25

TCP控制協(xié)議

2021-01-29 06:11:08

TCP通信三次握手

2019-02-01 09:38:16

2021-05-18 12:27:40

TCP控制協(xié)議
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)