第12章 网络通信:连接的建立
在互联网时代,网络通信编程已经是一个程序员必不可少的技能之一。几乎所有的产品都会涉及网络操作或访问。在Linux编程环境中,系统提供了socket套接字为程序员提供统一的网络编程接口。本书将对socket套接字进行详细的分析,由于篇幅较多,所以将内容分为三章来讲述。本章主要讲解与连接相关的分析,包括socket、bind、connect、listen和accept系统调用及相关的源码追踪。这里假设读者有一定的Linux网络编程基础,所以对于系统调用的解释都是点到为止,只针对不常见或容易忽视的问题进行详细说明。
12.1 socket文件描述符
socket翻译成中文是插座、插槽的意思,而在网络编程中,其被翻译为“套接字”。Linux环境下,我们经常说“一切皆文件”。因此套接字也被视为一种文件描述符。首先,来看看如何使用socket系统调用创建一个套接字,代码如下:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);其中的参数解释如下:
domain:用于指示协议族名字,如AF_INET为IPv4。type:用于指示类型,如基于流通信的SOCK_STREAM。protocol:用于指示对于这种socket的具体协议类型。一般情况下,使用前两个参数限定后,只会存在一种协议类型对应该情况。这时,可以将protocol设置为0。但是在某些情况下,会存在多个协议类型,这时就必须指定具体的协议类型。
成功创建socket后,会返回一个文件描述符。失败时,该接口返回-1。
那么对于Linux内核来说,如何知道一个文件描述符是一个套接字,还是一个普通文件呢?其实这个问题也可以扩展到,内核如何知道一个文件描述符的具体类型,如何调用实际类型的操作函数呢?这仍然是VFS的魔力。
在第1章中,我们了解了文件描述符fd与内核文件结构struct file之间的关系,后者是内核用于管理文件的真正结构,其中的成员变量file->f_op为VFS支持的所有文件操作。VFS层无须关心该文件file的实际类型,它会直接调用file->f_op中的操作函数(这样的处理,与面向对象语言中的多态是类似的)。
对于套接字来说,只要在创建套接字时,将file->f_op设置为正确的套接字操作函数即可。该操作是在socket->sock_map_fd->sock_alloc_file中完成的,代码如下:
static int sock_alloc_file(struct socket *sock, struct file **f, int flags)
{
......
......
/* 申请一个struct file,并将socket_file_ops作为参数来传递.
在alloc_file中,会将socket_file_ops赋给file->f_op.
*/
file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
&socket_file_ops);
......
......
/* 让sock->file指向file,完成sock和file的关联 */
sock->file = file;
file->f_flags = O_RDWR | (flags & O_NONBLOCK);
file->f_pos = 0;
file->private_data = sock;
*f = file;
return fd;
}尽管Linux内核是使用C语言编写的,但是其应用了很多面向对象的设计思想。以这里的file为例,内核利用f_op(对象操作函数指针集合)指向具体对象的操作函数集合。这样一来,对于VFS来说,就只须关心struct file,而无须关心具体的对象类型了,它会在处理过程中,调用正确的处理函数。
12.2 绑定IP地址
在成功创建套接字后,该套接字仅仅是一个文件描述符,并没有任何地址与之关联。使用该socket发送数据包时,由于该socket没有任何IP地址,内核会根据策略自动选择一个地址。但是,在某些情况下,我们需要手工指定socket使用哪个IP地址进行发送。这时,就需要使用bind系统调用了。
12.2.1 bind的使用
bind系统调用的接口定义如下:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);其中的参数解释如下:
sockfd:表示要绑定地址的套接字描述符。addr:表示绑定到套接字的地址。addrlen:表示绑定的地址长度。
返回值0表示成功,-1则表示错误。
因为Linux的套接字是针对多种协议族的,而每个协议族都可以有不同的地址类型。所以Linux套接字关于地址的系统调用,统一使用了一个公共结构体,并要求调用者将实际地址参数进行强制类型转换,以此来避免编译警告。
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}因为每个协议族的地址类型各不相同,所以需要通过参数addrlen来告诉内核这个地址的实际大小。
说明
struct sockaddr数据类型会在socket涉及地址的所有接口中出现。这是因为套接字接口要支持所有的协议族,所以涉及地址的地方都使用了一个统一的地址结构struct sockaddr。
下面是一个简单示例:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define LOOPBACK_ADDR 0x7F000001
#define LISTEN_PORT在上面的示例中,我们创建了一个TCP套接字,并将回环地址127.0.0.1和端口1234绑定到这个套接字上。运行这个程序,然后通过netstat检查监听端口:
[fgao@ubuntu ~]#netstat -ant
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:1234 0.0.0.0:* LISTEN
从上面的输出可以看到,创建的套接字已经成功地绑定了指定的地址和端口。
12.2.2 bind的源码分析
bind源码入口位于net/socket.c中,如下所示:
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
/* 由文件描述符获取struct socket.前文已经介绍了fd与struct socket的关联关系. */
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
/* umyaddr是用户空间地址,这里将其复制到内核空间address变量中 */
err = move_addr_to_kernel(umyaddr, addrlen, (struct sockaddr *)&address);
if (err >= 0) {
/* 对bind动作进行安全性检查 */
err = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
if (!err) {
/* 调用对应协议的bind动作 */
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
}
}
fput_light(sock->在bind的调用中,根据不同的协议调用不同的实现函数(Linux的内核代码中,大量使用了这种面向对象的设计思路)。对于AF_INET协议族来说,无论是面向连接的SOCK_STREAM类型,还是SOCK_DGRAM协议类型,其实现函数均是inet_bind。下面来看一下inet_bind的具体实现:
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct sock *sk = sock->sk;
struct in
/*
如果具体协议实现了bind函数,则调用协议的bind函数.
AF_INET协议族中,只有IPPROTO_ICMP和
IPPROTO_IP实现了自己的bind函数,
IPPROTO_TCP和IPPROTO_UDP都使用AF_INET通用的函数,即
这个inet_bind.
*/
if (sk->sk_prot->bind) {
err = sk->sk_prot->bind(sk, uaddr, addr_len);
goto out;
}
err = -EINVAL;
/* 检查地址长度 */
if (addr_len < sizeof(struct sockaddr_in))
goto out;
if (addr->sin_family != AF_INET) {
/* 本来要求地址的协议族要与sock相同,必须为AF_INET,但是这里有个兼容性问题.允许协
议族为AF_UNSPEC并且地址为INADDR_ANY的任意地址 */
err = -EAFNOSUPPORT;
if (addr->sin_family != AF_UNSPEC ||
addr->sin_addr.s_addr != htonl(INADDR_ANY))
goto out;
}
/* 判断地址类型 */
chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr);
err = -EADDRNOTAVAIL;
/*
sysctl_ip_nonlocal_bind系统控制开关,允许bind非本地IP;inet->freebind为一个
socket选项,允许该socket bind任意IP;在上面这些变量均不成立时,指定地址又不是任意的
本地地址INADDR_ANY,地址类型又不是本地地址类型,多播或广播时,则bind失败.
*/
if (!sysctl_ip_nonlocal_bind &&
!(inet->freebind || inet->transparent) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL &&
PROT_SOCK(1024),则需要检查用户是否有权限创建知名端口
*/
if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
goto out;
lock_sock(sk);
err = -EINVAL;
/* 确保套接字不会被bind两次 */
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock;
/* 使用参数设置套接字的接收和发送地址 */
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
/* 如果参数地址是多播或广播类型,则重置发送源地址为0,表示在发送时,使用的是设备地址 */
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
inet->inet_saddr = 0; /* Use device */
/*
调用协议自定义的操作函数get_port,判断该端口是否可以使用.
虽然这里是一个查询的动作,但是却会有修改的动作.
当该端口可以使用时,会让inet_sk(sk)->inet_num = snum;
这样做,是因为查询动作已经获得了锁.在确定可以使用该端口时,直接修
改inet_num,这样既可以保证设置端口的原子性,同时还可以提高性能
*/
if (sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
/* 如果设置了bind地址,则置上相应的标志 */
if (inet->inet_rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
/* 如果设置了源端口,则设置相应的标志 */
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
/* 设置inet_sport,其为网络序 */
inet->inet_sport = htons(inet->inet_num);
/* 重置目的地址和端口 */
inet->inet_daddr = 0;
inet->inet_dport = 0;
/* 重置该套接字的路由信息 */
sk_dst_reset(sk);
err = 0;
out_release_sock:
release_sock(sk);
out:
return err;
}说明
无论是APUE还是man手册,在讲解bind的时候都有点问题,或有偏差,或不够详尽。从上面的源码我们知道,通过使用系统控制开关
sysctl_ip_nonlocal_bind或套接字选项可以让套接字bind一个非本机地址。但APUE却说套接字只能绑定本机的有效地址——当然这也是由于APUE距现在的时间太久了,而man手册都没有提及非本机地址的事情。
12.3 客户端连接过程
12.3.1 connect的使用
connect的原型为:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);其中的参数解释如下:
int sockfd:套接字描述符。const struct sockaddr *addr:要连接的地址。socklen_t addrlen:要连接的地址长度。
返回值0表示成功,-1表示失败。
connect的用途是使用指定的套接字去连接对于TCP套接字来说,connect实际上是要真正地进行三次握手,所以其默认是一个阻塞操作。那么是否可以写一个非阻塞的TCP connect代码呢?这是一个合格的网络开发工程师的基本功,具体的实现可以参看UNPv1的实现。更重要是要理解其原理,这样才能在需要的时候,信手拈来。
12.4 服务器端监听与连接
12.3.2 connect的源码分析
connect的源码入口位于socket.c,代码如下:
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
/* 通过文件描述符获取 socket 结构 */
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
/* 将用户空间地址复制到内核空间变量 address 中 */
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;
/* 与 bind 类似,调用与协议族对应的 connect 操作函数 */
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
sock->file->f_flags);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}对于 AF_INET 协议族来说,面向连接的协议类型是 SOCK_STREAM,其连接函数为 inet_stream_connect,而非面向连接的协议类型 SOCK_DGRAM,其连接函数为 inet_dgram_connect。这很合理,因为从 connect 的功能实现上看,两者的实现效果完全不同。
让我们先从简单的 inet_dgram_connect 入手。
int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
int addr_len, int flags)
{
struct sock *sk = sock->sk;
/* 长度合法性检查 */
if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL;
/* 如果协议族为 AF_UNSPEC,则先执行 disconnect */
if (uaddr->sa_family == AF_UNSPEC)
return sk->sk_prot->disconnect(sk, flags);
/* 如果该套接字没有指定源端口,并且系统自动绑定端口失败,则返回错误 */
if (!inet_sk(sk)->inet_num && inet_autobind(sk))
return -EAGAIN;
/* 调用具体协议的 connect 实现函数 */
return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}udp_prot 是 UDP 协议中所有自定义操作函数的集合。其 connect 的实现函数为 ip4_datagram_connect。
int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct inet_sock *inet = inet_sk(sk);
struct sockaddr_in *usin = (struct sockaddr_in *) uaddr;
struct flowi4 *fl4;
struct rtable *rt;
int oif;
__be32 saddr;
int err;
if (addr_len < sizeof(*usin))
return -EINVAL;
/* 检查是否为 AF_INET 协议族 */
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
/* 因为 connect 会改变目的地址,所有 socket 中保存的路由缓存已经无用,必须重置. */
sk_dst_reset(sk);
lock_sock(sk);
/* 得到套接字绑定的发送接口 */
oif = sk->sk_bound_dev_if;
saddr = inet->inet_saddr;
/* 在目的地址是多播地址的情况下,
如果该套接字没有绑定网卡,则出口网卡为设置的多播网卡索引;
如果该套接字没有绑定源 IP,则使用设置的多播源地址; */
if (ipv4_is_multicast(usin->sin_addr.s_addr)) {
if (!oif)
oif = inet->mc_index;
if (!saddr)
saddr = inet->mc_addr;
}
/* 执行路由查找 */
fl4 = &inet->cork.fl.u.ip4;
rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,
RT_CONN_FLAGS(sk), oif,
sk->sk_protocol,
inet->inet_sport, usin->sin_port, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
goto out;
}
/* 如果是广播地址且未设置广播标志,则返回错误 */
if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {
ip_rt_put(rt);
err = -EACCES;
goto out;
}
/* 如果套接字没有设置发送地址或接收地址,则使用路由结果 */
if (!inet->inet_saddr)
inet->inet_saddr = fl4->saddr; /* Update source address */
if (!inet->inet_rcv_saddr) {
inet->inet_rcv_saddr = fl4->saddr;
if (sk->sk_prot->rehash)
sk->sk_prot->rehash(sk);
}
inet->inet_daddr = fl4->daddr;
inet->inet_dport = usin->sin_port;
sk->sk_state = TCP_ESTABLISHED;
inet->inet_id = jiffies;
/* 重新设置路由信息 */
sk_dst_set(sk, &rt->dst);
err = 0;
out:
release_sock(sk);
return err;
}由于功能比较简单,所以 UDP 的 connect 实现源码也一目了然,可以看到,只是设置了目的 IP、端口和路由信息。下面对比一下 TCP 的 connect 实现,其实现比 UDP 要复杂得多,代码如下:
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
/* 从 socket 结构获得 sock 结构,后者是内核真正用于管理网络层的套接字结构 */
struct sock *sk = sock->sk;
int err;
long timeo;
/* 地址长度检查 */
if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL;
lock_sock(sk);
/* 对 AF_UNSPEC 兼容性处理 */
if (uaddr->sa_family == AF_UNSPEC) {
err = sk->sk_prot->disconnect(sk, flags);
sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
goto out;
}
/* STREAM 协议是有连接状态的,所以需要对套接字进行状态检查 */
switch (sock->state) {
default:
err = -EINVAL;
goto out;
/* 若连接已经建立,则返回错误 */
case SS_CONNECTED:
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;
/* sock 的状态是未连接,但是套接字的 sk_state 却不是关闭状态,
此时无法进行连接 */
if (sk->sk_state != TCP_CLOSE)
goto out;
/* 既然需要产生连接,那么每种具体的协议肯定都有自己的实现.
所以这会调用具体协议的实现函数. */
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
goto out;
/* 将 sock 状态更改为正在连接中 */
sock->state = SS_CONNECTING;
/* Just entered SS_CONNECTING state; the only
* difference is that return value in non-blocking
* case is EINPROGRESS,表示正在连接中.
当 connect 为非阻塞时,就会返回这个错误
*/
err = -EINPROGRESS;
break;
}
/* 检查是否需要连接超时.若设置了非阻塞标志,则 timeo 为假.
若设置了阻塞标志,则 timeo 为真. */
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
/* 当前连接状态为正在连接的状态(即刚发送了 syn 或收到 syn) */
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
/* 如果没有超时标志或连接超时或失败,则返回 */
if (!timeo || !inet_wait_for_connect(sk, timeo))
goto out;
/* 判断是否由于信号导致等待连接退出 */
err = sock_intr_errno(timeo);
/* 如果有未处理的信号,则返回失败,表示信号中断了连接 */
if (signal_pending(current))
goto out;
}
/* 连接被关闭了.原因可能是对端 RST,超时等 */
if (sk->sk_state == TCP_CLOSE)
goto sock_error;
/* 至此,连接成功 */
sock->state = SS_CONNECTED;
err = 0;
out:
release_sock(sk);
return err;
sock_error:
err = sock_error(sk) ? : -ECONNABORTED;
sock->state = SS_UNCONNECTED;
if (sk->sk_prot->disconnect)
sk->sk_prot->disconnect(sk, 0);
goto out;
}接下来,就需要进入 TCP 协议自定义的 connect 函数 tcp_v4_connect 了,代码如下:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
int err;
__be32 saddr = 0, nexthop, daddr;
__be16 orig_sport, orig_dport;
/* 地址长度检查 */
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;
/* 协议族类型检查 */
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
/* 设置下一跳和目的地址 */
nexthop = daddr = usin->sin_addr.s_addr;
/* 获得 IP 选项 */
inet_opt = rcu_dereference_protected(inet->inet_opt,
sock_owned_by_user(sk));
/* 如果有严格源路由的 IP 选项 */
if (inet_opt && inet_opt->opt.srr) {
/* 若地址为 0,则返回错误 */
if (!daddr)
return -EINVAL;
/* 因为严格源路由的 IP 选项,所以下一跳要设置为选项中的第一跳地址 */
nexthop = inet_opt->opt.faddr;
}
/* 设置源端口和目的端口 */
orig_sport = inet->inet_sport;
orig_dport = usin->sin_port;
fl4 = &inet->cork.fl.u.ip4;
/* 查找路由 */
rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
IPPROTO_TCP,
orig_sport, orig_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
return err;
}
/* 如果路由结果是多播或广播地址,则返回错误 */
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}
/* 如果没有 IP 选项或没有设置严格路由,那么目的地址即为路由结果的目的地址 */
if (!inet_opt || !inet_opt->opt.srr)
daddr = fl4->daddr;
/* 如果没有设置源地址,则使用路由结果的源地址 */
if (!inet->inet_saddr)
inet->inet_saddr = fl4->saddr;
/* 套接字的接收地址即为源地址 */
inet->inet_rcv_saddr = inet->inet_saddr;
/* 若保存的 TCP 选项有时间戳,并且目的地址与要连接的地址不同,
则需要重置时间戳及相关变量 */
if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
/* Reset inherited state */
tp->rx_opt.ts_recent = 0;
tp->rx_opt.ts_recent_stamp = 0;
tp->write_seq = 0;
}
/* 如果系统启用了 TIME_WAIT 回收机制,并且当前没有时间戳信息,
则尝试从对端 peer 中获得时间戳信息 */
if (tcp_death_row.sysctl_tw_recycle &&
!tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr) {
struct inet_peer *peer = rt_get_peer(rt, fl4->daddr);
/* 如果找到对端 peer */
if (peer) {
inet_peer_refcheck(peer);
/* 如果对端保存的时间戳信息还没有过期 */
if ((u32)get_seconds() - peer->tcp_ts_stamp <= TCP_PAWS_MSL) {
/* 利用对端保存的时间戳信息初始化当前套接字的时间戳选项 */
tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp;
tp->rx_opt.ts_recent = peer->tcp_ts;
}
}
}
/* 设置目的端口和地址 */
inet->inet_dport = usin->sin_port;
inet->inet_daddr = daddr;
/* 设置 IP 头的选项长度 */
inet_csk(sk)->icsk_ext_hdr_len = 0;
if (inet_opt)
inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
/* 初始化 MSS */
tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
/* 设置 TCP 的状态为 SYN_SENT,即发送了 syn 包 */
tcp_set_state(sk, TCP_SYN_SENT);
/* 将套接字加入到 hash 表中,并分配源端口 */
err = inet_hash_connect(&tcp_death_row, sk);
if (err)
goto failure;
/* 检查源端口或目的端口是否发生了变化,如果发生了变化则重新查找路由 */
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
goto failure;
}
/* 设置 GSO 功能 */
sk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &rt->dst);
/* 如果没有设置初始的序列号,则根据双方地址,随机生成端口 */
if (!tp->write_seq)
tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
/* 设置 inet_id,该 ID 用于生成 IP 报文的 ID 值 */
inet->inet_id = tp->write_seq ^ jiffies;
/* 一切准备工作完毕,tcp_connect 生成 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->inet_dport = 0;
return err;
}下面来分析 tcp_connect,看看内核是如何至此,TCP的连接过程已经分析完毕,其中涉及的某些过程会在后面进行具体分析。
12.4 服务器端连接过程
12.4.1 listen的使用
服务器端用 listen 来监听端口,其原型为:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);其中的参数解释如下:
- 参数
int sockfd:成功创建的 TCP 套接字。 - 参数
int backlog:定义 TCP 未处理连接的队列长度。该队列虽然已经完成了三次握手,但服务器端还没有执行 accept 的连接。APUE 中说,backlog 只是一个提示,具体的数值实际上是由系统来决定的。后面会通过学习内核源码来确定这一点。
函数的返回值为 0,表示成功;-1 表示失败。
12.4.2 listen的源码分析
listen的源码入口位于socket.c,代码如下:
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
/* 从文件描述符得到socket结构 */
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
/* 得到系统设置的最大未处理连接队列长度 */
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
/* 如果用户指定的参数backlog大于系统最大值,则使用系统最大值 */
if ((unsigned)backlog > somaxconn)
backlog = somaxconn;
/* 进行安全性检查 */
err = security_socket_listen(sock, backlog);
/* 通过检查后,就调用指定协议族的listen实现函数 */
if (!err)
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}AF_INET协议族的listen实现函数为inet_listen,代码如下:
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
lock_sock(sk);
err = -EINVAL;
/* 如果套接字状态不是未连接 */
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
/* 得到之前的TCP连接状态 */
old_state = sk->sk_state;
/* 如果之前的状态不是关闭或监听,则返回错误 */
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
/*
经过前面的状态过滤,这里只可能是关闭或监听状态.
如果当前已经是监听状态了,那么我们只须改变backlog的值;
如果是关闭状态,则需要真正地启动监听操作.
*/
if (old_state != TCP_LISTEN) {
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
/* 更新backlog的值 */
sk->sk_max_ack_backlog = backlog;
err = 0;
out:
release_sock(sk);
return err;
}接下来进入inet_csk_listen_start,代码如下:
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
struct inet_sock *inet = inet_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
/* 为连接请求队列分配空间 */
int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
if (rc != 0)
return rc;
/* 初始化工作 */
sk->sk_max_ack_backlog = 0;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
/*
虽然这里是先将连接的状态设为了监听状态,看似有一个竞争时间窗口.但实际上只有在get_port
成功以后,该套接字才被加入到哈希表中—从系统的角度看,套接字加入到哈希表中后,才会真正
处于监听状态,可以接受连接请求了.因此实际上并没有竞争发生
*/
sk->sk_state = TCP_LISTEN;
/* 使用get_port进行端口绑定 */
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
/* 设置源端口 */
inet->inet_sport = htons(inet->inet_num);
/* 清除路由缓存 */
sk_dst_reset(sk);
/* 将套接字加入到哈希表中,这时才可以接受新连接 */
sk->sk_prot->hash(sk);
return 0;
}
/* 绑定端口失败,则设置连接未关闭状态 */
sk->sk_state = TCP_CLOSE;
/* 释放连接请求队列空间 */
__reqsk_queue_destroy(&icsk->icsk_accept_queue);
return -EADDRINUSE;
}现在服务器端已经处于监听状态,可以接收客户端的连接请求了。同时,通过源码跟踪,也可以发现在第二个参数不超过系统限制的最大值的情况下,内核已直接使用其值作为已连接队列的长度了。
12.4.3 accept的使用
accept用于从指定套接字的连接队列中取出第一个连接,并返回一个新的套接字用于与客户端进行通信,示例代码如下:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);其中的参数解释如下:
int sockfd:处于监听状态的套接字。struct sockaddr *addr:用于保存对端的地址信息。socklen_t *addrlen:是一个输入输出值。调用者将其初始化为addr缓存的大小,accept返回时,会将其设置为addr的大小。int flags:是新引入的系统调用accept4的标志位;目前支持SOCK_NONBLOCK和SOCK_CLOEXEC。
关于返回值,若执行成功,则返回一个非负的文件描述符;若失败则返回-1。
NOTE
若不关心对端地址信息,则可以将
addr和addrlen设置为NULL。
12.4.4 accept的源码分析
accept的源码入口位于文件socket.c,代码如下:
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen)
{
return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}进入sys_accept4,代码如下:
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags)
{
struct socket *sock, *newsock;
struct file *newfile;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
/* 保证设置的非阻塞标志SOCK_NONBLOCK与O_NONBLOCK相同 */
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
/* 通过文件描述符获得socket结构 */
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
err = -ENFILE;
/* 申请一个新的socket结构 */
newsock = sock_alloc();
if (!newsock)
goto out_put;
/* 新的socket的类型和操作函数与监听的socket一致 */
newsock->type = sock->type;
newsock->ops = sock->ops;
/*
这里必须增加该套接字模块的引用计数.这是因为这个套接字模块可能不是Linux内核内置的,
为了保证在套接字的使用过程中,该模块不会被意外卸载,所以,在创建套接字时,需要增加相应
的模块计数
*/
__module_get(newsock->ops->owner);
/* 为新的socket类型,申请一个新的文件描述符 */
newfd = sock_alloc_file(newsock, &newfile, flags);
if (unlikely(newfd < 0)) {
err = newfd;
sock_release(newsock);
goto out_put;
}
/* 对accept操作进行安全性检查 */
err = security_socket_accept(sock, newsock);
if (err)
goto out_fd;
/* 执行协议族的accept操作函数 */
err = sock->ops->accept(sock, newsock, sock->file->f_flags);
if (err < 0)
goto out_fd;
/* 用户想获得对端地址 */
if (upeer_sockaddr) {
/* 获得对端地址 */
if (newsock->ops->getname(newsock, (struct sockaddr *)&address,
&len, 2) < 0) {
err = -ECONNABORTED;
goto out_fd;
}
err = move_addr_to_user((struct sockaddr *)&address,
len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_fd;
}
/* 将文件描述符newfd和文件管理结构newfile安装到文件表中 */
fd_install(newfd, newfile);
/* 此时,已保证accept成功执行,将newfd赋给err,并在后面返回err */
err = newfd;
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
out_fd:
fput(newfile);
put_unused_fd(newfd);
goto out_put;
}对于AF_INET协议族,accept的实现函数为inet_accept,代码如下:
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
struct sock *sk1 = sock->sk;
int err = -EINVAL;
/* 调用具体协议的accept操作,并得到新的sock结构 */
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
if (!sk2)
goto do_err;
/* 锁住新的sk2 */
lock_sock(sk2);
/* 记录RFS信息 */
sock_rps_record_flow(sk2);
WARN_ON(!((1 << sk2->sk_state) &
(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE)));
/* 将新的sock与调用者传递的socket关联起来 */
sock_graft(sk2, newsock);
/* 设置socket为连接状态 */
newsock->state = SS_CONNECTED;
err = 0;
/* 释放sk2的控制权 */
release_sock(sk2);
do_err:
return err;
}对于TCP协议来说,其accept实现函数如下:
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sock *newsk;
int error;
/* 获得sk的控制权 */
lock_sock(sk);
error = -EINVAL;
/* sk,即TCP连接,若不是监听状态则报错 */
if (sk->sk_state != TCP_LISTEN)
goto out_err;
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
/* 已连接队列为空 */
/* 得到sk的超时时间 */
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
/* 如果超时为0,即非阻塞,则报错退出 */
if (!timeo)
goto out_err;
/* 以timeo为超时时间,等待一个新的连接 */
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
/* 得到新的sock */
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
WARN_ON(newsk->sk_state == TCP_SYN_RECV);
out:
/* 释放sk的控制权,并返回新建连接newsk */
release_sock(sk);
return newsk;
out_err:
newsk = NULL;
*err = error;
goto out;
}12.5 TCP三次握手的实现分析
前面两节分别从客户端和服务器端的系统调用的角度,来分析和学习TCP的连接过程。本节将从TCP三次握手的数据包交互过程,来研究TCP连接的建立。如果不熟悉TCP握手的三个数据包,则请自行阅读相关材料。
三次握手的过程如图12-1所示。

12.5.1 SYN包的发送
SYN包是指客户端主动建立一个TCP新连接的第一个包,其TCP标志为SYN,表示同步TCP的序列号。SYN包的发送是在tcp_connect函数中完成的,下面对SYN包的构建做进一步分析。在tcp_connect函数中,通过调用tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN)来完成SYN包的构建,示例代码如下:
static void tcp_init_nondata_skb(struct sk_buff *skb, u32 seq, u8 flags)
{
/* 设置为CHECKSUM_PARTIAL,表示需要计算TCP校验和 */
skb->ip_summed = CHECKSUM_PARTIAL;
/* 初始化校验和信息 */
skb->csum = 0;
/* TCP_SKB_CB是一个宏,用于将skb->cb转换为TCP的控制块 */
/* 设置TCP首部标志位 */
TCP_SKB_CB(skb)->tcp_flags = flags;
/* 重置控制块的SACK标志位 */
TCP_SKB_CB(skb)->sacked = 0;
/* 初始化skb的GSO */
skb_shinfo(skb)->gso_segs = 1;
skb_shinfo(skb)->gso_size = 0;
skb_shinfo(skb)->gso_type = 0;
/* 设置TCP的序列号 */
TCP_SKB_CB(skb)->seq = seq;
/* 如果是SYN或FIN包,则增加TCP序列号 */
if (flags & (TCPHDR_SYN | TCPHDR_FIN))
seq++;
/* 设置结束序列号 */
TCP_SKB_CB(skb)->end_seq = seq;
}从源码中可以发现,这个函数只是设置TCP控制块的序列号和标志,并没有真正构建TCP数据包。那么,让我们回过头来看tcp_connect,但是在tcp_init_nondata_skb和tcp_transmit_skb之间再没有任何与构建数据包相关的代码了。那么也只剩下一个可能,即在tcp_transmit_skb中实现数据包的构建,这样也合乎道理。tcp_transmit_skb作为TCP发送函数的入口,统一实现了TCP数据包的构建。
下面是其中用于构造TCP数据包的相关代码:
/* 为TCP报文头部保存空间 */
skb_push(skb, tcp_header_size);
/* 重置TCP报文头指针 */
skb_reset_transport_header(skb);
/* 设置skb的所有者为sk,同时增加sk的写缓存的使用统计 */
skb_set_owner_w(skb, sk);
/* 得到TCP报文头的内存指针,开始构建TCP的报文头部 */
/* 这里设置了源端口、目的端口、序列号、确认序列号、包长及标志位 */
th = tcp_hdr(skb);
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(tp->rcv_nxt);
*((__be32 *)th) = htonl((tcb->tcp_flags & 0x0F) << 12); /* 设置标志位 */
/* SYN包,则TCP窗口不会被扩展 */
if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
/* RFC1323: The window in SYN & SYN/ACK segments
* is never scaled.
*/
th->window = htons(min(tp->rcv_wnd, 65535U));
}
/* TCP的校验和与紧急指针为0 */
th->check = 0;
th->urg_ptr = 0;
/* 检查是否需要设置紧急指针 */
if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
if (before(tp->snd_up, tcb->seq + 0x10000)) {
th->urg_ptr th->urg_ptr = htons(tp->snd_up - tcb->seq);
th->urg = 1;
} else {
th->urg = 0;
}
/* TCP选项部分 */
tcp_options_write((__be32 *)(th + 1), tp, &opts);
/* 计算TCP的校验和 */
icsk->icsk_af_ops->send_check(sk, skb);
/* 完成了TCP数据包的构建,将数据包交给IP层 */
err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);从上面的代码中,我们可以进一步领会Linux内核协议栈的数据包传输机制——每一层都专注于自己的工作。对于TCP传输层来说,只须负责在skb中构建自己的首部,然后将skb数据包传递给IP层做进一步的处理即可。
12.5.2 接收SYN包,发送SYN+ACK包
为了跟踪SYN包的接收流程,首先进入内核并接收发给本机数据包的入口 ip_local_deliver_finish,代码如下:
static int ip_local_deliver_finish(struct sk_buff *skb)
{
/* 得到设备的网络空间 */
struct net *net = dev_net(skb->dev);
/* 取走网络层报文头部 */
__skb_pull(skb, ip_hdrlen(skb));
/* 重置传输层报文头部 */
skb_reset_transport_header(skb);
rcu_read_lock();
{
/* 得到传输层协议 */
int protocol = ip_hdr(skb)->protocol;
int hash, raw;
const struct net_protocol *ipprot;
resubmit:
/* 将数据包传递给对应的原始套接字 */
raw = raw_local_deliver(skb, protocol);
/* 得到协议表的桶索引 */
hash = protocol & (MAX_INET_PROTOS - 1);
/* 得到注册协议 */
ipprot = rcu_dereference(inet_protos[hash]);
if (ipprot != NULL) {
int ret;
if (!net_eq(net, &init_net) && !ipprot->netns_ok) {
/* xfrm策略检查 */
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb(skb);
goto out;
}
}
ret = ipprot->handler(skb);
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
IP_INC_STATS_BH(net, ...);
}
if (!raw) {
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
/* 没有任何已注册的网络协议可以处理这个数据包,因此回复 ICMP proto unreachable */
IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP...);
}
}
}
}
}TCP协议在系统初始化时,会将对应的处理函数注册到 inet_protos 上,接下来进入TCP的处理函数 tcp_v4_rcv 中,代码如下:
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
int ret;
struct net *net = dev_net(skb->dev);
/* 如果数据包类型不是发给本机的,drop掉 */
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* Count it even if it's bad */
TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);
/* 数据包至少还有一个TCP报文头部长度 */
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
/* 得到TCP报文头部 */
th = tcp_hdr(skb);
/* 检查TCP的数据偏移,至少要比头部大 */
if (th->doff < sizeof(struct tcphdr) / 4)
goto bad_packet;
/* 检查数据段长度 */
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
/* 计算校验和 */
if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
goto bad_packet;
/* 重新得到TCP头部.因为前面的代码可能会重新申请skb. */
th = tcp_hdr(skb);
/* 得到IP头部 */
iph = ip_hdr(skb);
/* 设置TCP控制块的序列号,结束序列号,确认序列号等 */
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
/* 查找对应的sock结构.
这里先对已经连接的sock进行查找,然后对监听的sock进行查找 */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
process:
/* 如果sock处于TIME_WAIT状态,则跳转到do_time_wait */
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
/* 如果数据包的TTL小于设置的TTL阀值,则丢弃 */
if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
NET_INC_STATS_BH(net, LINUX_MIB_TCPMINTTLDROP);
goto discard_and_relse;
}
/* xfrm策略失败 */
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
/* 虽然函数的名字为reset重置,但实际上是释放了netfilter的相关资源 */
nf_reset(skb);
/* 执行socket过滤器 */
if (sk_filter(sk, skb))
goto discard_and_relse;
/* 重置数据包的网卡信息 */
skb->dev = NULL;
/* 锁住sock,获得控制权 */
bh_lock_sock_nested(sk);
ret = 0;
if (!sock_owned_by_user(sk)) {
/* 如果用户进程没有再使用这个sock */
/* 由DMA来做数据包拷贝,实现TCP receive offload */
#ifdef CONFIG_NET_DMA
struct tcp_sock *tp = tcp_sk(sk);
if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
tp->ucopy.dma_chan = dma_find_channel(...);
#endif
/* 将数据包放入prequeue中 */
if (!tcp_prequeue(sk, skb)) {
/* 如果放到prequeue中失败,则只能即时处理该数据包 */
ret = tcp_v4_do_rcv(sk, skb);
}
} else if (unlikely(sk_add_backlog(sk, skb))) {
/* else if的时候,意味着用户进程正在使用这个套接字.
那么就把数据包保存到backlog中. */
bh_unlock_sock(sk);
NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
goto discard_and_relse;
}
bh_unlock_sock(sk);
sock_put(sk);
return ret;
no_tcp_socket:
/* 若没有找到对应的TCP套接字,并且xfrm策略检测失败,则丢弃数据包 */
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
/* 检查是否数据包长度出错,或者是校验和出错 */
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
} else {
/* 若数据包未出错,但也没有匹配的套接字,则发送TCP RESET */
tcp_v4_send_reset(NULL, skb);
}
discard_it:
/* Discard frame. */
kfree_skb(skb);
return 0;
discard_and_relse:
sock_put(sk);
goto discard_it;
do_time_wait:
/* TIME_WAIT状态的数据包处理,在此省略了这些代码分析 */
...
}根据上面的分析可知,对于SYN包的处理,还需要进入函数 tcp_v4_do_rcv,代码如下:
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
#ifdef CONFIG_TCP_MD5SIG
/* 进行TCP的MD5检查,是TCP的一个安全检查 */
if (tcp_v4_inbound_md5_hash(sk, skb))
goto discard;
#endif
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
/* 处理已连接的数据包流程 */
/* 如果数据包与套接字的rxhash不同,则重置套接字的rxhash */
sock_rps_save_rxhash(sk, skb);
/* 进入已连接TCP的处理函数 */
if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
return 0;
}
/* 运行到这里,则表明该TCP为非连接状态 */
/* 检查数据包长度和TCP校验值 */
if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb))
goto csum_err;
if (sk->sk_state == TCP_LISTEN) {
/* 连接处于监听状态,这是我们要跟踪的流程 */
/*
* 处理连接请求,即处理SYN包.作为第一个SYN包请求,服务端只有
* 一个监听sock,所以这里返回的nsk实际上就是sk.
* 这里就不跟踪tcp_v4_hnd_req了.
*/
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) {
/* 为SYN请求生成了新的sock结构,自然需要重新做RFS hash */
sock_rps_save_rxhash(nsk, skb);
/* 处理子sock */
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
}
/* 根据状态处理数据包 */
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
/* 省略其余的不相干的代码 */
...
}进入 tcp_rcv_state_process,我们截取部分相关的代码:
switch (sk->sk_state) {
/* sock处于监听状态 */
case TCP_LISTEN:
/* 监听状态不应收到ack包 */
if (th->ack)
return 1;
/* 丢弃RST数据包 */
if (th->rst)
goto discard;
/* 收到SYN请求包 */
if (th->syn) {
/* 若设置了FIN结束标志,则丢弃包 */
if (th->fin)
goto discard;
/* 调用对应的处理连接请求的回调函数 */
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
/* SYN包处理完毕,释放数据包 */
kfree_skb(skb);
return 0;
}
}对于IPv4的TCP来说,处理连接请求的函数是 tcp_v4_conn_request,代码如下:
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
struct tcp_extend_values tmp_ext;
struct tcp_options_received tmp_opt;
const u8 *hash_location;
struct tcp_sock *tp = tcp_sk(sk);
struct request_sock *req;
struct inet_request_sock *ireq;
struct dst_entry *dst = NULL;
__be32 saddr = ip_hdr(skb)->saddr;
__be32 daddr = ip_hdr(skb)->daddr;
__u32 isn = TCP_SKB_CB(skb)->when;
bool want_cookie = false;
/* 处理广播或多播包,直接丢弃 */
if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
goto drop;
/* TW buckets are converted to open requests without limitations, they consume only memory */
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
/* 判断是否使用syn cookie,如不使用则丢弃该包 */
want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
if (!want_cookie)
goto drop;
}
/* backlog队列已满,并且队列中已有足够多的最近未处理的连接请求.则丢弃该包 */
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
/* 申请一个请求sock */
req = inet_reqsk_alloc(&tcp_request_sock_ops);
if (!req)
goto drop;
#ifdef CONFIG_TCP_MD5SIG
tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops;
#endif
/* 解析TCP选项 */
tcp_clear_options(&tmp_opt);
tmp_opt.mss_clamp = TCP_MSS_DEFAULT;
tmp_opt.user_mss = tp->rx_opt.user_mss;
tcp_parse_options(skb, &tmp_opt, &hash_location, 0);
/* 处理syn cookie */
if (tmp_opt.cookie_plus > 0 &&
tmp_opt.saw_tstamp &&
!tp->rx_opt.cookie_out_never &&
(sysctl_tcp_cookie_size > 0 ||
(tp->cookie_va ...))) {
/* 具体情况待补充 */
}
/* 如果需要syn cookie,但没有时间戳选项,则清除TCP选项 */
if (want_cookie && !tmp_opt.saw_tstamp)
tcp_clear_options(&tmp_opt);
/* 初始化request sock */
tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
tcp_openreq_init(req, &tmp_opt, skb);
ireq = inet_rsk(req);
ireq->loc_addr = daddr;
ireq->rmt_addr = saddr;
/* 如果不是syn cookie,或者有时间戳选项时,如果请求表示支持ECN,则服务端也设置ECN标志 */
if (!want_cookie || tmp_opt.tstamp_ok)
TCP_ECN_create_request(req, tcp_hdr(skb));
if (want_cookie) {
/* 若需要做syn cookie,则产生一个cookie序列号 */
isn = cookie_v4_init_sequence(sk, skb, &req->mss);
req->cookie_ts = tmp_opt.tstamp_ok;
} else if (!isn) {
struct inet_peer *peer = NULL;
/*
* 检查处于TimeWait状态的socket是否可以重用:
* 1)该socket支持时间```c
} else if (!isn) {
struct inet_peer *peer = NULL;
/*
* TimeWait状态的socket是否可以重用:
* 1)该socket支持时间戳选项。
* 2)打开了TimeWait状态socket重用开关。
* 3)通过查找路由,获得对端peer信息。
* 4)当前时间与对端的上个时间戳间隔小于TCP_PAWS_MSL(60)秒,并且新请求时间小于对端
* 上个时间戳TCP_PAWS_MSL(60)秒以上。
* 当同时满足上面几个条件时,则认为该请求为非法请求
*/
if (tmp_opt.saw_tstamp &&
tcp_death_row.sysctl_tw_recycle &&
(dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&
fl4.daddr == ...) /* 此处条件不完整,原文如此 */
/*
* 在没有syn cookie的情况下,内核对syn flood做的简单防护:
* 1)连接队列已经使用了四分之三以上。
* 2)没有对端信息或对端没有时间戳。
* 3)没有路由信息或没有路由的RTT时间。
* 当同时满足以上条件时,表明队列已接近满队列,同时这个新连接可能还无法正常通信,那么
* 就会放弃这个请求
*/
else if (!sysctl_tcp_syncookies &&
(sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(sysctl_max_syn_backlog >> 2))) {
/* 原文此处缺少闭合括号和跳转 */
goto drop;
}
isn = tcp_v4_init_sequence(skb);
}
/* 保存初始序列号和synack发送时间 */
tcp_rsk(req)->snt_isn = isn;
tcp_rsk(req)->snt_synack = tcp_time_stamp;
/* 回复SYN+ACK数据包 */
if (tcp_v4_send_synack(sk, dst, req,
(struct request_values *)&tmp_ext) ||
want_cookie)
goto drop_and_free;
/* 将request sock加入到哈希表中 */
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
return 0;
drop_and_release:
dst_release(dst);
drop_and_free:
reqsk_free(req);
drop:
return 0;
}这就是服务端收到SYN包,并回复SYN+ACK的过程.
12.5.3 接收SYN+ACK数据包
客户端接收SYN+ACK数据包的流程与服务器端类似,都要经过 ip_local_deliver_finish → tcp_v4_rcv → tcp_v4_do_rcv → tcp_rcv_state_process,这里会根据TCP的不同连接状态,进行不同的处理.对于此时的客户端来说,其连接状态为 TCP_SYN_SENT(即发送了SYN包),其代码如下:
case TCP_SYN_SENT:
/* 处理SYN+ACK,完成三次握手 */
queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
if (queued >= 0)
return queued;
/* Do step6 onward by hand. */
/* 处理urgent数据 */
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk);然后进入 tcp_rcv_synsent_state_process,代码如下:
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
const u8 *hash_location;
struct tcp_options_received tmp;
/* 解析TCP选项 */
tcp_parse_options(skb, &tp->rx_opt, &hash_location, 0);
/* 设置了ACK标志 */
if (th->ack) {
/* rfc793:
* "If the state is SYN-SENT then
* first check the ACK bit
* If the ACK bit is set
* If SEG.ACK号非法,即不等于我们下次要发的序列号,则重置连接
*/
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt)
goto reset_and_undo;
/* 判断TCP时间戳是否合法 */
if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
!between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
tcp_time_stamp)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSABANDONED);
goto reset_and_undo;
}
/* ACK标志已经通过了检查。这时,如果设置了Reset位,则重置连接 */
if (th->rst) {
tcp_reset(sk);
goto discard;
}
/* 如果没有设置SYN标志,则丢弃该包 */
if (!th->syn)
goto discard_and_undo;
/* 如果TCP套接字设置了ECN标志,但是数据包没有设置ECE标志
* (表示对端不支持TCP ECN显示拥塞通告),则清除掉本端的ECN标志)
*/
TCP_ECN_rcv_synack(tp, th);
/* 处理ack数据包,设置TCP发送窗口 */
tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
tcp_ack(sk, skb, FLAG_SLOWPATH);
/* Ok.. it's good. Set up sequence numbers and
* move to established.
*/
/* TCP窗口大小,是不考虑scale选项的 */
tp->snd_wnd = ntohs(th->window);
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
/* 如果没有windows scale选项 */
if (!tp->rx_opt.wscale_ok) {
/* 将接收端和接收端的窗口扩展选项设置为0 */
tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0;
/* 设置窗口的最大值 */
tp->window_clamp = min(tp->window_clamp, 65535U);
}
/* 判断是否时间戳选项 */
if (tp->rx_opt.saw_tstamp) {
/* 设置时间戳选项 */
tp->rx_opt.tstamp_ok = 1;
tp->tcp_header_len =
sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
}
/* 初始化 */
tcp_mtup_init(sk);
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
tcp_initialize_rcv_mss(sk);
/* Remember, tcp_poll() does not lock socket! */
tp->copied_seq = tp->rcv_nxt;
if (cvp != NULL &&
cvp->cookie_pair_size > 0 &&
tp->rx_opt.cookie_plus > 0) {
int cookie_size = ... /* 省略细节 */
}
tcp_set_state(sk, TCP_ESTABLISHED);
security_inet_conn_established(sk, skb);
/* Make sure socket is routed, for correct metrics. */
/* 查找路由 */
icsk->icsk_af_ops->rebuild_header(sk);
/* 下面对路由的metric、TCP的阻塞控制和缓存等进行初始化 */
tcp_init_metrics(sk);
tcp_init_congestion_control(sk);
/* Prevent spurious tcp_cwnd_restart() on first data packet. */
tp->lsndtime = tcp_time_stamp;
/* 若设置了keepalive,则初始化keepalive定时器 */
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
/* 若发送方没有设置窗口扩展选项,则设置TCP快速路径预测标志 */
if (!tp->rx_opt.snd_wscale)
__tcp_fast_path_on(tp, tp->snd_wnd);
else
tp->pred_flags = 0;
/* 如果sock的状态不是死亡状态 */
if (!sock_flag(sk, SOCK_DEAD)) {
/* 改变sock状态,唤醒等待进程 */
sk->sk_state_change(sk);
/* 若有异步等待队列,则给该进程发送异步事件 */
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
}
/*
* 如果该套接字:
* 1)有写操作等待。
* 2)设置了延迟accept。
* 3)没有设置快速ack。
*/
if (sk->sk_write_pending ||
icsk->icsk_accept_queue.rskq_defer_accept ||
icsk->icsk_ack.pingpong) {
/* 满足上面条件之一,则延时确认 */
/* Save one ACK. Data will be ready after
* several ticks, if write_pending is set.
*
* It may be deleted, but with this feature
* we avoid doing two opens when doing a connect.
*/
tcp_send_ack(sk);
}
return -1;
}
/* 到此,表示没有ACK标志 */
if (th->rst) {
/* 如果没有ACK只有RST,则丢弃该包 */
goto discard_and_undo;
}
/* 时间戳检测 */
if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp &&
tcp_paws_reject(&tp->rx_opt, 0))
goto discard_and_undo;
if (th->syn) {
/*
* 若只有SYN标志,没有ACK,则可能是同时发出了多个SYN连接请求,
* 甚至有可能是自己连接自己
*/
/* 设置连接状态为收到SYN包 */
tcp_set_state(sk, TCP_SYN_RECV);
/*
* 后面的代码,与之前收到SYN包的流程基本一致,在此就不做分析了
*/
if (tp->rx_opt.saw_tstamp) {
tp->rx_opt.tstamp_ok = 1;
tcp_store_ts_recent(tp);
tp->tcp_header_len =
sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
}
/* ... 省略后续相同处理 */
}
/* 没有SYN也没有RST标志,则丢掉数据包后返回 */
discard_and_undo:
/* 丢弃数据包 */
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
goto discard;
reset_and_undo:
/* 重置连接。与丢弃数据包的区别在于返回值。非0时,调用者会重置连接 */
tcp_clear_options(&tp->rx_opt);
tp->rx_opt.mss_clamp = saved_clamp;
return 1;
}关键流程梳理
- 客户端在
TCP_SYN_SENT状态下收到 SYN+ACK 包后,进入tcp_rcv_synsent_state_process处理.- 首先检查 ACK 的合法性(序列号匹配、时间戳有效),然后处理 RST、SYN 标志.
- 若一切正常,会设置发送窗口、时间戳选项,更新路由与拥塞控制,最后将状态迁移至
TCP_ESTABLISHED.- 若只收到 SYN 而无 ACK,则会进入
TCP_SYN_RECV状态(类似同时打开的情况).- 若收到 RST 或不合法的包,则重置或丢弃连接.
至此,客户端完成了三次握手的最后一步,连接正式建立.
12.5.4 接收ACK数据包,完成三次握手
在前文中,我们已经知道了发往本机的TCP数据包会进入tcp_v4_do_rcv.但因为此时还未真正地完成三次握手,所以TCP仍然是未连接状态,自然就会再次进入函数tcp_v4_hnd_req了.
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
struct tcphdr *th = tcp_hdr(skb);
const struct iphdr *iph = ip_hdr(skb);
struct sock *nsk;
/*
* SYN请求时,已经将对应的request_sock加入到了队列中,
* 因此这次收到ACK答复时,是可以找到对应的request_sock的。
* 另外需要注意的是,这个函数还会有一个输出值prev,
* 其为返回值req前面的元素。之所以返回这个prev值,
* 是为了在后面的tcp_check_req函数中,移除req时,
* 不需要进行第二次查找。
*/
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
return ...
……
}下面进入tcp_check_req函数:
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct request_sock **prev)
{
struct tcp_options_received tmp_opt;
/*
* saw_tstamp,因为时间戳选项依赖于每个数据包
*/
tmp_opt.saw_tstamp = 0;
if (th->doff > (sizeof(struct tcphdr)>>2)) {
/* 若实际数据位置偏移量大于TCP固定报头长度,
则表明该报文一定包含了TCP选项 */
/* 解析TCP选项 */
tcp_parse_options(skb, &tmp_opt, &hash_location, 0);
/* 判断是否有时间戳选项 */
if (tmp_opt.saw_tstamp) {
/* 检查时间戳选项 */
tmp_opt.ts_recent = req->ts_recent;
/* We do not store true stamp, but it is not required,
* it can be estimated (approximately)
*/
}
}
/*
* 若为重复的SYN包,则回复SYN+ACK
*/
if (flg & TCP_FLAG_SYN) {
req->rsk_ops->rtx_syn_ack(sk, req, NULL);
return NULL;
}
/* 非法的ACK值 */
if ((flg & TCP_FLAG_ACK) &&
(TCP_SKB_CB(skb)->ack_seq !=
tcp_rsk(req)->snt_isn + 1 + tcp_s_data_size(tcp_sk(sk))))
return sk;
/* 时间戳检查失败,或者序列号不在窗口内 */
if (paws_reject ||
!tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
tcp_rsk(req)->rcv_isn + 1,
tcp_rsk(req)->rcv_isn + 1 + req->rcv_wnd)) {
/* 若需发送ACK确认 */
if (!(flg & TCP_FLAG_RST))
req->rsk_ops->send_ack(sk, skb, req);
/* 若时间戳检测失败,则增加相应的计数 */
if (paws_reject)
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
return NULL;
}
/* In sequence, PAWS is OK. */
/* 若数据包为有序数据 */
if (tmp_opt.saw_tstamp &&
!after(TCP_SKB_CB(skb)->seq, tcp_rsk(req)->rcv_isn + 1))
req->ts_recent = tmp_opt.rcv_tsval;
/* 若序列号在接收窗口之外,则去掉SYN标志 */
if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn) {
/* Truncate SYN, it is out of window starting
at tcp_rsk(req)->rcv_isn + 1. */
flg &= ~TCP_FLAG_SYN;
}
/* 检查SYN和RST标志,若都设置了,则将当前半连接从队列中清除 */
if (flg & (TCP_FLAG_RST|TCP_FLAG_SYN)) {
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_ATTEMPTFAILS);
goto embryonic_reset;
}
/* 若没有设置ACK,则丢弃该包 */
if (!(flg & TCP_FLAG_ACK))
return NULL;
/* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
/* 如果设置了延迟接收,则丢弃单独的ACK包 */
if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
inet_rsk(req)->acked = 1;
return NULL;
}
/* 记录收到SYN+ACK的时间 */
if (tmp_opt.saw_tstamp && tmp_opt.rcv_tsecr)
tcp_rsk(req)->snt_synack = tmp_opt.rcv_tsecr;
else if (req->retrans) /* don't take RTT sample if retrans && ~TS */
/* 不进行RTT采样 */
/* TCP的三次握手已经完成。使用syn_recv_sock创建真正的套接字 */
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
goto listen_overflow;
/* 新的套接字已经创建,因此原来的request sock可以从队列中删除 */
inet_csk_reqsk_queue_unlink(sk, req, prev);
inet_csk_reqsk_queue_removed(sk, req);
/* 将套接字加入到已连接的队列中 */
inet_csk_reqsk_queue_add(sk, req, child);
return child;
listen_overflow:
if (!sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = 1;
return NULL;
}
/* 否则,连接被重置(略) */
}这样,当tcp_check_req成功返回时,会返回一个新创建的sock结构.那么在tcp_v4_do_rcv中,就会进入tcp_child_process中.
int tcp_child_process(struct sock *parent, struct sock *child,
struct sk_buff *skb)
{
int ret = 0;
int state = child->sk_state;
/* 检查sock是否正在被用户进程使用 */
if (!sock_owned_by_user(child)) {
/* 用户进程没有占用sock的情况 */
ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb),
skb->len);
/* Wakeup parent, send SIGIO */
/* 唤醒阻塞在父sock的任务 */
if (state == TCP_SYN_RECV && child->sk_state != state)
parent->sk_data_ready(parent, 0);
} else {
/* 由于用户进程占用着sock,将数据包加入backlog,以后再处理 */
__sk_add_backlog(child, skb);
}
bh_unlock_sock(child);
sock_put(child);
return ret;
}这里我们考虑数据包被立刻处理的情况,即用户进程没有占用sock结构,那么这里数据还是会进入tcp_rcv_state_process的.根据前面的分析,tcp_rcv_state_process是根据套接字的状态来处理数据包的.而child是从父sock生成的,所以如果child的状态和父sock的状态一致,肯定是有问题的——因为父sock的状态是监听状态.那么child的状态是何时改变的呢?
让我们退回到创建child的函数tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone中,代码如下:
struct sock *inet_csk_clone(struct sock *sk, const struct request_sock *req,
const gfp_t priority)
{
/* 克隆一个新的sock结构 */
struct sock *newsk = sk_clone(sk, priority);
if (newsk != NULL) {
/* 开始克隆面向连接的sock信息 */
struct inet_connection_sock *newicsk = inet_csk(newsk);
/* 将新sock设置为TCP_SYN_RECV状态 */
newsk->sk_state = TCP_SYN_RECV;
newicsk->icsk_bind_hash = NULL;
/* 后面是复制其他变量的代码,在此省略掉 */
}
return newsk;
}这个函数的命名稍稍有些别扭,名字叫做clone(克隆),也就是说,所有的内容都应该保持一致.而这里在这个inet_csk_clone后,新sock的状态与父sock的状态并不一致.
下面来查看tcp_rcv_state_process处理TCP_SYN_RECV状态的代码:
case TCP_SYN_RECV:
/*
* acceptable是tcp_rcv_state_process在前面对ACK数据包进行的判断。
*/
if (acceptable) {
/* 这时已经完成了三次握手 */
/* 初始化用户态未读数据的序列号是我们期待接收的下一个序列号 */
tp->copied_seq = tp->rcv_nxt;
smp_mb();
/* 设置连接状态为已连接 */
tcp_set_state(sk, TCP_ESTABLISHED);
/*
* sk_state_change为一个回调函数,默认为sock_def_wakeup,
* 其会唤醒sleep在该socket的进程
*/
sk->sk_state_change(sk);
/*
* 若该sock有对应的用户态socket,则执行异步I/O通知
* 这里需要注意的是,对于我们目前的情况来说。
* 子sock是从监听sock clone而来的,其中sk_sleep和sk_socket都是NULL。
* 那么三次握手以后,阻塞在监听socket的进程是如何被唤醒的呢?
* tcp_child_process在调用tcp_rcv_state_process后,会检查sock状态是否发生了变
* 化。如果发生了变化,则会调用parent->sk_data_ready(parent, 0);
* 这样,就可以将事件通知到阻塞在监听sock的进程了。
*/
if (sk->sk_socket)
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
/* 初始化未确认回复的序列号 */
tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
/* 初始化发送窗口 */
tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
/* 如果有时间戳选项,则MSS需要减去时间戳所占的大小 */
if (tp->rx_opt.tstamp_ok)
tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
/* Make sure socket is routed, for correct metrics. */
icsk->icsk_af_ops->rebuild_header(sk);
tcp_init_metrics(sk);
tcp_init_congestion_control(sk);
/* Prevent spurious tcp_cwnd_restart() on first data packet. */
tp->lsnd = tcp_jiffies32;
/* ... 其他初始化 */
}目前为止,三次握手的源码分析已经结束了。其内部还有很多细节值得展开学习,但那就不是一两个章节所能完成的任务了。笔者只是抛砖引玉,给出一个脉络,关于剩下的细节大家可以自己通过阅读代码来完善。
Image Context:
- [Image 4389 on Page 599]
- [Image 3829 on Page 612]
- [Image 4494 on Page 616]