一提到TCP连接,会很自然的想到三次握手,没错,这几篇文章就主要讲的是在linux实现中的三次握手,只不过更注重于实现方法和设计结构。我们这里不会着重讲协议,虽然这与实现是密不可分的,有需要的话可以参考以下资料:

RFC 793:TCP最权威的定义,但理解起来可能会困难一些
http://www.tcpipguide.com/:国外一个牛人写的TCP/IP详解,很通俗易懂,涵盖了大部分协议
 
我们还是从上至下的看。入口是系统调用connect()
1.系统调用接口:sys_connect()[net/socket.c:1523]
获取fd对应的文件描述符,从用户空间中把sockaddr移到内核空间中,之后有一个security_socket_connect,我们不禁要问,这是做什么的?实际上在内核中的好多地方都有security_*的函数,我们之前的sys_socket()同样有一个security_socket_create(),这些都是内核中设置的一些"硬钩子",和linux的安全体系结构相关联,在这个安全体系结构下有若干可选的安全模块,有我们熟知的selinux,挺庞大的一套体系。好了,言归正传。
 
2.proto_ops接口:inet_stream_connect()[net/ipv4/af_inet.c:564]
如果不加说明,以后所说的都是TCP协议相关的接口,先把TCP看懂再说吧。
首先对该接口目前的状态做一个判断,因为毕竟只有在SS_UNCONNECTED的情况下才能进行连接操作。和我们所说的TCP连接的状态不同,这里所说的状态是BSD socket中的状态,只有四个:SS_CONNECTED,SS_CONNECTING,SS_UNCONNECTED,SS_DISCONNECTING。这里的连接操作就转到了下一层,我们现在先来看inet_stream_connect剩下的几个操作。
设置当前的发送超时时间(在阻塞调用的情况下),超时时间是从sk->sk_sndtimeo中获取的,这个值又是什么呢?返回socket初始化代码中找一下,很容易的就在sock_init_data()[net/core/sock.c:1694]中找到了对应的初始化代码,其值为LONG_MAX(0x7fffffff),也就是说如果对方没有响应的话connect操作会一直阻塞很长时间。
如果没有设置非阻塞操作的话,之后就会调用inet_wait_for_connect()来等待对方的相应。
 
3.proto接口:tcp_v4_connect()[net/ipv4/tcp_ipv4.c:145]
该函数完成了三个任务,下面按先后顺序来说:
第一,确定目的地址和目的端口,并设置inet的5个数据成员:daddr,dport,saddr,sport,rcv_saddr,如果用户没有将该socket和指定的地址和端口绑定,那么对源地址和源端口进行分配,并对应修改路由模块中相应的信息。
这一过程中调用了三个接口:首先是ip_route_connect(),查询路由表,来看目的地址是否能够到达。获得到目的地址的路由表rtable,并根据路由表中的地址对inet->daddr进行初始化,如果socket没有和指定的地址绑定,则inet->saddr也根据路由表进行初始化。然后inet_hash_connect(),socket没有指定源端口,则从空闲端口(tcp_hashinfo[net/ipv4/tcp_ipv4.c:101])中找一个分配给它。最后ip_route_new_ports(),这个函数的功能拿不太准,应该是对路由表的修改,因为分配了新的端口给它。
下面说一下我的几个疑问:为什么要在TCP层查询路由表?而且还根据路由表的信息对inet->daddr进行初始化?inet->daddr不是由用户指定的么?有时间的话可以做几个实验来进行研究,模拟一个简单的网络环境。ip_route_connect()和ip_route_newports()似乎是路由子系统对L4暴露的唯一两个接口,路由是网络问题中最复杂也是最难解决的一个问题,linux的路由算法由单独的一个模块xfrm来完成,在以后再来单独的学习该模块吧。
 
第二,初始化TCP的Sequence Number。
因为TCP的Initial Sequence Number不能是固定的一个数(为了防止端口复用的一个bug,详见RFC0793 3.3节),所以TCP协议中指定这个数应该和系统时间相加,这样就在一个时间周期内不会出现Sequence Number相同的情况。
我们在来看linux的实现,位于secure_tcp_sequence_number()[drivers/char/random.c:1557],它首先用源端口,地址和目的端口,地址进行了一个half_md4运算,但是因为用来运算的因子key只有两个(通过get_keyptr()获得),所以该运算的结果只能是两种,在同样的源,目的地址端口的情况下,得到的结果基本相同,这样的Sequence Number是很容易出bug的。所以,按照TCP协议的要求,之后对该数加上了一个现在的系统时间,充分满足了对Sequence Number多样性的需求。
 
第三,调用tcp_connect(),进入到真正的三次握手的第一次阶段。
 
4.tcp_connect()[net/ipv4/tcp_output.c:2344]
从这里开始就正式进入了数据包发送的过程(发送第一个数据包:SYN),但在发送之前,要先把数据包准备好,这也是这个函数的主要工作。接下来我们来看实际的实现代码。
在准备数据包之前,还要进行一些初始化,因为之前的初始化过程大多和AF_INET协议族相关,此时我们要做的,就是初始化那些只和TCP协议相关的数据。那么只和TCP协议本身相关的是什么呢?那当然是滑动窗口协议,它的核心数据都是在这里初始化的,具体滑动窗口协议的实现在单独的一章里再讲吧。另外,在此处还初始化了一个重要的数据就是mss(Maximum Segment Size),当然mss的值还要和连接的对方进行协商,在收到ACK后还是要修改的。
至此,繁琐的初始化工作终于可以告一段落了,接下来即将进入激动人心的数据包发送过程。网络数据包在linux中统一用sk_buff[include/linux/skbuff.h:262]来表示,linux的通用网络缓冲区的设计还是在单独的一个章节中介绍吧。现在我们要关注的是sk_buff的cb字段,它是预先分配好的一块内存区,作用好像一个"调色板",各层协议都可以使用,且使用过后数据即无效。在TCP这一层中它被看作是struct tcp_skb_cb[include/net/tcp.h:558],在TCP层发送数据包前都是对这个"调色板"进行初始化,然后统一由tcp_transmit_skb()根据这个"调色板"来构造TCP包头,这样就统一了TCP层的数据包发送接口,结构看上去也更简洁。
构造好了sk_buff,将其加入到写队列(sk->sk_write_queue),调用tcp_transmit_skb()就将该数据包发送了出去,剩下的就是一些收尾工作,重新设置Retransmit计时器,以防对方没有收到,在计时器超时之后重新发送该SYN数据包。
 
5.tcp_transmit_skb()[net/ipv4/tcp_output.c:595]
在TCP协议中,这是一个通用的接口,所有需要发送的数据包都只能经过该接口,不论SYN,ACK还是PSH,RST。
它的功能也很简单:根据目前sock(包括sk,inet,icsk,tp)和skb->cb来初始化tcp包头,之后用icsk->icsk_af_ops->queue_xmit()将该skb发送出去,icsk->icsk_af_ops是在socket创建时由proto层的接口初始化的,回到初始化代码中很容易就能找到,这一套接口(ipv4_specific[net/ipv4/tcp_ipv4.c:1728])是TCP对下层的接口,要和IP层进行交互的很多地方都能找到它的身影。
 
到这里,对于TCP来说,请求连接的SYN数据包就算是已经发送出去了,TCP连接的状态也变成了SYN_SENT,下面该IP上场,但目前我们暂时只看到这里,一口吃不成胖子,慢慢来吧。
目前还有两个未解决的问题:内核网络套接字缓冲区sk_buff的设计;内核TCP滑动窗口协议的数据结构设计。在我们继续进入到下一个状态SYN_RCVD之前,还是先把这两个东西搞懂吧,下面会先出这两个专题。