禁止转载 和 他用
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 间 `典型事件时间表`
指定 期望的 通信协议 的 (族 + 套接字类型 + 协议类型)
#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_` 值 `相等`
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
套接字
从 CLOSE 状态 转换到 SYN_SENT 状态
conncet 失败
时, 套接字 不再可用
, 必须 close sockfd 并 重新调 socket
// eg.
图 11-10
把 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 port
的 server
必须 以 超级用户特权
启动
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
`(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 )
仅 由 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 )
(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 大小
由 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 kernel
为 server 进程 接受
的 每个 client connection 创建 1 个 connected socket
, server 完成
对某个 给定 client 的 服务
时, 就 close 相应的 connected socket
图 1-9
`connected socket 每次都在 循环中 关闭,`
listening socket 在 server lifetime 内 都保持开放
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
1. simplest method
fork 1 个 子进程 来 服务 每个 cient
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
`文件 / 文件描述符` 同理
关闭 套接字, 可能 终止 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 连接终止序列 的 发生, 导致 `连接 一直打开着`
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`
上述 `Telnet server 启动后, 必须 获取 connfd`
2种方法:
1) 调 exec 的 `进程` 把 `connfd 格式化为 字符串`,
再 作为 `命令行参数` pass 给 新程序
2) 约定 调 exec 前, 总是把 某个特定 描述符 置为
所接受的 已连接套接字描述符
inet 采用 方法2:
总是把 `描述符 0/1/2` 置为 `所接受的 已连接套接字描述符`