首页 » 技术分享 » TCP建连过程详解/160805

TCP建连过程详解/160805

 

jehol.liuyang


本文仅涉及对基本的建连过程的讨论,同时打开、建连失败处理等异常流程均不涉及,后期有时间会逐步完善;
另外,因时间仓促,加之能力有限,文章错误之处在所难免,敬请批评指正

TCP建连状态机

TCP建立过程就是相互发送信息,驱动客户端、服务器状态变化至于稳定可通信状态的过程,我们先给出状态变迁图,后面的论述都将围绕此展开

这里写图片描述
图1.TCP状态变迁图

接口层代码

我们日常应用TCP协议都是通过调用接口层的系统调用完成的,下面摘录了两段服务器和客户端建立TCP连接的代码,我们主要关注建立连接部分(其他部分被省略了),接下来将通过代码中的系统调用深入内核代码去看看TCP三次握手背后的逻辑

int main(int argc, char *argv[])  
{  
    int server_sockfd;//服务器端套接字  
    int client_sockfd;//客户端套接字  
    int len;  
    struct sockaddr_in my_addr;   //服务器网络地址结构体  
    struct sockaddr_in remote_addr; //客户端网络地址结构体  
    int sin_size;  
    char buf[BUFSIZ];  //数据传送的缓冲区  
    memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零  
    my_addr.sin_family=AF_INET; //设置为IP通信  
    my_addr.sin_addr.s_addr=INADDR_ANY;//服务器IP地址--允许连接到所有本地地址上  
    my_addr.sin_port=htons(8000); //服务器端口号
    /*创建服务器端套接字--IPv4协议,面向连接通信,TCP协议*/  
    if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0)  
    {    
        perror("socket");  
        return 1;  
    }
    /*将套接字绑定到服务器的网络地址上*/  
    if (bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0)  
    {  
        perror("bind");  
        return 1;  
    }  
    /*监听连接请求--监听队列长度为5*/  
    listen(server_sockfd,5); 
    sin_size=sizeof(struct sockaddr_in);  
    /*等待客户端连接请求到达*/  
    if((client_sockfd=accept(server_sockfd,(struct sockaddr *)&remote_addr,&sin_size))<0)  
    {  
        perror("accept");  
        return 1;  
    }  
    printf("accept client %s/n",inet_ntoa(remote_addr.sin_addr));  
    ...
}  

图2.服务器端代码

int main(int argc, char *argv[])  
{  
    int client_sockfd;  
    int len;  
    struct sockaddr_in remote_addr; //服务器端网络地址结构体  
    char buf[BUFSIZ];  //数据传送的缓冲区  
    memset(&remote_addr,0,sizeof(remote_addr)); //数据初始化--清零  
    remote_addr.sin_family=AF_INET; //设置为IP通信  
    remote_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器IP地址  
    remote_addr.sin_port=htons(8000); //服务器端口号  
    /*创建客户端套接字--IPv4协议,面向连接通信,TCP协议*/  
    if((client_sockfd=socket(PF_INET,SOCK_STREAM,0))<0)  
    {  
        perror("socket");  
        return 1;  
    }  
    /*将套接字绑定到服务器的网络地址上*/  
    if(connect(client_sockfd,(struct sockaddr *)&remote_addr,sizeof(struct sockaddr))<0)  
    {  
        perror("connect");  
        return 1;  
    }  
    printf("connected to server/n");  
    ...
} 

图3.客户端代码
上面两部分代码可以概括成下图

这里写图片描述
图4.系统调用流程图

结构体

在深入系统调用之前有必要让大家大致了解一下,建连所必须的结构体以及其层级关系。我把需要讲解的数据结构基本关系图附在下面,里面涉及了一些和建连无关的结构体和关系,但是考虑到我学习过程所遇到的困惑大多数人也会有,所以在这里一并消除一下

这里写图片描述
图5.内核数据结构基本关系

这个图的大致意思就是我们可以通过一个描述符找到一个socket,通过这个socket在inpcb链表里有个结构体,如果要是TCP连接则inpcb下面挂着一个tcpcb结构体;当发送信息时候会在路由表里找到下一跳的rtentry,在rtentry所标记的接口(以以太网为例)发送该信息,这样整个发送流程都通了。

以下仅说明一下和建连有关的结构体以及项
socket{}结构体
so_type标明服务的协议(tcp、udp、原始套接字等)
so_head指向调用了accept函数的socket
so_q0指向未完成三次握手的socket队列
so_q指向完成了三次握手的消息队列
so_qlimit为so_q0队列设置上限

这里写图片描述
图6.插口连接队列

inpcb{}结构体
存储建连四元组外部地址、外部接口、本端地址、本端接口,标明一个tcp连接

tcpcb{}结构体
t_state标明连接状态

系统调用

下面将根据接口层代码图4,结合TCP状态变迁图1,说明在建立连接时系统内核的主要逻辑

首先从服务器开始:

1、调用socket来创建一个插口,对于tcp协议,这不仅仅是申请socket结构体挂在file下,而是连同inpcb、tcpcb以及结构体的关系都准备好,置t_state为CLOSED
2、调用bind为inpcb设定本地地址、本地端口,这个时候对端的ip和端口还不知道,所以四元组为(0.0.0.0,0,laddr,lport),这里需要记住这个四元组,后面会作出说明
3、接下来调用listen函数,这个函数主要是设定so_qlimit值;另外,图1中的状态机在这里做一次跳动CLOSED——>LISTEN
4、接下来是accept函数的调用,这个函数是需要和tcp_input函数配合完成任务的。
4.1调用accept函数如果so_q0没有socket则调用sleep睡眠
4.2当收到客户发送的含有SYN报文后三次握手第一次握手,tcp_input调用sonewconn生成一个socket,置t_state为CLOSED,把报文中源端dst、源端port赋值在新生成socket关联的inpcb中的laddr、lport,置t_state为LISTEN;然后把其挂在睡眠socket的so_q0上,然后调用in_pcbconnect回复客户端SYN、ACK,并给inpcb设定faddr、fport,状态迁移LISTEN——>SYN_RECV三次握手第二次握手等待对端ACK;

到这里大家有什么疑问没有?有那么多的socket,程序是怎么把SYN报文调度到正确的socket上的,自然是依靠四元组;可是睡眠在accept的socket四元组为(0.0.0.0,0,laddr,lport);客户端发送来的报文的四元组为(x.x.x.x,y,laddr,lport)并不一样如何匹配?原来协议栈规定在寻找四元组配对时如果没有完整匹配0.0.0.0可以匹配任意的ip;0可以匹配任意的端口号

4.3当收到对端ACK时t_state从SYN_RECV——>ESTABLISHED,并且tcp_input调用soisconnected函数把socket从so_q0移到so_q并唤醒睡眠在accept的进程
4.4accept被唤醒后分配一个文件描述符把socket移除so_q,至此这个连接就正式建立了
卷二有一张图,形象的描述了上述过程;实线表示真的函数调用;虚线表示状态变迁

这里写图片描述
图7.处理进入的TCP连接

记得有人问过我LISTEN可不可以不需要,因为貌似在这个状态没有做什么重大的工作。
首先我们看哪里用到LISTEN这个状态,
第一个地方:执行accept时设定so_qlimit的时候
第二个地方:tcp_input收到对端的SYN时候
在卷二里是这么解释这个状态的通知协议进程准备接收插口上的连接请求
请大家也考虑一下LISTEN是不是可以不需要。

客户端:

刚才在讲服务器的时候都涉及到了客户端,所以这里仅需要简要的说明一下
1、调用socket来创建一个插口,对于tcp协议,这不仅仅是申请socket结构体挂在file下,而是连同inpcb、tcpcb以及结构体的关系都准备好,置t_state为CLOSED
2、调用bind为inpcb设定本地地址、本地端口
3、调用connect发送SYN报文三次握手第一次握手,t_state从CLOSED——>SYN_SEND,然后connect也进入sleep
4、收到对端回复的SYN、ACK报文三次握手第二次握手后,这里的inpcb四元组为(远端ip、远端端口(服务器的知名端口)、本端ip、本端端口)然后置t_state为ESTABLISH,回复ACK三次握手第三次握手,之后建连成功

参考文献

1、http://blog.csdn.net/piaojun_pj/article/details/5920888
2、TCP/IP详解 卷1:协议(第二版)
3、TCP/IP详解 卷2:实现
4、计算机网络系统方法(第五版)

转载自原文链接, 如需删除请联系管理员。

原文链接:TCP建连过程详解/160805,转载请注明来源!

0