

我们就擦肩而过了
有趣
有用
有态度

阅读本文需要对level-ip的整体架构有所了解,如果读者尚未接触过level-ip,请先阅读下面文章:
Linux系统中间件的巧妙实现--以用户空间的tcp协议栈为例
请根据上述文章中的指引获取leve-ip的全部源码,并且尝试在任意Linux发行版本上编译运行。
著名的TCP三次握手
前面已经介绍过常用套接字接口函数,也就是服务器调用bind、listen 以及 accept 等待客户端进行连接,而客户端connect函数主动请求连接服务器。
客户在调用函数 connect 前不必非得调用 bind 函数,因为如果需要的话,内核会确定源IP 地址,并按照一定的算法选择一个临时端口作为源端口。当客户端使用tcp套接字进行连接时,调用 connect 函数将激发 TCP 的三次握手过程。如下图:

TCP三次握手的剖析
这里我们使用的网络编程模型都是阻塞式的。所谓阻塞式,就是调用发起后不会直接返回,由操作系统内核处理之后才会返回。相对的,还有一种叫做非阻塞式的,暂时先不讨论。
下面是具体的过程:
客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态; 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态; 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1; 应答包到达服务器端后,服务器端协议栈使得 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 spectsk->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握手帧
总结
暂时先分析握手的第一个状态变化,后面继续把剩余部分分析完,今天先到这里。



扫描二维码
获取更多精彩
just enjoy!




觉得不错,请点个在看