zoukankan      html  css  js  c++  java
  • 深入理解TCP协议及其源代码

            socket API与系统调用的关系在上篇文章中已经分析得很清楚https://www.cnblogs.com/wzzgeorge/p/12068455.html,那么本文主要深入TCP协议,分析connect及bind、listen、accept背后的三次握手机制。

    一、TCP概述

    1. TCP的概念

            传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(MTU)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

    2. 三次握手的概念

            握手过程中传送的包里不包含数据,三次握手完毕后,客户端不服务器才正式开始传送 数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主劢关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主劢发起断开TCP连接的请求。

    • 第一次握手:建立连接。客户端发送连接 请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端迚入 SYN_SEND状态,等待服务器的确认。即 A发送信息给B。
    • 第二次握手:服务器收到客户端的SYN报 文段,需要对这个SYN报文段迚行确认。 即B收到连接信息后向A返回确认信息。
    • 第三次握手:客户端收到服务器的 (SYN+ACK)报文段,并向服务器发送 ACK报文段。即A收到确认信息后再次向B 返回确认连接信。

     

    二、源代码分析

            客户端请求连接服务器时,经历的三次握手从用户程序的角度看就是客户端connect和服务端accept建立起连接时背后完成的工作,上一篇文章说到,在内核socket接口层这两个socket API函数对应着sys_connect和sys_accept函数:

    SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
    {
     ......
        switch (call) {
        case SYS_SOCKET:
            err = __sys_socket(a0, a1, a[2]);
            break;
        case SYS_BIND:
            err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
            break;
        case SYS_CONNECT:
            err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
            break;
        case SYS_LISTEN:
            err = __sys_listen(a0, a1);
            break;
        case SYS_ACCEPT:
            err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                        (int __user *)a[2], 0);
            break
    ......

          进一步查看每个子函数:

    int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
    {
        ......
        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,sock->file->f_flags);
        ......
    }
    
    int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,int __user *upeer_addrlen, int flags)
    {
        err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
    }
    
    int __sys_listen(int fd, int backlog)
    {
        err = sock->ops->listen(sock, backlog);//如果是TCP套接字,sock->ops指向的是inet_stream_ops,sock->ops是在inet_create()函数中初始化,所以listen接口调用的是inet_listen()函数。
    }
    
    int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
    {
        err = sock->ops->bind(sock,(struct sockaddr *)&address, addrlen);
    }
        在文件/net/ipv4/tcp_ipv4.c文件中的结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数,例如__sys_accept4,SOCK_STREAM套接口的TCP层操作函数集实例为tcp_prot,对应连接的接收函数为inet_csk_accept()。
    
    
    struct proto tcp_prot = {
        .name            = "TCP",
        .owner            = THIS_MODULE,
        .close            = tcp_close,
        .pre_connect        = tcp_v4_pre_connect,
        .connect        = tcp_v4_connect,
        .disconnect        = tcp_disconnect,
        .accept            = inet_csk_accept,
        .ioctl            = tcp_ioctl,
    .init = tcp_v4_init_sock, ...... }

    当创建了TCP协议套接字后,客户端再调用connect()函数请求建立TCP链接。该函数通过系统调用触发tcp_v4_connect函数的执行,产生一个包含SYN标志和一个32位的序号的连接请求包,并发送给服务器端。 这是TCP三次握手的第一步。
     int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
    
    {
             struct inet_opt *inet = inet_sk(sk);
    //变量inet指向套接字struct sock中的inet选项
             struct tcp_opt *tp = tcp_sk(sk);
    //变量tp指向套接字struct sock中的TCP选项
             struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
    //将通用地址结构转换为IPv4的地址结构
             struct rtable *rt;
    //记录路由表项
             u32 daddr, nexthop;
    //daddr记录目的地址,nesthop记录下一条地址
             int tmp;
             int err;
     
    //判断地址长度是否合法,应该要大于或者等于其地址的长度
             if (addr_len < sizeof(struct sockaddr_in))
                     return -EINVAL;
     //判断是否为inet协议族 
             if (usin->sin_family != AF_INET)
                     return -EAFNOSUPPORT;
    //初始化目的地址和下一条地址
             nexthop = daddr = usin->sin_addr.s_addr;
             if (inet->opt && inet->opt->srr) {
                     if (!daddr)
                             return -EINVAL;
                     nexthop = inet->opt->faddr;
             }
    //查找路由表项,并通过变量rt记录下来
             tmp = ip_route_connect(&rt, nexthop, inet->saddr,
                                    RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                                    IPPROTO_TCP,
                                    inet->sport, usin->sin_port, sk);
             if (tmp < 0)
                     return tmp;
     
             if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
                     ip_rt_put(rt);
                     return -ENETUNREACH;
             }
     
             if (!inet->opt || !inet->opt->srr)
                     daddr = rt->rt_dst;//如果源地址为0,则把rt_ src赋给源地址

    if (!inet->saddr) inet->saddr = rt->rt_src; inet->rcv_saddr = inet->saddr; //初始化TCP选项 if (tp->ts_recent_stamp && inet->daddr != daddr) { /* Reset inherited state */ tp->ts_recent = 0;//Time stamp to echo next tp->ts_recent_stamp = 0;//Time we stored ts_recent (for aging) tp->write_seq = 0;//序号初始化为0 } if (sysctl_tcp_tw_recycle && !tp->ts_recent_stamp && rt->rt_dst == daddr) { struct inet_peer *peer = rt_get_peer(rt); if (peer && peer->tcp_ts_stamp + TCP_PAWS_MSL >= xtime.tv_sec) { tp->ts_recent_stamp = peer->tcp_ts_stamp; tp->ts_recent = peer->tcp_ts; } } inet->dport = usin->sin_port;//目的端口地址 inet->daddr = daddr;//目的IP地址 tp->ext_header_len = 0; if (inet->opt) tp->ext_header_len = inet->opt->optlen;//inet的opt选项的长度 tp->mss_clamp = 536; //把套接字结构sk的状态置为TCP_SYN_SENT tcp_set_state(sk, TCP_SYN_SENT); //为套接字绑定一个端口,并记录在TCP的哈希表中 err = tcp_v4_hash_connect(sk); if (err) goto failure; err = ip_route_newports(&rt, inet->sport, inet->dport, sk); if (err) goto failure; /* OK, now commit destination to socket. */ //设置套接字的路由出口信息 __sk_dst_set(sk, &rt->u.dst); tcp_v4_setup_caps(sk, &rt->u.dst); tp->ext2_header_len = rt->u.dst.header_len; //生成一个序号 if (!tp->write_seq) tp->write_seq = secure_tcp_sequence_number(inet->saddr, inet->daddr, inet->sport, usin->sin_port); inet->id = tp->write_seq ^ jiffies; //调用tcp_connect(sk)函数,为请求包设置SYN标志,并发出请求包 err = tcp_connect(sk); rt = NULL;//rt指针指向内存0开始处 if (err) goto failure; return 0; failure: //如果连接失败,则需把套接字状态设置为:TCP_CLOSE /* 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;//sk_route_caps,网络驱动特征标志 inet->dport = 0; return err;
    }
        而另一头服务端调用inet_csk_accept函数会请求队列中取出一个连接请求,如果队列为空则通过inet_csk_wait_for_connect阻塞住等待客户端的连接。
    /**********************************************
    sk:监听sock
    flags:这些是文件标志, 例如 O_NONBLOCK
    err:传出参数 用于接收错误
    ******************************************************/
    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;
     
        //获取sock锁将sk->sk_lock.owned设置为1
        //此锁用于进程上下文和中断上下文
        lock_sock(sk);
     
        /* We need to make sure that this socket is listening,
         * and that it has something pending.
         */
        //用于accept的sock必须处于监听状态
        error = -EINVAL;
        if (sk->sk_state != TCP_LISTEN)
            goto out_err;
     
        /* Find already established connection */
        //在监听套接字上的连接队列如果为空
        if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
     
            //设置接收超时时间,若调用accept的时候设置了O_NONBLOCK,表示马上返回不阻塞进程
            long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
     
            /* If this is a non blocking socket don't sleep */
            error = -EAGAIN;
            if (!timeo)//如果是非阻塞模式timeo为0 则马上返回
                goto out_err;
     
            //将进程阻塞,等待连接的完成
            error = inet_csk_wait_for_connect(sk, timeo);
            if (error)//返回值为0说明监听套接字的完全建立连接队列不为空
                goto out_err;
        }
     
        //在监听套接字建立连接的队列中删除此request_sock连接项 并返回建立连接的sock
        //三次握手的完成是在tcp_v4_rcv中完成的
        newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
     
        //此时sock的状态应为TCP_ESTABLISHED
        WARN_ON(newsk->sk_state == TCP_SYN_RECV);
    out:
        release_sock(sk);
        return newsk;
    out_err:
        newsk = NULL;
        *err = error;
        goto out;
    }
  • 相关阅读:
    ORACLE PL/SQL 实例精解之第七章 迭代控制之二
    ORACLE PL/SQL 实例精解之第六章 迭代控制之一
    ORACLE PL/SQL 实例精解之第五章 条件控制:CASE语句
    ORACLE PL/SQL 实例精解之第四章 条件控制:if 语句
    sql中用JOIN USING 简化JOIN ON
    ORACLE PL/SQL 实例精解之第三章 PL/SQL中的SQL
    ORACLE PL/SQL 实例精解之第二章 通用编程语言基础
    删除文件时提示“找不到该项目”,怎么解决? 转摘自:http://jingyan.baidu.com/article/e4d08ffdf5ab470fd2f60df4.html
    C#获取文件夹/文件的大小以及占用空间 转摘自:http://www.cnblogs.com/chenpeng-dota/articles/2176470.html
    git update-index --assume-unchanged on directory 转摘自:http://stackoverflow.com/questions/12288212/git-update-index-assume-unchanged-on-directory
  • 原文地址:https://www.cnblogs.com/wzzgeorge/p/12098983.html
Copyright © 2011-2022 走看看