深入理解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;
}
posted @ 2019-12-25 21:10  Tsungcheng  阅读(242)  评论(0编辑  收藏  举报