首页 > 其他 > 详细

深入理解TCP协议及其源代码

时间:2019-12-26 16:53:24      阅读:66      评论:0      收藏:0      [点我收藏+]

  在上一次实验中,我们已经探究了SOCKET底层API的具体功能以及具体调用过程,并且简单分析了replyhi/hello这个通讯过程,并且我们已经分析得出,这个过程是一个基于TCP协议的通信过程,在这篇博文中我们将具体分析一下TCP协议以及相关源码。在下文中我将侧重分析connect及bind、listen、accept背后的三次握手。

        首先,我们需要知道TCP在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层,在第二层上的数据,我们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。我们程序的数据首先会打到TCP的Segment中,然后TCP的Segment会打到IP的Packet中,然后再打到以太网Ethernet的Frame中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。

技术分享图片

  • Sequence Number:是包的序号,用来解决网络包乱序(reordering)问题。
  • Acknowledgement Number:就是ACK——用于确认收到,用来解决不丢包的问题。
  • Window:又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。
  • TCP Flag :也就是包的类型,主要是用于操控TCP的状态机的。

tcp协议有三次握手和四次挥手

技术分享图片

 

  所谓三次握手(Three-Way Handshake)即建立TCP连接,是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

 

     TCP状态转换图:

技术分享图片

      结合底层API我们分析出客户端和服务器端的连接过程如图所示:

技术分享图片

  TCP协议的双方分为主动打开和被动打开,从三次握手的角度讲,主动发起握手的一方属于主动打开;被动接受握手的一方属于被动打开。客户端属于主动打开,服务器端属于被动打开。API分为两类,一类客户端和服务器端都可以调用;另一类API独属于客户端或者服务器端。

客户端和服务器端都可以调用的API:socket(), bind(), send/write(),write()/recv(),close()

独属于客户端和服务器端的API:客户端:connect(),服务器端:listen(),accept()

 

TCP/IP协议的初始化函数为inet_inet,由fs_initcall(inet_init); 在系统启动时,自动调用。

技术分享图片
static int __init inet_init(void)
{
    struct sk_buff *dummy_skb;
    struct inet_protosw *q;
    struct list_head *r;
    int rc = -EINVAL;

    BUILD_BUG_ON(sizeof(struct inet_skb_parm) > sizeof(dummy_skb->cb));
     /* 申请reserved ports的bitmap */
    sysctl_local_reserved_ports = kzalloc(65536 / 8, GFP_KERNEL);
    if (!sysctl_local_reserved_ports)
        goto out;
     /*
     注册TCP,UDP和RAW协议 
     */
    rc = proto_register(&tcp_prot, 1);
    if (rc)
        goto out_free_reserved_ports;

    rc = proto_register(&udp_prot, 1);
    if (rc)
        goto out_unregister_tcp_proto;

    rc = proto_register(&raw_prot, 1);
    if (rc)
        goto out_unregister_udp_proto;

    /*
     *    Tell SOCKET that we are alive...
     */
     /* 注册Inet familiy*/
    (void)sock_register(&inet_family_ops);

#ifdef CONFIG_SYSCTL
    ip_static_sysctl_init();
#endif

    /*
        Add all the base protocols.
     */
     /*
     添加协议:ICMP,UDP,TCP和IGMP。
     从这里就可以看出,内核中支持的TCP/IP协议种类
     */
    if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST
    if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
        printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif

    /* Register the socket-side information for inet_create. */
    /* 初始化inetsw */
    for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
        INIT_LIST_HEAD(r);
     /* 
     将inetsw_array中的协议挂载到inetsw上 
     */
    for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);

     /*
     下面是各个Inet模块的初始化。
     */
    /*
      Set the ARP module up
     */

    arp_init();

    /*
       Set the IP module up
     */

    ip_init();

    tcp_v4_init();

    /* Setup TCP slab cache for open requests. */
    tcp_init();

    /* Setup UDP memory threshold */
    udp_init();

    /* Add UDP-Lite (RFC 3828) */
    udplite4_register();

    /*
       Set the ICMP layer up
     */

    if (icmp_init() < 0)
        panic("Failed to create the ICMP control socket.\n");

    /*
       Initialise the multicast router
     */
#if defined(CONFIG_IP_MROUTE)
    if (ip_mr_init())
        printk(KERN_CRIT "inet_init: Cannot init ipv4 mroute\n");
#endif
    /*
        Initialise per-cpu ipv4 mibs
     */
    if (init_ipv4_mibs())
        printk(KERN_CRIT "inet_init: Cannot init ipv4 mibs\n");

    ipv4_proc_init();

    ipfrag_init();
 
     /*
     这个将IP协议注册L2层
     */
    dev_add_pack(&ip_packet_type);

    rc = 0;
out:
    return rc;
out_unregister_udp_proto:
    proto_unregister(&udp_prot);
out_unregister_tcp_proto:
    proto_unregister(&tcp_prot);
out_free_reserved_ports:
    kfree(sysctl_local_reserved_ports);
    goto out;
}

 

实验过程

技术分享图片

 

 然后根据连接过程的所用到的系统调用函数进行设置断点:

技术分享图片

 

 

然后根据提示找到socket函数,connect函数,bind函数,listen函数和accept函数。

技术分享图片
int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    /* Check the SOCK_* constants for consistency.  */
    BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

    flags = type & ~SOCK_TYPE_MASK;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;
    type &= SOCK_TYPE_MASK;

    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        return retval;

    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    return __sys_socket(family, type, protocol);
}
技术分享图片

socket

  socket函数是一种可用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数,函数原型为int socket(int domain,int type, int protocol)。返回值:非负描述符 – 成功,-1 - 出错。

  其中:family指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等;type是套接口类型,主要SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW(原始socket);protocol一般取为0。成功时,返回一个小的非负整数值,与文件描述符类似。

  在我们截取的过程当中,包括两个socket函数的调用,其实也很好理解,分别是客户端和服务端的socket初始化。因为在TCP连接的过程中,实际上是套接字之间建立联系的过程,在连接建立之前,我们首先要先将套接字初始化完成,以方便后面进行连接。

 

技术分享图片
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;
 
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;
    err = move_addr_to_kernel(uservaddr, addrlen, &address);
    if (err < 0)
        goto out_put;
 
    err =
        security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
    if (err)
        goto out_put;
 
    err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                 sock->file->f_flags);
out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
}
 
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
        int, addrlen)
{
    return __sys_connect(fd, uservaddr, addrlen);
}
技术分享图片

connect

  connect()的调用格式如下:int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);

  参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。

  由于地址簇总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议簇相关。因此bind()和connect()无须协议作为参数。

 

技术分享图片
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (!err) {
            err = security_socket_bind(sock,
                           (struct sockaddr *)&address,
                           addrlen);
            if (!err)
                err = sock->ops->bind(sock,
                              (struct sockaddr *)
                              &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
    }
    return err;
}

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    return __sys_bind(fd, umyaddr, addrlen);
}
技术分享图片

bind

  bind函数原型为int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen)。其返回值:0 – 成功,-1 - 出错。

       当socket函数返回一个描述符时,只是存在于其协议族的空间中,并没有分配一个具体的协议地址(这里指IPv4/IPv6和端口号的组合),bind函数可以将一组固定的地址绑定到sockfd上。

  此时再看我们之前的流程图,bind函数是服务端完成的操作,该过程用于将IP端口绑定到套接字上,也就是我们后面显示的通信IP地址。

 

技术分享图片
int __sys_listen(int fd, int backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn;

        err = security_socket_listen(sock, backlog);
        if (!err)
            err = sock->ops->listen(sock, backlog);

        fput_light(sock->file, fput_needed);
    }
    return err;
}

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    return __sys_listen(fd, backlog);
}
技术分享图片

listen 

  此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:int PASCAL FAR listen(SOCKET s, int backlog);

  参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。

  此步骤也为服务端进行的操作,主要用于监听来自客户端的连接请求。

技术分享图片
int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
          int __user *upeer_addrlen, int flags)
{
    struct socket *sock, *newsock;
    struct file *newfile;
    int err, len, newfd, fput_needed;
    struct sockaddr_storage address;

    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;

    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;

    err = -ENFILE;
    newsock = sock_alloc();
    if (!newsock)
        goto out_put;

    newsock->type = sock->type;
    newsock->ops = sock->ops;

    /*
     * We don‘t need try_module_get here, as the listening socket (sock)
     * has the protocol module (sock->ops->owner) held.
     */
    __module_get(newsock->ops->owner);

    newfd = get_unused_fd_flags(flags);
    if (unlikely(newfd < 0)) {
        err = newfd;
        sock_release(newsock);
        goto out_put;
    }
    newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
    if (IS_ERR(newfile)) {
        err = PTR_ERR(newfile);
        put_unused_fd(newfd);
        goto out_put;
    }

    err = security_socket_accept(sock, newsock);
    if (err)
        goto out_fd;

    err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
    if (err < 0)
        goto out_fd;

    if (upeer_sockaddr) {
        len = newsock->ops->getname(newsock,
                    (struct sockaddr *)&address, 2);
        if (len < 0) {
            err = -ECONNABORTED;
            goto out_fd;
        }
        err = move_addr_to_user(&address,
                    len, upeer_sockaddr, upeer_addrlen);
        if (err < 0)
            goto out_fd;
    }

    /* File flags are not inherited via accept() unlike another OSes. */

    fd_install(newfd, newfile);
    err = newfd;

out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
out_fd:
    fput(newfile);
    put_unused_fd(newfd);
    goto out_put;
}

SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen, int, flags)
{
    return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);
}

SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen)
{
    return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}
技术分享图片

accept

  accept()的调用格式如下:SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);

  参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。

  accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。

 

      首先是服务器端的socket初始化,之后服务端进行bind进行端口绑定,并设置监听函数listen()监听来自客户端的连接请求,而客户端首先进行socket初始化,之后发出connect请求,connect阻塞;最后服务端同意连接之后执行accept()函数,此时accept阻塞,发回客户端回应信息之后,客户端的connect完成,发送回应信息给服务端,accept()执行完成,至此三次握手完成,开始进行双端之间的信息传递。

深入理解TCP协议及其源代码

原文:https://www.cnblogs.com/yongjason/p/12102072.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!