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

4基本TCP套接字编程:UNIX网络编程卷1

禁止转载和他用keypoints:1.讲writea完整`TCPclient/server程序`所需的

禁止转载 和 他用

key points:

1. 讲 write a 完整 `TCP client/server 程序` 
所需的 `基本 套接字函数`

next chapter 将 `开发` TCP client/server 程序

`本书` 将 `围绕 该 client/server 程序` 展开, 
并对其进行 `多次改进` 

2. 讲 并发 server 
每个 `client connection` 都迫使 server 为它 `fork` ( 派生 ) 1 个 `新进程`

本章 `只` 考虑 `fork 每 client 单 进程 `

3. TCP client / server 间 `典型事件时间表`  
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png

4.1 socket

指定 期望的 通信协议 的 (族 + 套接字类型 + 协议类型)

#include 
int
socket(int family, int type, int protocol);

return:
    `非负 套接字描述符` (socket descriptor), success
    -1, error
faimly  : `协议族`
    IPv4/6 等

type    : `套接字 类型`
    字节流/数据报 套接字 等

protocol: `传输层 协议类型`
    TCP/UDP/SCTP 协议
    set 0 -> 选 给定 family 和 type 组合 的 系统默认值

1. 套接字 描述符 ( socket descriptor / 简称 sockfd ): socket 成功 时, return 的 small 非负 integer, 与 文件描述符 类似

2. AF_ / PF_

`address family (地址族) / protocol family (协议族)`

历史 idea: 
1) 单 协议族 support 多 地址族
2) PF_ / AF_ 用于 创建套接字 / 套构

实际上:
1) support 多 地址族 的 协议族 从未出现
2)  中 
`given 协议 的 PF_ 值 与 此协议的 AF_` 值 `相等`
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png

4.2 connect

TCP client 用于 建立 与 TCP server 的 connection

#include 
int
connect(int                   sockfd, 
        const struct sockaddr *servaddr, 
        socklen_t             addrlen);

return
     0 , success
    -1 , error
3 参数:

sockfd
    socket 返回的 套..符
`通用套构 的 pointer / len`
    `套构 含 server 的 IP 地址 + port number`
    addrlen 通常为 sizeof(servaddr)
1. client 调 connect 前 不必非得调 bind

reason: 若 需要, `kernel` 会 `确定 源 IP 地址`, 
        并 choose a temp port 作 源 port
2. 若为 `TCP 套接字`

调 connect 会 激发 TCP 3 路握手, 
且 concect 仅在 `client 认为连接建立成功` 或 `出错` 时 才 `return`

2.1 client 认为 连接建立成功: 
第 2 路握手成功, 即 client 收到 SYN 分节的 ACK 响应

2.2 出错 return 的 cases:

(1) 若 指定时间后, TCP client `没收到 SYN 分节 的 响应`, 
则 return ETIMEDOUT 错误

// eg
调 connect -> 4.4BSD kernel 发 SYN 分节 
-> 若 无 响应, 等待 6s 后 再发 1个
-> 若 仍无响应, 等待 24s 后 再发 1个
-> ...
-> 若 总共等待 75s 后, 仍无响应, 则 return ETIMEDOUT 错误

(2) client 收到 SYN `响应 是 RST`

表明 `server 在 指定 port` 上 `没有 进程 在 等待 与之连接`
(如 server 进程 maybe 没 运行)

这是1种 `硬错误 ( hard error )`

client 收到 RST 就马上 return ECONNREFUSED 错误

RST

: TCP 在 发生错误时 发送的1种 TCP 分节

产生 RST 的 3 条件:

1) SYN 到达 目的地 某 port, 但 该 port 上 没有 正在监听的 server

2) `TCP 想 取消 1个 已有 连接`
3) TCP 收到1个 `根本不存在的 连接上` 的 分节
(3) client 发的 SYN 在 中间 某 `router` 上 
引发 "destination unreachable" ICMP 错误,
则 认为是 `软错误 (soft error)`
 
client 主机的 `kennel 保存该 ICMP msg`, 
并按 `case1 中 time interval 继续发 SYN`,
若 规定时间(4.4BSD 75s) 后 仍未收到 响应,
则 把 保存的 `ICMP 消息` 作为 
`EHOSTUNREACH 或 ENETUNREACH 返回给 进程`

(4) 按 local 系统 转发表, `无 到达远程系统 的 path`

(5) connect 不等待 就 rteturn
  1. connect 导致 current 套接字CLOSE 状态 转换到 SYN_SENT 状态

conncet 失败 时, 套接字 不再可用, 必须 close sockfd 并 重新调 socket

// eg. 
图 11-10

4.3 bind

把 local 协议地址 赋予 1 个 套接字

协议地址 含义 取决于 协议本身:
网际协议 IP, 协议地址 是 32 位 IPv4 地址128 位 IPv6 地址TCP或 UDP 端口号组合

#include 

int 
bind(int                   sockfd, 
     const struct sockaddr *myaddr,
     socklen_t             addrlen);
参数2/3
    特定于协议的 地址结构 的 pointer/len

1. 捆绑 or 选择 地址 和/或 端口 到 套接字 ?

捆绑(binding): 3 个对象 套接字 / 地址 / 端口

(1) `捆绑 地址 和/或 端口 到 套接字`
    由 (client / server 主机 的) 应用进程 捆绑(即 指定)
    
(2) `选..`  
    由 (client / server 主机 的) kernel 选
绑定 ( bound ): 捆绑成功后的状态

2. which case 下, 应用进程 应该 + 怎么 捆绑? kernel 应该 + 怎么 选?

以 TCP 为例, 地址 即 `IP 地址`

2.1 捆绑/选 port

(1) 进程 可 `捆绑 port` 到 套接字

1) 对 TCP client: rare case
应用 需要 1个 `reserved port` 时,
    应用进程 调 bind 捆绑 port
    
reserved port = 0-1023: 

使用 reserved portserver 必须 以 超级用户特权 启动

2) 对 TCP server: most case
启动 server 时, 捆绑 其 well-known port 到 套接字

(2) kernel 可 `选择 port` 到 套接字
1) 对 TCP client: most case

TCP client 未 调 bind 捆绑 port, 
    则 调 connect 时, kernel 要 选 temp port
    
2) 对 TCP server: rare case: 
TCP server 未 调 bind 捆绑 port, 则
    调 listen 时, kernel 要 选 temp port 

// eg   
`RPC (remote procedure call, 远程过程调用) server: `

1> server 的 kernel 为 其 `监听 套接字` 选 temp port
2> port `通过 RPC 端口映射器 register`
3> client 先 `联系 RPC 端口映射器 获取` server 的 `temp port`
   再 connect server  

2.2 捆绑/选 IP 地址

(1) 进程 可 `捆绑 特定 IP 地址` 到 套接字
    该 IP 地址 必须属于 进程所在 `主机 的 网络接口` 之一

1) 对 TCP client: rare case
指定 该 套接字 上 发送的 IP 数据报 的 `源 IP 地址`

2) 对 TCP server: most case
限定 套接字 只接收 `目的地址` 为 
    `该 IP 地址 的 client connection`

(2) kernel 可 `选择 特定 IP 地址` 到 套接字

1) 对 TCP client: most case
kernel 据 connection 所用 `外出网络接口` 
    选 `源 IP 地址`,
    外出网络接口 取决于 到达 server 所需 path

2) 对 TCP server: rare case
kernel 把 `client 发送的 SYN` 的 `目的IP 地址` 
    作为 `server 的 源 IP 地址`

3. 对 TCP, 调 bind 可 指定 IP 地址 / 端口号 中的 0/1/2 个

IP 地址 / port: set 为 通配地址 /  0 
=> kernel 选 IP 地址 / port

IP 地址 / port: set 为 local IP 地址 / 非 0
=> 进程 指定(绑定) IP 地址 / port

`通配地址`: 由 常值 `INADDR_ANY` 指定, 值 一般为 0
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
`(1) kernel 选 IP 地址 / port 的 时机 ?`

`bind 被调用` 时, 选 temp port
等到 `TCP 套接字 已连接` 时, 才选 local IP 地址

(2) `kernel 选 的 port 如何获得?`
调 getsockname 返回 所选 协议地址

(3) 进程 捆绑 非通配 IP 地址 到 套接字 的例子,
`单主机` 为 `多组织` 提供 `Web 服务器:`

1) 每个 组织 有各自 域名, 如 www.org.com

2) 每个 组织 的 域名 映射到 不同 IP地址, 通常 仍在 `同一子网`
如 子网: 198.69.10 / org1: 198.69.10.128 / org2: 198.69.10.128

3) 把 所有这些 IP 地址 定义为 `单个网络接口` 的 `别名`
如 4.4BSD 系统上 用 ifconfig 的 alias 选项 定义
=> IP 层 将接收 目的地 为 任一 别名地址 的 外来 数据报

4) `每个组织` 启动1个 `HTTP server` 的 `copy`, 
每个 copy 只 `捆绑` 相应组织的 `IP 地址`

(4) 进程 捆绑 非通配 IP 地址 的好处
`kernel` 完成  `给定 目的 IP 地址` 解复用到 `给定 server 进程`

(5) 分组 的 到达接口 / 目的 IP 地址

`目的 IP 地址`: 
只要能 标识 `目的主机` 的 `某个 network interface` 即可

捆绑 `非通配 IP 地址`:
只是 限定 据 `目的 IP 地址` 来 `确定` 递送到 套接字 的 `数据报`

(6) 到达 ( arriving ) / 接收 ( received ) 含义相同, 仅 视角不同:
从 接收主机 以 外 / 内 看 该接口

同义词: 外来 ( incoming )

4.4 listen

仅 由 TCP server 调用

(1) 做 2 件事:

1) 转换 未连接主动套接字被动套接字, 指示 kernel接受 指向 该套接字连接请求

`socket` 创建的 套接字 为 `主动套接字:`
`将 调 connect 发起连接` 的 `client 套接字`

2)规定 kernel 应 set套接字 的 连接队列最大连接数

#include 

int
listen(int sockfd,
       int backlog)
       
return
     0, success
    -1, error
(2) 理解 第 2 参数

`kernel` 为 `监听套接字` 维护 `2 个队列:`

1) 未完成连接队列 ( incomplete connectionqueue )

未..队列 中 创建 一项 时, 来自 监听套接字参数copy 到 即将 建立的 连接 中

连接的创建 是 完全自动的, 无需 server 进程 插手

2) 已完成连接队列 ( completed connectionqueue )

4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
(3) 若 3 路握手 的 3分节 正常完成, 则 
`未..队列 中 任一项 留存时间 为 RTT`

Web server: RTT = 187 ms

(4) 应用进程 应 指定 多大的 backlog ?

大多系统 allow 管理员 修改 backlog

指定 1 个 默认值 + 需要时, kernel 悄然 修改 之 + wrappre func 中 用 环境变量 覆写

1) 指定值 比 kernel 能支持的 最大值 大 时, 
`kernel 悄然 修改` 其为 kernel 能支持的 最大值

2) 若 设为 `常值`, 
则 `想增大 backlog` 时, 需要 `重新编写 server 程序`
(5) `指定 较大的 backlog 值` 的 原因:
随着 client SYN 分节 的 到达, 未完成连接队列 中 项数可能增长,

(6) `client SYN 分节 到达 时, 若 未完成连接队列 full,
TCP 忽略 SYN 分节, 并不发 RST` 的原因

1) 未完成连接队列 full 是 暂时的, 
TCP client 重发 SYN 可 期望在 未..队列中 找到 可用空间

2) `client 无法区分 响应 SYN 的 RST` 表明 的 
是  `该端口 没有 server 在监听`, 
还是 `该端口 有 server 在监听, 但 它的 未..队列 满了`

(7) `三路握手 完成后, server 调 accept 前` 到达的 `data`

由 server TCP 排队, max 数据量 为 相应 已连接套接字接收 buf 大小

4.5 accept

TCP server (进程) 调用, 用于从 已完成连接队列 队头 return 下一 已完成连接
若 已完成连接队列 empty, 则 进程 被置 休眠状态

#include 

int
accept(int              sockfd,
       struct sockaddr *cliaddr,
       socklen_t       *addrlen)
       
return
    非负描述符, success
    -1        , error
(1) 参数
第1参数: `监听套接字 ( listening socket ) 描述符`

第2/3参数 用于 return 已连接 的 `对端/client 进程 的 协议地址`
-> 若 对 client 协议 不感兴趣, 可把 cliaddr/addrlen 置 NULL

(2) 3 个 return 值:

para2: client 协议地址 (pointer)

para3: client 协议地址的长度 (pointer)

形式上的 return 值: 
`已连接套接字 (connected socket) 描述符` or 出错提示的 整数 -1

(3) server 通常 只创建 1 个 listening socket, 它 在 server lifetime 内 一直存在;
server kernelserver 进程 接受每个 client connection 创建 1 个 connected socket, server 完成 对某个 给定 client 的 服务 时, 就 close 相应的 connected socket

图 1-9
`connected socket 每次都在 循环中 关闭,` 
listening socket 在 server lifetime 内 都保持开放

4.6 fork / exec

1. fork 是 UNIX 中 派生 新进程唯一方法

#include 

pid_t
fork(void);
       
return
    0         , 子进程 中
    子进程 ID , 父进程 中
    -1        , 出错
父进程: 调用 fork 的 进程
子进程: 派生的 新进程
2. 理解 fork 的 `难点`

调用 fork 1次, fork 却 返回 2次

`在 父 / 子进程 中 都返回 1 次`, 
返回值 是 `子进程 的 进程 ID / 0`

返回值 本身 告知 当前进程 时 父进程 还是 子进程

3. fork 在 子进程 中 return `0 而不是 父进程 的 进程 ID` 的 原因

(1) any 子进程 只有1个父进程,
而 子进程 可通过调 `getppid 获取 父进程 的 进程 ID`

(2) 父进程 可能有 多个 子进程, 
且 `无法 获取 各子进程 的 进程 ID`

`父进程 为了 跟踪 各子进程 的 进程 ID`, 
只能 在 父进程中 record 由 fork 返回的 子进程 ID

4. 父进程 调 fork 之前 打开的所有 描述符 在 fork 返回后 由 子进程 share

网络 server 利用该特性:
父进程 accept 
-> fork 
-> 父进程 所接受的 connected socket 在 父/子 进程间 共享: 通常, 
`子进程 接着 读/写` 该 connected socket,
`父进程 close` 该 connected socket,
5. fork 的 2个 `典型用法:`

(1) 进程 fork 自身 的 副本, 各副本 可 同时执行 各自的操作

这是 网络 server 典型用法

(2) 进程 fork 自身 的 副本, 副本( 子进程) 调 exec 把 自身 ( 当前进程 映像 ) 替换成 新程序( 如 硬盘上的 可执行程序文件 ), 新程序 通常从 mian 开始执行, 子进程 ID 不变

`shell` 等 程序 典型用法
`调用进程 (calling process)`: 调用 exec 的 进程
`新程序 (new program)`: 新执行 的 程序
6. exec 函数
execl/v/le/ve/lp/vp 共 6 个函数 中 是哪个被调用 不重要时, 统称为 exec

出错 时 才 return 到 caller, 否则, 控制 被 转移给 新程序起点 main

4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png

4.7 并发 server

1. simplest method

fork 1 个 子进程 来 服务 每个 cient

4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
server:
accept -> fork ->

父进程 等待 另一连接(通过 监听套接字)
子进程 服务 client (通过 已连接套接字)

=> 父/子 进程 不再需要 服务 client / listen 
=> `父/子 进程 close 已连接套接字 / 监听套接字`
2. 子进程 exit 前 可 不必 close(connfd): 
进程 exit 时, 会 close 所有 由 kernel 打开的 该 进程的 描述符

3. why 父进程 close 已连接描述符 没有 终止 与 client 的 connection ?

(1) fork 后, 描述符 在 父/子 进程间 共享(即 被 copy) -> 套接字描述符 引用计数 加 1

(2) 套接字 ( 描述符 ) 引用计数 + 文件表项 中 维护 + 套接字 真正的 清理 和 资源释放 要等到其 引用计数 为 0 时 才发生

`套接字(描述符) reference count`:
当前 打开着的  `该 套接字 ( 描述符)` 的 reference count
`文件 / 文件描述符` 同理
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png

4.8 close

关闭 套接字, 可能 终止 TCP 连接

#include 

int
close(int sockfd);
       
return
     0 , success
    -1 , 出错

close TCP 套接字 的 default behavior标记 套接字 为 已关闭, 然后 立即 return 到 调用进程

(1) 该 套接字描述符 不能再由 调用进程 使用, 即
`不能再 作 read / write 第 1 参数`

(2) 但 `TCP 仍将 尝试 发` 送 `已排队等待发送到对端` 的 `任何数据`,
发完后 才是 正常的 `TCP 连接终止序列`

(3) chapter7.5 `SO_LINGER 套接字选项` 可用于
`改变` TCP 套接字这种 `default behavior`

(4) `描述符  引用计数`

`父进程 close` 已连接套接字 `并不引发 TCP 连接终止序列`

若 确实想在 某连接上 发 FIN, 可调 `shutdown`
6.5 节 讲 该 scenario 的 动机

(5) `父进程 不 close` accept 返回 的 `已连接 套接字`, 
并发 server 中 发生什么?

1) `父进程 最终将 耗尽 可用描述符`,
因为 任何进程 任何时刻 可拥有的 打开着的 描述符 有限

2) 妨碍 TCP 连接终止序列 的 发生, 导致 `连接 一直打开着`

4.9 getsockname / getpeername

return 某 套接字 关联的 本地/外地 协议地址 ( getsockname / getpeername)

#include 

int
getsockname(int             sockfd,
            struct sockaddr *localaddr,
            socklen_t       *addrlen);
       

int
getpeername(int             sockfd,
            struct sockaddr *peeraddr,
            socklen_t       *addrlen);
            
return
     0 , success
    -1 , 出错
需要 2 个函数的 场景:
(1) TCP client 上
`没调 bind -> connect 成功 return` 
-> getsockname 用于 return kernel 赋予 本连接 的 
`local IP 地址 和 local 端口号`

(2) 以 `端口号 0` 调 `bind`
-> getsockname 用于 return kernel 赋予 本连接 的
`local 端口号`

(3) TCP server 上
以 `通配 IP 地址` 调 `bind -> accept 成功 return`
-> getsockname 用于 return kernel 赋予  本连接 的 
`local IP 地址 `

(4) server 是由 调用过 accept 的某 进程 通过 调 exec 执行程序 时,
server 能获取 client唯一途径是getpeername

inet fork 并 exec 某 TCP 服务器程序 时, 就是 such case

`inet 派生 server`
4 基本 TCP 套接字 编程: UNIX 网络编程 卷1
image.png
上述 `Telnet server 启动后, 必须 获取 connfd`

2种方法:

1) 调 exec 的 `进程` 把 `connfd 格式化为 字符串`,
再 作为 `命令行参数` pass 给 新程序

2) 约定 调 exec 前, 总是把 某个特定 描述符 置为 
所接受的 已连接套接字描述符

inet 采用 方法2:
总是把 `描述符 0/1/2` 置为 `所接受的 已连接套接字描述符`

推荐阅读
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • 本文介绍了在mac环境下使用nginx配置nodejs代理服务器的步骤,包括安装nginx、创建目录和文件、配置代理的域名和日志记录等。 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • CentOS 6.5安装VMware Tools及共享文件夹显示问题解决方法
    本文介绍了在CentOS 6.5上安装VMware Tools及解决共享文件夹显示问题的方法。包括清空CD/DVD使用的ISO镜像文件、创建挂载目录、改变光驱设备的读写权限等步骤。最后给出了拷贝解压VMware Tools的操作。 ... [详细]
  • Windows7 64位系统安装PLSQL Developer的步骤和注意事项
    本文介绍了在Windows7 64位系统上安装PLSQL Developer的步骤和注意事项。首先下载并安装PLSQL Developer,注意不要安装在默认目录下。然后下载Windows 32位的oracle instant client,并解压到指定路径。最后,按照自己的喜好对解压后的文件进行命名和压缩。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • CEPH LIO iSCSI Gateway及其使用参考文档
    本文介绍了CEPH LIO iSCSI Gateway以及使用该网关的参考文档,包括Ceph Block Device、CEPH ISCSI GATEWAY、USING AN ISCSI GATEWAY等。同时提供了多个参考链接,详细介绍了CEPH LIO iSCSI Gateway的配置和使用方法。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
author-avatar
鐘文斌kebenJ
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有