热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

IP双栈环境下网络应用迁移

IPv4向IPv6迁移有多种途径,在选择具体的迁移方式时,当前环境中运行的应用是否支持IPv6是重要的考量因素之一,同时在编写新的应用时,需要考虑新编写的应用不仅可以适应当前主流的IPv4环境,

IPv4向IPv6迁移有多种途径,在选择具体的迁移方式时,当前环境中运行的应用是否支持IPv6是重要的考量因素之一,同时在编写新的应用时,需要考虑新编写的应用不仅可以适应当前主流的IPv4环境,还要能运行于IPv6环境c socket。当前主要的ICT基础设施都支持IPv6,但目前仍有众多的客户应用是基于IPv4环境开发的,如果在不修改应用以支持IPv6访问的情形下,所有的迁移方式都难言是彻底的。这篇短文拟对IPv4应用向IPv6应用迁移涉及到的相关内容进行简要介绍,双栈改造的方式有多种,本文以ipv4- mapped ipv6为例,期待对需要了解IPv6应用迁移的读者有所帮助。

2. IPv4-mapped IPv6地址

IPv4-mapped IPv6地址格式是::FFFF:IPv4-address,例如::FFFF:192.168.1.1,运行在双协议栈设备上的IPv6应用利用IPv4-mapped IPv6地址可以和基于IPv4的应用互通c socket

基于IPv6的服务端应用在接收到来自IPv4客户端的连接请求时,内部采用IPv4-mapped IPv6地址来表示此连接c socket

基于IPv6的客户端应用连接IPv4的服务端时c socket,采用IPv4-mapped IPv6的地址访问对端.

IPv4-mapped IPv6地址只用在系统内部,不能设置在网卡上作为IPv6的地址,也不会出现在网络数据包的源或目的IP地址字段中c socket

3. IP双栈环境下应用访问

IP双栈主机同时运行IPv4和IPv6协议c socket,配置IPv4和IPv6地址,可以同时和IPv4以及IPv6设备进行通讯:

IP双栈环境下网络应用迁移

图1 IPv4以及IPv6设备同时进行通讯

双栈环境下的不同的网络客户端应用发送数据包的协议路径:

IP双栈环境下网络应用迁移

展开全文

图2 不同的网络客户端应用发送数据包的协议路径

双栈环境下不同的网络服务端应用接受数据包的协议路径:

IP双栈环境下网络应用迁移

图3 不同的网络服务端应用接受数据包的协议路径

4. IPv4客户访问 IPv4服务

这是当前最常见的访问方式,IPv4的客户端在创建socket时采用的IPv4地址族AF_INET,IPv4服务端的socket也采用IPv4地址族AF_INET,客户端和服务端的网络通信都通过IPv4协议栈,客户端和服务端的通讯采用的源和目的IP是IPv4地址c socket

IP双栈环境下网络应用迁移

图4 IPv4客户访问IPv4服务

5. IPv4客户访问 IPv6服务

留存的客户端网络应用在早期创建socket时采用的地址族为IPv4 地址族AF_INET,但服务端为了适应IPV6环境,经过改造,在创建socket时采用的是IPv6地址族AF_INET6,并且在bind调用时不指定具体的IPv6地址或指定IPv4-mapped IPv6地址(实际应用基本不会采用,技术上可行),那么IPv4客户端可以通过连接对端可通达的IPv4地址来访问服务c socket

客户端主机上发送和接受的网络包通过IPv4协议栈,服务端主机从IPv4协议栈接受到数据包,根据IPv4数据包中的传输层协议发送给TCP或UDP模块处理,TCP/UDP模块查找TCP/UDP协议控制块,找到符合条件的的协议控制块(如目的端口相同以及V6only为0等)来进一步接收/处理数据包,同时在协议栈内部用IPv4-mapped IPv6地址来标记此连接c socket

IP双栈环境下网络应用迁移

图5 IPv4客户访问IPv6服务

下文中的所有python的示例都是基于Centos 7.5c socket,采用的python版本是2.7,示例仅为功能演示,错误处理都已忽略:

创建一个 IPv6 地址族的 socket, 绑定服务监听端口 7788 c socket,等待客户的连接请求,服务端接收 IPv4/v6 客户连接请求并输出接收到的数据,并打印相关的 TCP 连接信息

#python

>>> from socket import *

>>> s=socket(AF_INET6,SOCK_STREAM)

>>> s.bind(("",7788))

>>> s.listen(5)

>>> s1,c1=s.accept

>>> s1.recv(100)

'aaaaaaaaa'

>>> print c1

('::ffff:192.168.100.165', 33178, 0, 0)

>>> from os import *

>>> system("ss -tn|grep 7788")

ESTAB 0 0 ::ffff:192.168.100.128:7788 ::ffff:192.168.100.165:33178

>>>

创建 IPv4 地址族的客户端 , 连接服务器 192.168.100.128 上的服务端口 7788 并发送数据c socket,同时在系统层面用 ss 命令输出 TCP 的连接信息

#python

>>> from socket import *

>>> s=socket(AF_INET,SOCK_STREAM)

>>> s.connect(("192.168.100.128",7788))

>>> s.send(b"aaaaaaaaa")

9

>>> from os import *

>>> system("ss -tn|grep 7788")

ESTAB 0 0 192.168.100.165:33178 192.168.100.128:7788

>>>

6. IPv6客户访问 IPv6服务

新开发的网络客户端和服务端应用socket建议采用IPv6地址族的AF_INET6,此类通讯是向IPv6迁移的最终目标,通讯的源和目的地址都是128bit的IPv6地址c socket

缺省情形下IPv6的服务端可以同时接受来自IPv4和IPv6的客户端的连接请求,在只有IPv6的环境下,可以通过setsockopt设置IPv6_V6ONLY为1或设置sysctl的参数net.IPv6.bindv6Only=1(LINUX环境)使服务程序只接受来自IPv6地址族的连接请求c socket。在IPv6的客户端也可以采用同样设置,客户端只能和IPv6的地址进行通讯。

IP双栈环境下网络应用迁移

图6 IPv6客户访问IPv6服务

7. IPv6客户端访问 IPv4服务端

新开发的网络客户端为了适应IPv6环境,采用IPv6地址族AF_INET6的socket,但留存的IPv4的服务端暂时无法改造,仍然采用IPv4地址族AF_INET,由于客户端采用的是IPv6地址族,IPv6的客户端访问IPv4服务端可以利用服务端的IPv4-mapped IPv6 地址方式和IPv4的服务端通讯c socket

IPv6客户端通过本机的IPv4协议栈,数据包头的源和目的网络层地址都是标准的IPv4地址,IPv4服务端无法区分客户端应用是基于IPv4地址族还是基于IPv6地址族c socket

IP双栈环境下网络应用迁移

图7 IPv6客户端访问IPv4服务端

创建 IPv4 地址族的 socket c socket,绑定服务监听端口 8888 ,在此端口上接收客户连接请求并输出接收到的数据以及连接请求客户端的 IP 地址以及 TCP 端口号信息

#python

>>> from socket import *

>>> s=socket(AF_INET,SOCK_STREAM)

>>> s.bind(("",8888))

>>> s.listen(4)

>>> s1,c1=s.accept

>>> s1.recv(100)

'xxxxxxxxx'

>>> print c1

('192.168.100.128', 49314)

>>> from os import *

>>> system("ss -tn")

State Recv-Q Send-Q Local Address:Port Peer Address:Port

ESTAB 0 0 192.168.100.165:8888 192.168.100.128:49314

>>>

创建 IPv6 地址族的 socket 客户端 , 用 IPV4-mapped 地址连接服务端 192.168.100.165 上的服务监听端口 8888 c socket,连接成功后发送数据给服务端,同时在系统层面采用 ss 命令输出 TCP 的连接信息

#python

>>> from socket import *

>>> s=socket(AF_INET6,SOCK_STREAM)

>>> s.connect(("::ffff:192.168.100.165",8888))

>>> s.send("xxxxxxxxx")

>>> from os import *

>>> system("ss -tn")

State Recv-Q Send-Q Local Address:Port Peer Address:Port

ESTAB 0 0 ::ffff:192.168.100.128:49314 ::ffff:192.168.100.165:8888

8. IPv6 V6ONLY应用

基于IPv6地址族的应用缺省情形下可以和基于IPv4以及IPv6地址族的应用互通,设置IPv6_V6ONLY参数可以改变此行为,此参数在HPUX、Linux环境下缺省为0,Windows环境下缺省为1,如果设置为1,那么应用只能和IPv6地址族的应用互通,在一个只有IPv6的网络环境下,可以设置IPv6_V6ONLY为真c socket。下面针对客户端和服务端的应用设置分别说明.

IPv6 地址族的 socket 设置 V6ONLY 选项为真的客户端访问 IPv4 服务端 ( 失败 )

在IPv6的客户端程序中调用setsockopt来设置IPv6_V6ONLY为真,那么此客户端只能和IPv6的服务端进行通讯,不能通过IPv4-mapped IPv6地址的形式和IPv4的服务端进行通讯c socket

IP双栈环境下网络应用迁移

图8 IPv6 V6ONLY应用

IPv4 客户访问 IPv6 V6ONLY 服务端 ( 失败 )

采用IPv6 地址族AF_INET6的socket 的服务端应用缺省能接受来自IPv4和IPv6客户端的连接请求(windows系统下IPv6地址族的应用V6ONLY缺省为真),在只有IPv6的环境下,调用setsockopt设置IPv6_V6ONLY为1来拒绝来自IPv4 的客户端连接请求,只接受来自IPv6 客户端的连接请求c socket

由于服务端收到IPv4客户端的地址是IPv4的,服务端在查找到对应的TCP/UDP协议控制块后,发现协议控制块被标记为V6ONLY,故拒绝连接请求c socket

IP双栈环境下网络应用迁移

图9 IPv6 V6ONLY应用

V6ONLY服务端示例c socket,创建一个基于IPv6地址族的socket,设置新建socket的IPV6_V6ONLY的选项为真,绑定服务端口9999并启动服务监听

# python

>>> from socket import *

>>> from os import *

>>> s=socket(AF_INET6,SOCK_STREAM)

>>> s.setsockopt(IPPROTO_IPV6,IPV6_V6ONLY,1)

>>> s.bind(('',9999))

>>> s.listen(8)

>>> system("ss -6ltne|grep 9999")

>>>

9. 现有应用改造

在具备源代码的情形下,将一个IPv4的服务端程序改造成支持IPv6的程序,改造后的应用能同时接受来自IPv4和IPv6客户端的服务请求,改造主要涉及到的是有关地址结构的变化c socket

以C源码为例c socket,现有的支持IPv4 地址族的服务端代码:

int sock,sock_new;

unsigned short srv-port;

struct sockaddr_in serv,cli;

socklen_t cli_len=soizeof(cli);

sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

memset((&serv,0,sizeof(serv));

serv.sin_family=AF_INET;

serv.sin_len=sizeof(serv);

serv.sin_port=htons(srv-port));

bind(sock,(struct sockaddr*)&serv,sizeof(serv));

listen(sock,5);

sock_new=accept(sock,(struct sockaddr*)&cli,&len);

改造后的基于IPv6地址族的服务程序c socket,可以同时接受来自IPv4和IPv6 客户端的连接请求:

int sock,sock_new;

unsigned short srv-port;

struct sockaddr_in6 serv,cli;

socklen_t cli_len=soizeof(cli);

sock=socket (AF_INET6,SOCK_STREAM,IPPROTO_TCP);

memset((&serv,0,sizeof(serv));

serv.sin6_family=AF_INET6;

serv.sin6_len=sizeof(serv);

serv.sin6_port=htons(srv-port));

bind(sock,(struct sockaddr*)&serv,sizeof(serv));

listen(sock,5);

sock_new=accept(sock,(struct sockaddr*)&cli,&len);

10. IPv6地址族服务端程序设计

在协议迁移的过渡阶段c socket,为了能同时响应来自IPv4和IPv6的客户端应用请求,服务端程序的设计常见有两种方式:

只创建 1 个 IPv6 地址族的 socket 监听

只建立一个AF_INET6地址族的监听socket来同时响应来自IPv4和IPv6客户端的服务请求c socket。当接受到的请求目的地址属于IPv4地址族时,服务器内部采用IPv4-mapped IPv6地址的方式来表示,此连接请求通过IPv4协议栈处理,当接受到的请求目的地址属于IPv6地址族时,连接请求通过IPv6协议栈处理。

Linux上常用的ftp服务器vsftpd采用此类方式实现c socket,从下面的输出可以看出,针对TCP Port 21只有一条基于AF_INET6地址族的监听项,并且V6ONLY选项是0,也即意味着此TCP port 21可以同时接受来自IPv4和IPv6客户端的连接请求:

#ss -6ltnep|grep :21

LISTEN 0 32 :::21:::* users:(( "vsftpd",pid=1290,fd=3)) ino:16150 sk:12 v6only:0 <->

同时创建 1 个 IPv4 socket 监听和 1 个 IPv6 socket 监听

为了避免在内核中采用IPv4-mapped IPv6地址,在同一个服务端程序中分别创建AF_INET和AF_INET6两个地址族的socket,同时监听在相同的端口上,IPv4地址族AF_INET的监听socket负责接受来自IPv4客户端的连接,并且设置IPv6地址族socket的IPv6_V6ONLY属性为真,从而只负责接受来自IPv6客户端的连接,由于有两个处于监听状态的socket, 调用select来处理来自不同socket地址族的连接请求c socket

Linux系统中sshd的是采用此方式实现的c socket,从strace跟踪sshd相关系统调用的输出或查看openssh的源码都可以验证:

socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3

setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

bind(3, {sa_family=AF_INET, sin_port=htons(22), sin_addr=inet_addr("0.0.0.0")}, 16) = 0

listen(3, 128) = 0

socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP) = 4

setsockopt(4, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

setsockopt(4, SOL_IPv6, IPv6_V6ONLY, [1], 4) = 0

bind(4, {sa_family=AF_INET6, sin6_port=htons(22), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0

listen(4, 128) = 0

select(5, [3 4], NULL, NULL, NULL)

ss指令输出可以直观的表明其机制:

# ss -ltnep|grep 22

LISTEN 0 128 *:22*:* users:(( "sshd",pid=3236,fd=3)) ino:88651 sk:1f <->

LISTEN 0 128 :::22 :::* users:(( "sshd",pid=3236,fd=4)) ino:88653 sk:20 v6only:1<->

11. IPv6客户端程序设计

IPv6客户端通过指定地址或域名来访问IPv4以及IPv6的服务端,如果服务提供方是IPv4,那么需要用IPv4-mapped IPv6地址的形式去访问c socket。针对域名访问,需要采用DNS实现名称来解析,在IPv4环境下的解析库函数调用有两个:gethostbyname(正向解析)、gethostbyaddr(反向解析),此类解析用于IPv4环境。IPv6环境下则采用getaddrinfo和getnameinfo来解析IPv4和IPv6地址信息,getaddrinfo根据域名/服务名称以及相关hints等信息返回一组A和AAAA记录,每条返回的记录附加有用于创建socket所需的AF_xxx以及SOCK_xxx等信息,客户端程序通常利用循环语句针对getaddrinfo返回地址列表的每一项尝试创建socket以及connect对端,直到发现针对一条地址记录的socket和connect调用都返回成功为止。

客户端只需要知道服务端的域名以及服务端口,采用getaddrinfo调用返回的地址列表信息,就可以创建合适的地址族的socket和服务端连接c socket

Python 的 getaddrinfo 使用示例 :

解析地址的列表
#python

>>> from socket import *

>>>from os import *

>>> from pprint import pprint

>>> list=getaddrinfo("")

>>> pprint(list)

[(2, 1, 6, '', ('23.7.213.221', 80)),

(2, 2, 17, '', ('23.7.213.221', 80)),

(10, 1, 6, '', ('2600:1417:a000:195::1463', 80, 0, 0)),

(10, 2, 17, '', ('2600:1417:a000:195::1463', 80, 0, 0)),

(10, 1, 6, '', ('2600:1417:a000:1b4::1463', 80, 0, 0)),

(10, 2, 17, '', ('2600:1417:a000:1b4::1463', 80, 0, 0))]

>>> list[0][0:3]

(2, 1, 6)

>>> list[0][4]

('23.7.213.221', 80)

连接DNS解析返回列表中的第一项的IPv4地址

c socket

,成功后在系统层面输出TCP连接的四元组信息:
>>> s=socket(*list[0][0:3])

>>> s.connect(list[0][4])

>>> system("ss -tn|grep 80")

ESTAB 0 0 192.168.100.165:39838 23.7.213.221:80 0

尝试用DNS解析返回列表中的第三项IPv6的地址信息来连接地址,连接失败:

>>> s1=socket(*list[2][0:3])
>>> s1.connect(list[2][4])

Traceback (most recent call last):

File "", line 1, in

File "/usr/lib64/python2.7/socket.py", line 224, in meth

return getattr(self._sock,name)(*args)

socket.error: [Errno 101] Network is unreachable

>>>

C 语言的 getaddrinfo 使用示例:

main(int argc, char **argv)

struct addrinfo *res, *ainfo;

struct addrinfo hints;

int error;

struct sockaddr_in6 peeraddr6;

struct sockaddr_in6 addr6;

char connect_addr[INET6_ADDRSTRLEN];

if (argc != 2) {

fprintf(stderr, "Usage: %s \n", argv[0]);

exit(1);

memset ((char *)&hints, 0, sizeof(hints));

hints.ai_socktype = SOCK_STREAM;

error = getaddrinfo(argv[1], ");

if (error != 0)

exit(1);

for (ainfo = res; ainfo != NULL; ainfo = ainfo->ai_next) {

s = socket (ainfo->ai_family,ainfo->ai_socktype,ainfo->ai_protocol);

if (s == -1)

continue;

if (connect(s, ainfo->ai_addr, ainfo->ai_addrlen) == -1)

continue;

else

break;

java,Python等语言对这些细节都做了很好的封装,如python中的socket.create_connection就可以实现以上C代码的所有功能

c socket

12. 结束语
本文对基于不同IP地址族的应用的访问方式以及具体实现的方法进行了简要说明,同时也介绍了如何改造现有IPv4应用以适应新的IPv6环境,介绍了同时适用于IPv4以及IPv6环境的应用的方法

c socket

。IPv6应用迁移是未来应用迁移发展的方向,希望本文内容对IPv6应用迁移感兴趣的读者有所帮助。



推荐阅读
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 如何方便地退订邮件列表,避免混乱和烦恼
    本文介绍了如何方便地退订邮件列表,避免混乱和烦恼。文章指出,退订邮件列表可能会造成混乱,特别是当被意外添加到列表中时。为了快速、轻松地取消订阅,建议不要将退订电子邮件发送到用于发布消息的电子邮件地址。文章还介绍了邮件列表由邮件列表软件控制,作为邮件列表成员,可以对该软件进行一些用户控制。一些邮件列表允许使用自动电子邮件地址退订,但这可能会带来一些混乱。最后,文章提到退订邮件列表需要向电子邮件服务器发送特殊命令来脱离列表。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 三、寻找恶意IP并用iptables禁止掉找出恶意连接你的服务器80端口的IP,直接用iptables来drop掉它;这里建议写脚本来运行, ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 【重识云原生】第四章云网络4.8.3.2节——Open vSwitch工作原理详解
    2OpenvSwitch架构2.1OVS整体架构ovs-vswitchd:守护程序,实现交换功能,和Linux内核兼容模块一起,实现基于流的交换flow-basedswitchin ... [详细]
author-avatar
肉斯情-
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有