Linux 网络系统学习: IPv6 的初始化
Linux 网络系统学习IPv6 的初始化作者: 小马哥 rstevens (rstevens2008@hotmail.com)欢迎转载,未经允许,请勿用于商业目的 1. 网络子系统1.1. 网络子系统概述 Linux 内核中,与网络相关的代码是一个相对独立的子系统,称为网络子系统。网络子系统是一个层次化的结构,可
Linux 网络系统学习
IPv6 的初始化
作者: 小马哥 rstevens (rstevens2008@hotmail.com)
欢迎转载,未经允许,请勿用于商业目的
1. 网络子系统
1.1. 网络子系统概述
Linux 内核中,与网络相关的代码是一个相对独立的子系统,称为网络子系统。
网络子系统是一个层次化的结构,可分为以下几个层次:
1、Socket 层
Linux 在发展过程中,采用 BSD socket APIs 作为自己的网络相关的 API 接口。同时,Linux 的目标又要能支持各种不同的协议族,而且这些协议族都可以使用 BSD socket APIs 作为应用层的编程接口。因此,在 socket APIs 与协议族层之间抽象出一个 socket 层,用于将 user space 的 socket API 调用,转给具体的协议族做处理。
2、协议族层(INET协议族、INET6协议族等)
Linux 网络子系统功能上相当完备,它不仅支持 INET 协议族(也就是通常所说的 TCP/IP stack),而且还支持其它很多种协议族,如 DECnet, ROSE, NETBEUI 等。INET6 就是一种新增加的协议族。
对于 INET、INET6 协议族来说, 又进一步划分为传输层和网络层。
3、设备驱动层
设备驱动层则主要将协议族层与物理的网络设备隔离开。它不在本文的讨论范围之内。
下图是 Linux 网络系统层次结构图。
1.2. 网络子系统的初始化
· Socket 层的初始化:
Init()->do_basic_setup()->sock_init()
Sock_init():对 sock 和 skbuff 结构进行 SLAB 内存的初始化工作
· 各种网络协议族的初始化:
Do_initcalls():
对于编译到内核中的功能模块(而不是以模块的形式动态加载),它的初始化函数会在这个地方被调用到。
内核映象中专门有一个初始化段,所有编译到内核中的功能模块的初始化函数都会加入到这个段中;而 do_initcalls() 就是依次执行初始化段中的这些函数。
INET 协议族通常是被编译进内核的;它的模块初始化函数是 net/ipv4/af_inet.c 中的 inet_init()
而 INET6 是作为一个模块编译的。它的模块初始化函数是 net/ipv6/af_inet6.c 中的 inet6_init()
2. 协议族
Linux 网络子系统可以支持不同的协议族,Linux 所支持的协议族定义在 include/linux/socket.h
2.1. 协议族数据结构
协议族数据结构是 struct net_proto_family。
int family;
int ( * create)( struct socket * sock, int protocol);
short authentication;
short encryption;
short encrypt_net;
struct module * owner;
};
这个结构中,最重要的是create 函数,一个新的协议族,必须提供此函数的实现。这是因为:
不同的网络协议族,从 user space 的使用方法来说,都是一样的,都是先调用 socket() 来创建一个 socket fd,然后通过这个 fd 发送/接收数据。
在 user space 通过 socket() 系统调用进入内核后,根据第一个参数协议族类型,来调用相应协议族的 create() 函数。对 INET6 来说,这个函数是 inet6_create()。
因此,要实现一个新的协议族,首先需要提供一个create() 的实现。关于 create() 里面具体做了什么,后面再叙述。
Linux 系统通过这种方式,可以很方便的支持新的网络协议族,而不用修改已有的代码。这很好的符合了 “开-闭原则”,对扩展开放,对修改封闭。
2.2. 协议族注册
Linux 维护一个struct net_proto_family 的数组net_families[]
如果要支持一个新的网络协议族,那么需要定义自己的struct net_proto_family,并且通过调用 sock_register 将它注册到 net_families[] 中。
3. socket 层的主要数据结构
socket 层又叫 “socket access protocol layer”。它处于 BSD socket APIs 与底层具体的协议族之间。这是一个抽象层,它起着承上启下的作用。在这一层的数据结构也有着这种特点。
3.1. Struct socket
在 user space,通过 socket() 创建的 socket fd,在内核中对应的就是一个 struct socket。
socket_state state;
unsigned long flags;
struct proto_ops * ops;
struct fasync_struct * fasync_list;
struct file * file;
struct sock * sk;
wait_queue_head_t wait;
short type;
};
它定义于 include/linux/net.h 中。
3.2. Struct proto_ops
Struct socket 的 ops 域指向一个 struct proto_ops 结构,struct proto_ops定义于 include/linux/net.h 中,它是 socket 层提供给上层的接口,这个结构中,都是 BSD socket API 的具体实现的函数指针。
int family;
struct module * owner;
int ( * release) ( struct socket * sock);
int ( * bind) ( struct socket * sock,
struct sockaddr * myaddr,
int sockaddr_len);
int ( * connect) ( struct socket * sock,
struct sockaddr * vaddr,
int sockaddr_len, int flags);
int ( * socketpair)( struct socket * sock1,
struct socket * sock2);
int ( * accept) ( struct socket * sock,
struct socket * newsock, int flags);
int ( * getname) ( struct socket * sock,
struct sockaddr * addr,
int * sockaddr_len, int peer);
unsigned int ( * poll) ( struct file * file, struct socket * sock,
struct poll_table_struct * wait);
int ( * ioctl) ( struct socket * sock, unsigned int cmd,
unsigned long arg);
int ( * listen) ( struct socket * sock, int len);
int ( * shutdown) ( struct socket * sock, int flags);
int ( * setsockopt)( struct socket * sock, int level,
int optname, char __user * optval, int optlen);
int ( * getsockopt)( struct socket * sock, int level,
int optname, char __user * optval, int __user * optlen);
int ( * sendmsg) ( struct kiocb * iocb, struct socket * sock,
struct msghdr * m, size_t total_len);
int ( * recvmsg) ( struct kiocb * iocb, struct socket * sock,
struct msghdr * m, size_t total_len,
int flags);
int ( * mmap) ( struct file * file, struct socket * sock,
struct vm_area_struct * vma);
ssize_t ( * sendpage) ( struct socket * sock, struct page * page,
int offset, size_t size, int flags);
};
一个 socket API 通过系统调用进入内核后,首先由 socket 层处理。Socket 层找到对应的 struct socket,通过它找到 struct proto_ops,然后由它所指向的函数进行进一步处理。
以 sendmsg() 这个函数为例,从 user space 通过系统调用进入 kernel 后,由 sys_sendmsg()、sock_sendmsg() 依次处理,然后交给 struct proto_ops 的 sendmsg() 处理。
4. 传输层、网络层的主要数据结构
Socket 层之下,是具体的协议族。
对INET 和 INET6 来说,又分为传输层和网络层。
这两层重要的数据结构有 struct sock 和 struct proto。
4.1. Struct sock
struct sock 定义于 include/net/sock.h 中,用于 INET 和 INET6 协议族。
应用层的 socket fd,在 socket 层对应的是 struct socket。 struct socket 很简单,并不做什么具体的事情,它通过 sk 域与一个 struct sock 关联。因此,对应用层的一个 socket fd 来说,在内核中对应的是一个 struct socket 加上一个 struct sock 结构,struct socket 负责 socket 层的处理,struct sock 负责传输层、网络层的处理。
在 2.4 内核中,这个结构非常杂乱,到 2.6 内核,对它做了简化,但仍然有很多成员。我们在这里不具体了解它的作用,只要知道它所处的层次即可。
4.2. Struct proto
struct sock 通过 sk_prot 域指向 struct proto 结构。
void ( * close)( struct sock * sk,
long timeout);
int ( * connect)( struct sock * sk,
struct sockaddr * uaddr,
int addr_len);
int ( * disconnect)( struct sock * sk, int flags);
struct sock * ( * accept) ( struct sock * sk, int flags, int * err);
int ( * ioctl)( struct sock * sk, int cmd,
unsigned long arg);
int ( * init)( struct sock * sk);
int ( * destroy)( struct sock * sk);
void ( * shutdown)( struct sock * sk, int how);
int ( * setsockopt)( struct sock * sk, int level,
int optname, char __user * optval,
int optlen);
int ( * getsockopt)( struct sock * sk, int level,
int optname, char __user * optval,
int __user * option);
int ( * sendmsg)( struct kiocb * iocb, struct sock * sk,
struct msghdr * msg, size_t len);
int ( * recvmsg)( struct kiocb * iocb, struct sock * sk,
struct msghdr * msg,
size_t len, int noblock, int flags,
int * addr_len);
int ( * sendpage)( struct sock * sk, struct page * page,
int offset, size_t size, int flags);
int ( * bind)( struct sock * sk,
struct sockaddr * uaddr, int addr_len);
int ( * backlog_rcv) ( struct sock * sk,
struct sk_buff * skb);
…
}
struct proto是传输层提供给 socket 层的接口。可以看到,它的成员也都是 BSD socket API 相关的函数指针。
应用层的 socket API 调用进入内核空间后,首先由socket 层的 struct proto_ops 结构做处理,此后,对于 INET 和 INET6 协议族来说,进一步由 struct proto 的相应函数做处理。
还是以 sendmsg() 为例,数据在 socket 层由 struct proto_ops 的 sendmsg() 处理完毕之后,会由 struct proto 的 sendmsg() 进行传输层的处理。
5. Socket 层与传输层的关联
INET 和 INET6 这两种协议族,可以支持多种传输层协议,包括 TCP、UDP、RAW,在 2.6 内核中,又增加了一种新的传输层协议:SCTP。
从内核角度看,要实现 INET6 协议族的某种传输层协议,则必须既提供 socket 层的 struct proto_ops 的实现,也提供 struct proto 的实现。除此之外,还需要提供一种手段,把这两个结构关联起来,也就是把 socket 层和传输层关联起来。
Linux 提供了一个 struct inet_protosw 的结构,用于 socket 层与传输层的关联。
5.1. struct inet_protosw
struct list_head list;
/* These two fields form the lookup key. */
unsigned short type; /* This is the 2nd argument to socket(2). */
int protocol; /* This is the L4 protocol number. */
struct proto * prot;
struct proto_ops * ops;
int capability; /* Which (if any) capability do
* we need to use this socket
* interface?
*/
char no_check; /* checksum on rcv/xmit/none? */
unsigned char flags; /* See INET_PROTOSW_* below. */
};
这个结构定义于 include/net/protocol.h 中,从它的命名上可以看到它属于 INET 和 INET6 协议族,但是没有查到资料为什么叫做 protosw。
这个结构中,ops 指向 socket 层的 struct proto_ops,prot 指向传输层的 struct proto。
因此,对 INET6 这种要支持多种传输层协议的协议族,从内核的角度来说,只需要为每一种传输层协议定义相应的 struct proto_ops、struct proto,然后再定义 struct inet_protosw,并将三者关联起来即可:
以 INET6 所支持的 TCP 为例:
.family = PF_INET6,
.owner = THIS_MODULE,
.release = inet6_release,
.bind = inet6_bind,
.connect = inet_dgram_connect, /* ok */
.socketpair = sock_no_socketpair, /* a do nothing */
.accept = sock_no_accept, /* a do nothing */
.getname = inet6_getname,
.poll = datagram_poll, /* ok */
.ioctl = inet6_ioctl, /* must change */
.listen = sock_no_listen, /* ok */
.shutdown = inet_shutdown, /* ok */
.setsockopt = sock_common_setsockopt, /* ok */
.getsockopt = sock_common_getsockopt, /* ok */
.sendmsg = inet_sendmsg, /* ok */
.recvmsg = sock_common_recvmsg, /* ok */
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
struct proto tcpv6_prot = {
.name = " TCPv6 " ,
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v6_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v6_init_sock,
.destroy = tcp_v6_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.sendmsg = tcp_sendmsg,
.recvmsg = tcp_recvmsg,
.backlog_rcv = tcp_v6_do_rcv,
.hash = tcp_v6_hash,
.unhash = tcp_unhash,
.get_port = tcp_v6_get_port,
.enter_memory_pressure = tcp_enter_memory_pressure,
.sockets_allocated = & tcp_sockets_allocated,
.memory_allocated = & tcp_memory_allocated,
.memory_pressure = & tcp_memory_pressure,
.orphan_count = & tcp_orphan_count,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem = sysctl_tcp_wmem,
.sysctl_rmem = sysctl_tcp_rmem,
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof ( struct tcp6_sock),
.twsk_obj_size = sizeof ( struct tcp6_timewait_sock),
.rsk_prot = & tcp6_request_sock_ops,
};
static struct inet_protosw tcpv6_protosw = {
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = & tcpv6_prot,
.ops = & inet6_stream_ops,
.capability = - 1 ,
.no_check = 0 ,
.flags = INET_PROTOSW_PERMANENT,
};
5.2. Socket 层和传输层关联表
Linux 为 INET6 协议族定义一个 struct inet_protosw 的链表数组inetsw6[] 。
要支持某种传输层协议,首先实现相应的 struct proto_ops、struct proto,然后实现struct inet_protosw,将两者关联,最后,通过 inet6_register_protosw() ,将此 struct inet_protosw 注册到 inet6_sw[] 中。
注册的时候,根据 struct inet_protosw 的 type ,将它放到 inet6_sw[type] 所在的链表中,相同的 type, 不同的 protocol,会在同一个链表上。
6. 建立数据结构之间的关系
从 user space 角度看,要使用 INET6 协议族的某种传输层协议,首先需要通过 socket() 调用创建一个相应的 socket fd,然后再通过这个 socket fd,接收和发送数据。
socket() 的原型是:
int socket(int domain, int type, int protocol);
domain 指定了协议族.
type 表明在网络中通信所遵循的模式。主要的值有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等。
SOCK_STREAM 是面向流的通信方式,而 SOCK_DGRAM 是面向报文的通信方式。不同的通信方式,在接收数据和发送数据时,具有不同的处理方式。
Protocol 则指明具体的传输层协议。不同的传输层协议,可能具有相同的 type,例如 TCP 和 SCTP 都是 SOCK_STREAM 类型。
以 socket(PF_INET6, SOCK_STREAM, 0) 为例,在进入内核空间后,
根据 domain,找到inet6_family_ops。
创建 struct socket
调用inet6_family_opsde create(),也就是inet6_create()
inet6_create() 根据 type 和 protocol 在 inet6_sw[] 中找到对应的 struct inet_protosw,也就是tcpv6_protosw
创建 struct sock,将 struct socket 和 struct sock 关联起来
将 struct socket 和 tcpv6_protosw 的 ops ,也就是inet6_stream_ops 关联起来
将 struct sock 和 tcpv6_protosw 的 prot,也就是tcpv6_prot 关联起来。
这样,socket 层和传输层的数据结构之间的关系建立起来了,此后,应用层通过 socket fd 接收或者发送数据的时候,就会先经过 socket 层 inet6_stream_ops 处理,然后经过传输层的 tcpv6_prot 处理。
下图描述了 socket 层与传输层之间数据结构关系的建立过程,以及发送数据时,是如何通过这些数据结构进行交互的。
7. 网络层协议类型
注册协议族,是从面向 user space 的角度来考虑的。当注册一个新的协议族后,user space 就可以创建此协议族的 socket,并通过此 socket 来接收和发送数据了。
而从面向设备驱动层的角度来考虑,通常需要为新的协议族注册一个网络层协议类型(或者叫“包类型”)。我们知道当网络设备接收到数据包之后,经过 L2 处理之后,需要根据其网络层协议类型,进行进一步处理。对于 INET 协议族来说,就是 IP 协议,而对于 INET6 协议族来说,就是 IPv6 协议。
因此,要实现一个新的协议族,还必须提供对网络层协议包进行处理的方法。
这个结构是 struct packet_type。
__be16 type; /* This is really htons(ether_type). */
struct net_device * dev; /* NULL is wildcarded here */
int ( * func) ( struct sk_buff * ,
struct net_device * ,
struct packet_type * ,
struct net_device * );
void * af_packet_priv;
struct list_head list;
};
这个结构中中的 func ,就是在网络层对接收到的数据包进行处理的方法。
Linux 系统中维护一个 struct packet_type 的数组ptype_base[],一个新的 struct packet_type 变量通过调用 dev_add_packet() ,注册到这个数组中。
在接收数据包的过程中,根据网络层协议类型,到 ptype_base[] 数组中寻找对应的 struct packet_type 变量,然后由它的 func() 做进一步处理。
8. 传输层协议类型
同样,网络层处理完毕之后,需要交给传输层处理。因此,一个新的传输层协议需要向网络层注册处理函数。
对 INET6 协议来说,这个结构是struct inet6_protocol
{
int ( * handler)( struct sk_buff ** skb, unsigned int * nhoffp);
void ( * err_handler)( struct sk_buff * skb,
struct inet6_skb_parm * opt,
int type, int code, int offset,
__u32 info);
unsigned int flags; /* INET6_PROTO_xxx */
};
这个结构中的 handler ,就是传输层提供给网络层的处理函数。
Linux 系统中维护一个struct inet6_protocol 的数组inet6_protos[],一个新的struct inet6_protocol 变量通过调用inet6_add_protocol() ,注册到这个数组中。在接收数据包的过程中,根据传输层协议类型,到 inet6_protos[] 数组中寻找对应的 struct inet6_protocol 变量,然后由它的 handler() 做进一步处理。
9. 数据包从设备驱动层向上传递处理的过程
10. INET6 的初始化
经过前面的分析,现在可以理解 INET6 协议族在初始化的时候要做哪些事情:
1、注册 INET6 协议族,提供协议族的创建函数。
2、为所支持的传输层协议分别提供 struct proto_ops、struct proto和struct inet_protosw 结构,并注册到关联表中。
3、向设备驱动层注册 IPv6 数据包的处理函数
4、向网络层注册 TCP、UDP、RAW 等传输层的处理函数。
5、其它初始化工作
10.1. 注册 INET6 协议族
对于 INET6 的实现来说,第一步是要注册 INET6 协议族。
.family = PF_INET6,
.create = inet6_create,
.owner = THIS_MODULE,
};
sock_register( & inet6_family_ops);
inet6_create() 的实现:TBW
10.2. 为 TCP、UDP、RAW 等传输层协议提供关联表
1、初始化关联表
INIT_LIST_HEAD(r);
2、RAW 的关联
.family = PF_INET6,
.owner = THIS_MODULE,
.release = inet6_release,
.bind = inet6_bind,
.connect = inet_dgram_connect, /* ok */
.socketpair = sock_no_socketpair, /* a do nothing */
.accept = sock_no_accept, /* a do nothing */
.getname = inet6_getname,
.poll = datagram_poll, /* ok */
.ioctl = inet6_ioctl, /* must change */
.listen = sock_no_listen, /* ok */
.shutdown = inet_shutdown, /* ok */
.setsockopt = sock_common_setsockopt, /* ok */
.getsockopt = sock_common_getsockopt, /* ok */
.sendmsg = inet_sendmsg, /* ok */
.recvmsg = sock_common_recvmsg, /* ok */
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
struct proto rawv6_prot = {
.name = " RAWv6 " ,
.owner = THIS_MODULE,
.close = rawv6_close,
.connect = ip6_datagram_connect,
.disconnect = udp_disconnect,
.ioctl = rawv6_ioctl,
.init = rawv6_init_sk,
.destroy = inet6_destroy_sock,
.setsockopt = rawv6_setsockopt,
.getsockopt = rawv6_getsockopt,
.sendmsg = rawv6_sendmsg,
.recvmsg = rawv6_recvmsg,
.bind = rawv6_bind,
.backlog_rcv = rawv6_rcv_skb,
.hash = raw_v6_hash,
.unhash = raw_v6_unhash,
.obj_size = sizeof ( struct raw6_sock),
};
static struct inet_protosw rawv6_protosw = {
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = & rawv6_prot,
.ops = & inet6_sockraw_ops,
.capability = CAP_NET_RAW,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
};
inet6_register_protosw( & rawv6_protosw);
3、UDP 的关联
.family = PF_INET6,
.owner = THIS_MODULE,
.release = inet6_release,
.bind = inet6_bind,
.connect = inet_dgram_connect, /* ok */
.socketpair = sock_no_socketpair, /* a do nothing */
.accept = sock_no_accept, /* a do nothing */
.getname = inet6_getname,
.poll = udp_poll, /* ok */
.ioctl = inet6_ioctl, /* must change */
.listen = sock_no_listen, /* ok */
.shutdown = inet_shutdown, /* ok */
.setsockopt = sock_common_setsockopt, /* ok */
.getsockopt = sock_common_getsockopt, /* ok */
.sendmsg = inet_sendmsg, /* ok */
.recvmsg = sock_common_recvmsg, /* ok */
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
struct proto udpv6_prot = {
.name = " UDPv6 " ,
.owner = THIS_MODULE,
.close = udpv6_close,
.connect = ip6_datagram_connect,
.disconnect = udp_disconnect,
.ioctl = udp_ioctl,
.destroy = udpv6_destroy_sock,
.setsockopt = udpv6_setsockopt,
.getsockopt = udpv6_getsockopt,
.sendmsg = udpv6_sendmsg,
.recvmsg = udpv6_recvmsg,
.backlog_rcv = udpv6_queue_rcv_skb,
.hash = udp_v6_hash,
.unhash = udp_v6_unhash,
.get_port = udp_v6_get_port,
.obj_size = sizeof ( struct udp6_sock),
};
static struct inet_protosw udpv6_protosw = {
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = & udpv6_prot,
.ops = & inet6_dgram_ops,
.capability =- 1 ,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_PERMANENT,
};
inet6_register_protosw( & udpv6_protosw);
4、TCP 的关联
前面已经看过 TCP 相关的结构。
inet6_register_protosw(&tcpv6_protosw);
10.3. 注册 IPv6 包的接收函数
.type = __constant_htons(ETH_P_IPV6),
.func = ipv6_rcv,
};
ipv6_packet_init() ==> dev_add_pack(&ipv6_packet_type);
ipv6_rcv 的具体实现不在本文范围之内。
10.4. 注册传输层协议
.handler = udpv6_rcv,
.err_handler = udpv6_err,
.flags = INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL,
};
static struct inet6_protocol tcpv6_protocol = {
.handler = tcp_v6_rcv,
.err_handler = tcp_v6_err,
.flags = INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL,
};
inet6_add_protocol( & udpv6_protocol, IPPROTO_UDP);
inet6_add_protocol( & tcpv6_protocol, IPPROTO_TCP);
RAW 不需要注册。
Udpv6_rc、tcp_v6_rcv 的具体实现不在本文范围之内。
10.5. 其它
此外,还要做其它初始化工作,包括 ICMPv6、IGMPv6、Neighbor discovery、route 等等的初始化。本文不具体分析。
11. 附录:
| Structure | Register functions | description |
| net_proto_family | sock_register | 注册协议族 |
| packet_type | dev_add_pack | 向设备驱动层注册网络层协议处理函数 |
| inet6_protocol | inet6_add_protocol | 向网络层注册传输层协议处理函数 |
| proto_ops BSD APIs 与 socket 层的接口 |
|
|
| Proto Socket 层与传输层的接口 |
|
|
| inet_protosw 将 struct proto_ops 与 struct proto 对应起来 | inet6_register_protosw | 注册到系统的 struct inet_protosw数组 inetsw6 中 此数组用于创建 socket 之用。 |
| Proto Socket 层与传输层的接口 | proto_register | 将传输层协议处理函数注册到系统中的 struct proto 的链表 proto_list。 这个目的是为了在 proc 系统中显示各种协议的信息
|
更多推荐
所有评论(0)