La pila del protocolo del kernel de Linux conecta la llamada al sistema I para enviar el paquete SYN

Tabla de contenido

1 Descripción general de la función de conexión

2. Conectar la implementación del kernel

2.1 sys_connect

2.2 Conexión de socket de flujo TCP inet_stream_connect

2.3 El cliente inicia el establecimiento del enlace TCP tcp_v4_connect (core)

2.3.1 Enlace de puertos inet_hash_connect

2.3.2 Construcción y envío de paquetes SYN tcp_connect

2.4 Bloqueo de espera inet_wait_for_connect

2.5 Despertar después de recibir el paquete sock_def_wakeup


1 Descripción general de la función de conexión

La interfaz de conexión es la primera vez que el cliente inicia una solicitud de conexión al servidor. Tanto TCP como UDP pueden iniciar una solicitud de conexión al servidor a través del cambio de interfaz. La implementación de UDP es relativamente simple, es decir, se establece la dirección de destino predeterminada . Para un uso específico, consulte "Programación de sockets: conectar Función " , aquí se analiza principalmente la implementación del protocolo TCP para implementar la implementación del kernel de connect, el cliente envía un paquete SYN al servidor llamando a connect (iniciar el temporizador de retransmisión), completa la primera operación del protocolo de enlace de tres vías tcp, ya sea que el socket raíz esté configurado en modo de bloqueo , Connect realizará una operación de bloqueo y esperará mensajes de respuesta como SYN + ACK y RST del servidor.

2. Conectar la implementación del kernel

  1. Verifique la validez de la dirección del servidor remoto
  2. Realizar operaciones relacionadas con el enrutamiento basadas en la dirección remota
  3. Determine si el socket local ha realizado la operación de vinculación. N: La dirección IP es la dirección de búsqueda de ruta y el puerto se asigna automáticamente (se agrega al ehash global de tcp)
  4. Migre el estado del socket tcp de TCP_CLOSE a TCP_SYN_SENT
  5. Construya un mensaje SYN y envíelo al servidor, únase a la cola de envío y configure el temporizador de retransmisión
  6. ¿Según si el socket está configurado en modo de bloqueo? Y: Realice la operación de bloqueo y espere a que se complete o rechace la conexión (esperando SYN + ACK, RST desde el servidor)
sys_connect
	--inet_stream_connect //TCP
		--tcp_v4_connect
	--inet_dgram_connect //UDP
		--ip4_datagram_connect

2.1 sys_connect

  1. Encuentra socket struct socket * sock de acuerdo con fd
  2. Copie la dirección del espacio de usuario en el espacio del kernel
  3. Llame a la interfaz de conexión de protocolo de cuatro capas, tcp, udp se dividen aquí
asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr,
			    int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;
	err = move_addr_to_kernel(uservaddr, addrlen, (struct sockaddr *)&address);
	if (err < 0)
		goto out_put;

	err =
	    security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
	if (err)
		goto out_put;

	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);
out_put:
	fput_light(sock->file, fput_needed);
out:
	return err;
}

2.2 Conexión de socket de flujo TCP inet_stream_connect

  1. Para determinar la dirección del servidor debe especificar el tipo de protocolo
  2. Determine el estado actual de la conexión del enchufe como desconectado para evitar la reconexión, etc.
  3. Llame a tcp_v4_connect para ejecutar la interfaz de conexión tcp para iniciar la operación de establecimiento de enlace
  4. Según si el socket está configurado en modo de bloqueo, decida esperar a que se complete la conexión
/*
 *	Connect to a remote host. There is regrettably still a little
 *	TCP 'magic' in here.
 */
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
			int addr_len, int flags)
{
	struct sock *sk = sock->sk;
	int err;
	long timeo;

	lock_sock(sk);

	//connect()时,服务器端地址族必须要指定
	if (uaddr->sa_family == AF_UNSPEC) {
		err = sk->sk_prot->disconnect(sk, flags);
		sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
		goto out;
	}

	//根据socket的状态进行处理,这里只有到scoket的状态为SS_UNCONNECTED时才进行真正的处理,其余情况均返回错误
	switch (sock->state) {
	default:
		err = -EINVAL;
		goto out;
	case SS_CONNECTED:
		//connect()不能重复调用
		err = -EISCONN;
		goto out;
	case SS_CONNECTING:
		//正在连接
		err = -EALREADY;
		/* Fall out of switch with err, set for this state */
		break;
	case SS_UNCONNECTED:
		err = -EISCONN;
		//TCB的状态必须为TCP_CLOSE状态
		if (sk->sk_state != TCP_CLOSE)
			goto out;
		//调用传输层接口进行connect()操作,AF_INET中TCP为tcp_v4_connect()
		err = sk->sk_prot->connect(sk, uaddr, addr_len);
		if (err < 0)
			goto out;
		//执行成功,将socket的状态设置为SS_CONNECTING状态,表示正在连接
		sock->state = SS_CONNECTING;

		/* Just entered SS_CONNECTING state; the only
		 * difference is that return value in non-blocking
		 * case is EINPROGRESS, rather than EALREADY.
		 */
		err = -EINPROGRESS;
		break;
	}

	//根据是否是阻塞操作,确定connect()调用的超时时间,
	//阻塞模式下,connect()返回时三次握手已经完成了
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);

	//如果上面执行都没有问题,那么此时TCP的状态应该为TCPF_SYN_SENT或者TCPF_SYN_RECV(同时打开场景)
	if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
		/* 如果是非阻塞的,那么就直接返回错误码-EINPROGRESS。
         * socket为阻塞时,使用inet_wait_for_connect()来等待协议栈的处理:
         * 1. 使用SO_SNDTIMEO,睡眠时间超过timeo就返回0,之后返回错误码-EINPROGRESS。
         * 2. 收到信号,就返回剩余的等待时间。之后会返回错误码-ERESTARTSYS或-EINTR。
         * 3. 三次握手成功,被sock I/O事件处理函数唤醒,之后会返回0。
         */
		/* Error code is set above */
		if (!timeo || !inet_wait_for_connect(sk, timeo))
			goto out;
		//检查是否有错误发生
		err = sock_intr_errno(timeo);
		if (signal_pending(current))
			goto out;
	}

	//连接建立失败情形处理
	/* Connection was closed by RST, timeout, ICMP error
	 * or another process disconnected us.
	 */
	if (sk->sk_state == TCP_CLOSE)
		goto sock_error;

	/* sk->sk_err may be not zero now, if RECVERR was ordered by user
	 * and error was received after socket entered established state.
	 * Hence, it is handled normally after connect() return successfully.
	 */
	//连接建立成功,设置socket的状态为SS_CONNECTED
	sock->state = SS_CONNECTED;
	err = 0;
out:
	release_sock(sk);
	return err;

sock_error:
	//连接失败,socket的状态设置为SS_UNCONNECTED
	err = sock_error(sk) ? : -ECONNABORTED;
	sock->state = SS_UNCONNECTED;
	if (sk->sk_prot->disconnect(sk, flags))
		sock->state = SS_DISCONNECTING;
	goto out;
}

2.3 El cliente inicia el establecimiento del enlace TCP tcp_v4_connect (core)

La operación principal anterior es llamar a la devolución de llamada connect () proporcionada por el protocolo de la capa de transporte. Para TCP sobre IP, esta función es tcp_v4_init (). El propósito final de esta función es enviar una solicitud SYN.

  1. Verificar la legitimidad de la dirección remota
  2. Operaciones de enrutamiento relacionadas xxxx
  3. ¿Determinar si el calcetín local está vinculado? N: La dirección IP es la dirección de la interfaz de enrutamiento y el puerto se asigna automáticamente
  4. Migre el estado de tcp_sock a SYN_SENT
  5. Llame a tcp_connect para enviar el paquete SYN
/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
	struct rtable *rt;
	__be32 daddr, nexthop;
	int tmp;
	int err;

	//校验目标地址长度
	if (addr_len < sizeof(struct sockaddr_in))
		return -EINVAL;
	//校验地址族,该函数只处理IPv4
	if (usin->sin_family != AF_INET)
		return -EAFNOSUPPORT;

	//路由相关处理
	nexthop = daddr = usin->sin_addr.s_addr;
	if (inet->opt && inet->opt->srr) {
		if (!daddr)
			return -EINVAL;
		nexthop = inet->opt->faddr;
	}
	//路由缓存相关操作
	tmp = ip_route_connect(&rt, nexthop, inet->saddr,
			       RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
			       IPPROTO_TCP,
			       inet->sport, usin->sin_port, sk, 1);
	if (tmp < 0) {
		if (tmp == -ENETUNREACH)
			IP_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
		return tmp;
	}
	//TCP不支持多播和广播,返回错误
	if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
		ip_rt_put(rt);
		return -ENETUNREACH;
	}
	//源路由选项相关处理
	if (!inet->opt || !inet->opt->srr)
		daddr = rt->rt_dst;
	//如果没有指定过bind(),那么源地址还尚未确定,这时源IP选用路由查询结果中的源IP
	if (!inet->saddr)
		inet->saddr = rt->rt_src;
	inet->rcv_saddr = inet->saddr;

	//时间戳选项相关处理
	if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) {
		/* Reset inherited state */
		tp->rx_opt.ts_recent	   = 0;
		tp->rx_opt.ts_recent_stamp = 0;
		tp->write_seq		   = 0;
	}

	if (tcp_death_row.sysctl_tw_recycle &&
	    !tp->rx_opt.ts_recent_stamp && rt->rt_dst == daddr) {
		struct inet_peer *peer = rt_get_peer(rt);
		/*
		 * VJ's idea. We save last timestamp seen from
		 * the destination in peer table, when entering state
		 * TIME-WAIT * and initialize rx_opt.ts_recent from it,
		 * when trying new connection.
		 */
		if (peer != NULL &&
		    peer->tcp_ts_stamp + TCP_PAWS_MSL >= get_seconds()) {
			tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp;
			tp->rx_opt.ts_recent = peer->tcp_ts;
		}
	}

	//将目的地址和目的端口保存到TCB中
	inet->dport = usin->sin_port;
	inet->daddr = daddr;
	//设置TCP首部选项部分长度
	inet_csk(sk)->icsk_ext_hdr_len = 0;
	if (inet->opt)
		inet_csk(sk)->icsk_ext_hdr_len = inet->opt->optlen;
	//初始化对端MSS为536
	tp->rx_opt.mss_clamp = 536;

	//设置TCP状态为TCP_SYN_SENT
	/* Socket identity is still unknown (sport may be zero).
	 * However we set state to SYN-SENT and not releasing socket
	 * lock select source port, enter ourselves into the hash tables and
	 * complete initialization after this.
	 */
	tcp_set_state(sk, TCP_SYN_SENT);
	//如果需要,该函数动态绑定一个端口并将该传输控制块加入到ehash散列表中
	err = inet_hash_connect(&tcp_death_row, sk);
	if (err)
		goto failure;

	//上一步操作可能会导致源端口和源地址信息发生变化,所以这里可能需要重新路由
	err = ip_route_newports(&rt, IPPROTO_TCP,
				inet->sport, inet->dport, sk);
	if (err)
		goto failure;

	/* OK, now commit destination to socket.  */
	sk->sk_gso_type = SKB_GSO_TCPV4;
	sk_setup_caps(sk, &rt->u.dst);

	//计算一个客户端初始发送序号
	if (!tp->write_seq)
		tp->write_seq = secure_tcp_sequence_number(inet->saddr,
							   inet->daddr,
							   inet->sport,
							   usin->sin_port);

	inet->id = tp->write_seq ^ jiffies;

	//构造并发送SYN包
	err = tcp_connect(sk);
	rt = NULL;
	if (err)
		goto failure;

	return 0;

failure:
	/*
	 * This unhashes the socket and releases the local port,
	 * if necessary.
	 */
	tcp_set_state(sk, TCP_CLOSE);
	ip_rt_put(rt);
	sk->sk_route_caps = 0;
	inet->dport = 0;
	return err;
}

2.3.1 Enlace de puertos inet_hash_connect

Esta operación en realidad logra dos cosas:

  1. Si la operación de vinculación no se ha realizado antes de llamar a connect (), entonces se vincula una dirección local al bloque de control de transmisión. Este proceso y el inet_csk_get_port () se llaman durante bind (), pero utilizan diferentes funciones de detección de conflictos de puertos (por qué No he estudiado);
  2. Agregue el TCB vinculado a la tabla hash ehash de TCP.
/*
 * Bind a port for a connect operation and hash it.
 */
int inet_hash_connect(struct inet_timewait_death_row *death_row,
		      struct sock *sk)
{
	return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),
			__inet_check_established, __inet_hash_nolisten);
}

int __inet_hash_connect(struct inet_timewait_death_row *death_row,
		struct sock *sk, u32 port_offset,
		int (*check_established)(struct inet_timewait_death_row *,
			struct sock *, __u16, struct inet_timewait_sock **),
		void (*hash)(struct sock *sk))
{
	struct inet_hashinfo *hinfo = death_row->hashinfo;
	const unsigned short snum = inet_sk(sk)->num;
	struct inet_bind_hashbucket *head;
	struct inet_bind_bucket *tb;
	int ret;
	struct net *net = sk->sk_net;

	//端口的自动绑定部分非常类似于时调用的inet_csk_get_port()
	if (!snum) {
		int i, remaining, low, high, port;
		static u32 hint;
		u32 offset = hint + port_offset;
		struct hlist_node *node;
		struct inet_timewait_sock *tw = NULL;

		inet_get_local_port_range(&low, &high);
		remaining = (high - low) + 1;

		local_bh_disable();
		for (i = 1; i <= remaining; i++) {
			port = low + (i + offset) % remaining;
			head = &hinfo->bhash[inet_bhashfn(port, hinfo->bhash_size)];
			spin_lock(&head->lock);

			/* Does not bother with rcv_saddr checks,
			 * because the established check is already
			 * unique enough.
			 */
			inet_bind_bucket_for_each(tb, node, &head->chain) {
				if (tb->ib_net == net && tb->port == port) {
					BUG_TRAP(!hlist_empty(&tb->owners));
					if (tb->fastreuse >= 0)
						goto next_port;
					if (!check_established(death_row, sk,
								port, &tw))
						goto ok;
					goto next_port;
				}
			}

			tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
					net, head, port);
			if (!tb) {
				spin_unlock(&head->lock);
				break;
			}
			tb->fastreuse = -1;
			goto ok;

		next_port:
			spin_unlock(&head->lock);
		}
		local_bh_enable();

		return -EADDRNOTAVAIL;

ok:
		hint += i;

		/* Head lock still held and bh's disabled */
		//将TCB和端口绑定
		inet_bind_hash(sk, tb, port);
		if (sk_unhashed(sk)) {
			inet_sk(sk)->sport = htons(port);
			hash(sk);
		}
		spin_unlock(&head->lock);

		if (tw) {
			inet_twsk_deschedule(tw, death_row);
			inet_twsk_put(tw);
		}

		ret = 0;
		goto out;
	}

	head = &hinfo->bhash[inet_bhashfn(snum, hinfo->bhash_size)];
	tb  = inet_csk(sk)->icsk_bind_hash;
	spin_lock_bh(&head->lock);
	if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
		//将TCB加入到TCP的ehash哈希表中
		hash(sk);
		spin_unlock_bh(&head->lock);
		return 0;
	} else {
		spin_unlock(&head->lock);
		/* No definite answer... Walk to established hash table */
		ret = check_established(death_row, sk, snum, NULL);
out:
		local_bh_enable();
		return ret;
	}
}

2.3.2 Construcción y envío de paquetes SYN tcp_connect

tcp_connect es construir un paquete SYN y enviarlo , pero debido a que TCP proporciona un servicio confiable, después del envío también es necesario enviar el paquete SYN para unirse a la cola, y el temporizador de retransmisión SYN comienza, el único otro ingreso Después del mensaje ACK del extremo opuesto, el mensaje se puede eliminar de la cola de envío y se puede detener el temporizador de retransmisión SYN.

/*
 * Build a SYN and send it off.
 */
int tcp_connect(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *buff;

	//对TCB进行一定的初始化
	tcp_connect_init(sk);

	//分配一个SKB用于构造SYN段
	buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
	if (unlikely(buff == NULL))
		return -ENOBUFS;

	/* Reserve space for headers. */
	skb_reserve(buff, MAX_TCP_HEADER);

	tp->snd_nxt = tp->write_seq;
	tcp_init_nondata_skb(buff, tp->write_seq++, TCPCB_FLAG_SYN);
	TCP_ECN_send_syn(sk, buff);

	/* Send it off. */
	TCP_SKB_CB(buff)->when = tcp_time_stamp;
	tp->retrans_stamp = TCP_SKB_CB(buff)->when;
	skb_header_release(buff);
	//把SKB加入发送队列,并更新相关的统计值
	__tcp_add_write_queue_tail(sk, buff);
	sk->sk_wmem_queued += buff->truesize;
	sk_mem_charge(sk, buff->truesize);
	tp->packets_out += tcp_skb_pcount(buff);
	//发送该SYN请求报文
	tcp_transmit_skb(sk, buff, 1, GFP_KERNEL);

	/* We change tp->snd_nxt after the tcp_transmit_skb() call
	 * in order to make this packet get counted in tcpOutSegs.
	 */
	tp->snd_nxt = tp->write_seq;
	tp->pushed_seq = tp->write_seq;
	TCP_INC_STATS(TCP_MIB_ACTIVEOPENS);

	//启动SYN重传定时器,初始超时时间为1s
	/* Timer for repeating the SYN until an answer. */
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;
}

2.4 Bloqueo de espera inet_wait_for_connect

static long inet_wait_for_connect(struct sock *sk, long timeo, int writebias)
{
    DEFINE_WAIT(wait);
/* 把等待任务加入到socket的等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
    prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
    sk->sk_write_pending += writebias;
 
    /* Basic assumption: if someone sets sk->sk_err, he _must_
     * change state of the socket from TCP_SYN_*.
     * Connect() does not allow to get error notifications
     * without closing the socket.
     */
/* 完成三次握手后,状态就会变为TCP_ESTABLISHED,从而退出循环 */
    while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
        release_sock(sk);
 /* 进入睡眠,直到超时或收到信号,或者被I/O事件处理函数唤醒。
         * 1. 如果是收到信号退出的,timeo为剩余的jiffies。
         * 2. 如果使用了SO_SNDTIMEO选项,超时退出后,timeo为0。
         * 3. 如果没有使用SO_SNDTIMEO选项,timeo为无穷大,即MAX_SCHEDULE_TIMEOUT,
         *      那么返回值也是这个,而超时时间不定。为了无限阻塞,需要上面的while循环。
         */
 
        timeo = schedule_timeout(timeo);
        lock_sock(sk);
/* 如果进程有待处理的信号,或者睡眠超时了,退出循环,之后会返回错误码 */
        if (signal_pending(current) || !timeo)
            break;
        prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
    }
 /* 等待结束时,把等待进程从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
    finish_wait(sk_sleep(sk), &wait);
    sk->sk_write_pending -= writebias;
    return timeo;
}

2.5 Despertar después de recibir el paquete sock_def_wakeup

En el protocolo de enlace de tres vías, cuando el cliente recibe SYNACK y envía un ACK, la conexión se establece con éxito.
En este momento, el estado de la conexión cambia de TCP_SYN_SENT o TCP_SYN_RECV a TCP_ESTABLISHED o TCP_CLOSE. Cuando el estado de sock cambie, se
llamará a sock_def_wakeup () para manejar el evento de cambio de estado de la conexión, reactivar el proceso y connect () puede regresar exitosamente. La pila de llamadas de función de sock_def_wakeup () es la siguiente:

tcp_v4_rcv
	--tcp_v4_do_rcv
		--tcp_rcv_state_process
			--tcp_rcv_synsent_state_process//TCP_SYN_SENT
				--sock_def_wakeup
					--wake_up_interruptible_all
					--__wake_up
			--sock_def_wakeup //TCP_SYN_RECV
				--wake_up_interruptible_all
					--__wake_up
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  struct tcphdr *th, unsigned len)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	int queued = 0;
	int res;

	tp->rx_opt.saw_tstamp = 0;

	switch (sk->sk_state) {

	case TCP_SYN_SENT:
		queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
		if (queued >= 0)
			return queued;

		/* Do step6 onward by hand. */
		tcp_urg(sk, skb, th);
		__kfree_skb(skb);
		tcp_data_snd_check(sk);
		return 0;
	}

	...

	/* step 5: check the ACK field */
	if (th->ack) {
		int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0;

		switch (sk->sk_state) {
		case TCP_SYN_RECV:
			if (acceptable) {
				tp->copied_seq = tp->rcv_nxt;
				smp_mb();
				tcp_set_state(sk, TCP_ESTABLISHED);
				sk->sk_state_change(sk);

	...
}



static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 struct tcphdr *th, unsigned len)
{
	u8 *hash_location;
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	struct tcp_cookie_values *cvp = tp->cookie_values;
	int saved_clamp = tp->rx_opt.mss_clamp;

	tcp_parse_options(skb, &tp->rx_opt, &hash_location, 0);

	if (th->ack) {
		...

		if (!sock_flag(sk, SOCK_DEAD)) {
			sk->sk_state_change(sk);
			sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
		}
}
 
 
static inline void sk_wake_async(struct sock *sk, int how, int band)
{
    if (sock_flag(sk, SOCK_FASYNC))
        sock_wake_async(sk->sk_socket, how, band);
}
 
static void sock_def_wakeup(struct sock *sk)
{
    struct socket_wq *wq;
 
    rcu_read_lock();
    wq = rcu_dereference(sk->sk_wq);
    if (wq_has_sleeper(wq))
        wake_up_interruptible_all(&wq->wait);
    rcu_read_unlock();
}

Finalmente, llame a __wake_up_common (), ya que nr_exclusive es 0, todos los procesos en espera en este socket se despertarán

Supongo que te gusta

Origin blog.csdn.net/wangquan1992/article/details/108886504
Recomendado
Clasificación