从源码看tcp三次握手(上)


差一点

我们就擦肩而过了

有趣

有用

有态度



阅读本文需要对level-ip的整体架构有所了解,如果读者尚未接触过level-ip,请先阅读下面文章:

分享一款Linux平台下的tcp协议栈!超级透彻!

Linux系统中间件的巧妙实现--以用户空间的tcp协议栈为例

请根据上述文章中的指引获取leve-ip的全部源码,并且尝试在任意Linux发行版本上编译运行。

著名的TCP三次握手

前面已经介绍过常用套接字接口函数,也就是服务器调用bind、listen 以及 accept 等待客户端进行连接,而客户端connect函数主动请求连接服务器。

客户在调用函数 connect 前不必非得调用 bind 函数,因为如果需要的话,内核会确定源IP 地址,并按照一定的算法选择一个临时端口作为源端口。当客户端使用tcp套接字进行连接时,调用 connect 函数将激发 TCP 的三次握手过程。如下图:

TCP三次握手的剖析

这里我们使用的网络编程模型都是阻塞式的。所谓阻塞式,就是调用发起后不会直接返回,由操作系统内核处理之后才会返回。相对的,还有一种叫做非阻塞式的,暂时先不讨论。

下面是具体的过程:

  1. 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态;
  2. 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
  3. 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
  4. 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

从socket()函数来看CLOSE状态

前面的文章分析了linux系统中间件的实现思路,明白了应用程序中使用的socket()函数,实际上是调用了level-ip协议站中的ipc_socket()函数,该函数的核心是_socket()函数,我们来看一下这个函数,如下:

int _socket(pid_t pid, int domain, int type, int protocol){    struct socket *sock;    struct net_family *family;
if ((sock = alloc_socket(pid)) == NULL) { print_err("Could not alloc socket\n"); return -1; } ... family = families[domain];
if (!family) { print_err("Domain not supported: %d\n", domain); goto abort_socket; } if (family->create(sock, protocol) != 0) { print_err("Creating domain failed\n"); goto abort_socket; } ...}

在_socket()函数里面,我们是调用alloc_socket)()函数来分配一个管理套接字的结构体sock,在该结构体里面保存了套接字初始状态和应用程序的相关信息,如下图:

static struct socket *alloc_socket(pid_t pid){    // TODO: Figure out a way to not shadow kernel file descriptors.    // Now, we'll just expect the fds for a process to never exceed this.    static int fd = 4097;    struct socket *sock = malloc(sizeof (struct socket));    list_init(&sock->list);
sock->pid = pid; sock->refcnt = 1;
pthread_rwlock_wrlock(&slock); sock->fd = fd++; pthread_rwlock_unlock(&slock);
sock->state = SS_UNCONNECTED; sock->ops = NULL; sock->flags = O_RDWR; wait_init(&sock->sleep); pthread_rwlock_init(&sock->lock, NULL); return sock;}

在第16行,设置了该套接字的初始状态为SS_UNCONNECTED,注意此处并不是指tcp的SYNC_SENT状态,大家不要把它们相混淆。

接着在_socket()函数里面调用了family->create()函数,这里考虑到多种协议族的支持,通过函数指针做了一个代码分离。该函数指针真正指向的函数为inet_create()函数,我们来分析一下它:

int inet_create(struct socket *sock, int protocol){    struct sock *sk;    struct sock_type *skt = NULL;
for (int i = 0; i < INET_OPS; i++) { if (inet_ops[i].type & sock->type) { skt = &inet_ops[i]; break; } }
if (!skt) { print_err("Could not find socktype for socket\n"); return 1; }
sock->ops = skt->sock_ops;
sk = sk_alloc(skt->net_ops, protocol); sk->protocol = protocol; sock_init_data(sock, sk); return 0;}

第6~10行:获取tcp连接的相关操作接口集合inet_ops,它里面又细分出tcp操作接口集合tcp_ops和以太网底层操作接口集合inet_stream_ops。如下:

static struct sock_type inet_ops[] = {    {        .sock_ops = &inet_stream_ops,        .net_ops = &tcp_ops,        .type = SOCK_STREAM,        .protocol = IPPROTO_TCP,    }};

第29行:调用sk_alloc函数分配sk结构体,在该函数里面调用了tcp_ops接口里面的tcp_alloc_sock()函数来完成分配工作,如下:

struct sock *sk_alloc(struct net_ops *ops, int protocol){    struct sock *sk;    sk = ops->alloc_sock(protocol);    sk->ops = ops;    return sk;}

第4行:调用tcp_ops中的tcp_alloc_sock()函数,产生管理tcp通信的接=结构体sk

第5行:把tcp操作接口集合tcp_ops记录在sk->ops中

其中,tcp_alloc_sock()函数的实现如下:

struct sock *tcp_alloc_sock(){    struct tcp_sock *tsk = malloc(sizeof(struct tcp_sock));
memset(tsk, 0, sizeof(struct tcp_sock)); tsk->sk.state = TCP_CLOSE; tsk->sackok = 1; tsk->rmss = 1460; // Default to 536 as per spec tsk->smss = 536;
skb_queue_init(&tsk->ofo_queue); return (struct sock *)tsk;}

第3~5行:使用malloc函数动态申请内存,并初始化内存值为0

第6行:初始化tcp的连接状态为close,终于看到tcp的初始连接状态了!!!

第13行:初始化tcp无序队列,当tcp接收到的数据不是有序的时候,先把数据挂载在这个队列上。

从connect()函数来看SYNC_SENT状态

同理,应用程序中的connect()函数,实际上是调用了level-ip协议站中的ipc_connect()函数,该函数的核心是_connect()函数,我们来看一下这个函数,如下:

int _connect(pid_t pid, int sockfd, const struct sockaddr *addr, socklen_t addrlen){    struct socket *sock;
if ((sock = get_socket(pid, sockfd)) == NULL) { print_err("Connect: could not find socket (fd %u) for connection (pid %d)\n", sockfd, pid); return -EBADF; }
socket_wr_acquire(sock);
int rc = sock->ops->connect(sock, addr, addrlen, 0); switch (rc) { case -EINVAL: case -EAFNOSUPPORT: case -ECONNREFUSED: case -ETIMEDOUT: socket_release(sock); socket_free(sock); break; default: socket_release(sock); } return rc;}

第5行:获取_socket()函数申请到的套接字结构体sock,以进一步操作该次tcp链接。

第12行:调用以太网底层接口inet_stream_connect,该函数实现如下:

static int inet_stream_connect(struct socket *sock, const struct sockaddr *addr,                        int addr_len, int flags){    struct sock *sk = sock->sk;    int rc = 0;    ...    switch (sock->state) {    default:        sk->err = -EINVAL;        goto out;    case SS_CONNECTED:        sk->err = -EISCONN;        goto out;    case SS_CONNECTING:        sk->err = -EALREADY;        goto out;    case SS_UNCONNECTED:        sk->err = -EISCONN;        if (sk->state != TCP_CLOSE) {            goto out;        }
sk->ops->connect(sk, addr, addr_len, flags); sock->state = SS_CONNECTING; sk->err = -EINPROGRESS;
if (sock->flags & O_NONBLOCK) { goto out; }
pthread_mutex_lock(&sock->sleep.lock); while (sock->state == SS_CONNECTING && sk->err == -EINPROGRESS) { socket_release(sock); wait_sleep(&sock->sleep); socket_wr_acquire(sock); } pthread_mutex_unlock(&sock->sleep.lock); socket_wr_acquire(sock); switch (sk->err) { case -ETIMEDOUT: case -ECONNREFUSED: goto sock_error; }
if (sk->err != 0) { goto out; }
sock->state = SS_CONNECTED; break; }...
}

第7~21行,判断套接字状态是否正常,前面我们已经通过_socket()函数把套接字状态初始化为SS_UNCONNECTED了,因此这里将进入最后一个分支来执行代码。

第23行:调用tcp操作接口集合tcp_ops中的tcp_v4_connect函数,在该函数中会构建tcp数据帧,然后调用ip数据帧发送接口来进行数据的发送。

第34行:发送握手帧之后,线程进行睡眠状态,直到服务器返回应答帧之后再唤醒。

其中,tcp_v4_connect()调用了tcp_connect()函数,实现如下:

int tcp_connect(struct sock *sk){    struct tcp_sock *tsk = tcp_sk(sk);    struct tcb *tcb = &tsk->tcb;    int rc = 0;        tsk->tcp_header_len = sizeof(struct tcphdr);    tcb->iss = generate_iss();    tcb->snd_wnd = 0;    tcb->snd_wl1 = 0;
tcb->snd_una = tcb->iss; tcb->snd_up = tcb->iss; tcb->snd_nxt = tcb->iss; tcb->rcv_nxt = 0;
tcp_select_initial_window(&tsk->tcb.rcv_wnd);
rc = tcp_send_syn(sk); tcb->snd_nxt++; return rc;}

第8行、第14行:随机产生了一个序列号,填充到tcp首部中

第18行:进一步调用tcp_send_syn()函数,该函数实现如下:

static int tcp_send_syn(struct sock *sk){    if (sk->state != TCP_SYN_SENT && sk->state != TCP_CLOSE && sk->state != TCP_LISTEN) {        print_err("Socket was not in correct state (closed or listen)\n");        return 1;    }
struct sk_buff *skb; struct tcphdr *th; struct tcp_options opts = { 0 }; int tcp_options_len = 0;
tcp_options_len = tcp_syn_options(sk, &opts); skb = tcp_alloc_skb(tcp_options_len, 0); th = tcp_hdr(skb);
tcp_write_syn_options(th, &opts, tcp_options_len); sk->state = TCP_SYN_SENT; th->syn = 1;
return tcp_queue_transmit_skb(sk, skb);}

第18行:此时tcp的连接状态已经变为TCP_SYN_SENT状态了

第19行:数据帧中握手标志置1

第21行:发送tcp握手帧

总结

暂时先分析握手的第一个状态变化,后面继续把剩余部分分析完,今天先到这里。


END




什么是零拷贝?一招学会高效传输文件

怎样学好网络编程?

TCP/IP和Unix的发展轨迹


扫描二维码

获取更多精彩

just enjoy!


觉得不错,请点个在看