深入理解TCP/IP协议的实现之三次握手(基于linux1.2.13)
- 2020 年 3 月 12 日
- 笔记
上篇我们分析了accept函数,他是消费者,这篇我们看看生产者是怎么实现的。我们从tcp_rcv函数开始,这个函数是一个分发器。当接收到一个tcp包的时候,底层就会调这个函数交给tcp层处理。
// daddr,saddr是ip头的字段,len为tcp头+数据长度 int tcp_rcv( struct sk_buff *skb, struct device *dev, struct options *opt, unsigned long daddr, unsigned short len, unsigned long saddr, int redo, struct inet_protocol * protocol ){ // tcp报文头 th = skb->h.th; // 从tcp的socket哈希链表中找到对应的socket结构 sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); // 读缓冲区已满,丢弃数据包 if (sk->rmem_alloc + skb->mem_len >= sk->rcvbuf) { kfree_skb(skb, FREE_READ); release_sock(sk); return(0); } // 这个就是上篇我们说的skb中关联的sock结构体。 skb->sk=sk; // 增加读缓冲区已使用的内存的大小 sk->rmem_alloc += skb->mem_len; // 连接还没有建立,不是通信数据包 if(sk->state!=TCP_ESTABLISHED) { // 是监听socket则可能是一个syn包 if(sk->state==TCP_LISTEN) { // 不存在这种可能的各种情况,直接丢包 if(th->rst || !th->syn || th->ack || ip_chk_addr(daddr)!=IS_MYADDR) { kfree_skb(skb, FREE_READ); release_sock(sk); return 0; } // 是个syn包,建立连接 tcp_conn_request(sk, skb, daddr, saddr, opt, dev, tcp_init_seq()); release_sock(sk); return 0; } } }
上面的函数主要的逻辑两个 1 get_sock是根据源ip、端口和目的ip、端口从tcp的sock_array哈希表里找到对应的sock结构体。
/* num 目的端口 raddr 源ip rnum 源端口 laddr 目的ip */ struct sock *get_sock(struct proto *prot, unsigned short num, unsigned long raddr, unsigned short rnum, unsigned long laddr) { struct sock *s; struct sock *result = NULL; int badness = -1; unsigned short hnum; hnum = ntohs(num); for(s = prot->sock_array[hnum & (SOCK_ARRAY_SIZE - 1)]; s != NULL; s = s->next) { int score = 0; // 绑定的端口是否等于报文中的目的端口 if (s->num != hnum) continue; // 该sock已经不用了,下一个 if(s->dead && (s->state == TCP_CLOSE)) continue; /* local address matches? */ // 绑定的ip,bind的时候赋值 if (s->saddr) { // 报文中的目的ip是不是等于该socket绑定的ip if (s->saddr != laddr) continue; score++; } /* remote address matches? */ // 目的ip if (s->daddr) { if (s->daddr != raddr) continue; score++; } /* remote port matches? */ // 目的端口 if (s->dummy_th.dest) { if (s->dummy_th.dest != rnum) continue; score++; } /* perfect match? */ // 全匹配,直接返回 if (score == 3) return s; /* no, check if this is the best so far.. */ if (score <= badness) continue; // 记录最好的匹配项 result = s; badness = score; } return result; }
对于监听型的socket,我们在bind的时候写入了绑定的ip和端口。对于监听型的socket,是没有目的ip和目的端口的。通信型的socket才有。所以上面的函数根据服务端绑定的ip和端口。判断是否等于tcp报文中的目的ip和端口。最后拿到监听型的sock结构体。 2 tcp_conn_request处理sync包,tcp_conn_request里完成了tcp的第一次和第二次握手。
// 收到一个syn包时的处理 static void tcp_conn_request(struct sock *sk, struct sk_buff *skb, unsigned long daddr, unsigned long saddr, struct options *opt, struct device *dev, unsigned long seq) { struct sk_buff *buff; struct tcphdr *t1; unsigned char *ptr; struct sock *newsk; struct tcphdr *th; struct device *ndev=NULL; int tmp; struct rtable *rt; th = skb->h.th; // 过载则丢包,防止ddos,max_ack_backlog即listen的参数 if (sk->ack_backlog >= sk->max_ack_backlog) { tcp_statistics.TcpAttemptFails++; kfree_skb(skb, FREE_READ); return; } // 分配一个新的sock结构用于通信。accept的时候返回的就是这个sock结构体 newsk = (struct sock *) kmalloc(sizeof(struct sock), GFP_ATOMIC); // 从listen套接字复制内容,再覆盖某些字段 memcpy(newsk, sk, sizeof(*newsk)); skb_queue_head_init(&newsk->write_queue); skb_queue_head_init(&newsk->receive_queue); newsk->send_head = NULL; newsk->send_tail = NULL; skb_queue_head_init(&newsk->back_log); newsk->rtt = 0; /*TCP_CONNECT_TIME<<3*/ newsk->rto = TCP_TIMEOUT_INIT; newsk->mdev = 0; newsk->max_window = 0; newsk->cong_window = 1; newsk->cong_count = 0; newsk->ssthresh = 0; newsk->backoff = 0; newsk->blog = 0; newsk->intr = 0; newsk->proc = 0; newsk->done = 0; newsk->partial = NULL; newsk->pair = NULL; newsk->wmem_alloc = 0; newsk->rmem_alloc = 0; newsk->localroute = sk->localroute; newsk->max_unacked = MAX_WINDOW - TCP_WINDOW_DIFF; newsk->err = 0; newsk->shutdown = 0; newsk->ack_backlog = 0; // 期待收到的对端下一个字节的序列号 newsk->acked_seq = skb->h.th->seq+1; // 进程可以读但是还没有读取的字节序列号 newsk->copied_seq = skb->h.th->seq+1; // 当收到对端fin包的时候,回复的ack包中的序列号 newsk->fin_seq = skb->h.th->seq; // 进入syn_recv状态 newsk->state = TCP_SYN_RECV; newsk->timeout = 0; newsk->ip_xmit_timeout = 0; // 下一个发送的字节的序列号 newsk->write_seq = seq; // 可发送的字节序列号最大值 newsk->window_seq = newsk->write_seq; // 序列号小于rcv_ack_seq的数据包都已经收到 newsk->rcv_ack_seq = newsk->write_seq; newsk->urg_data = 0; newsk->retransmits = 0; // 关闭套接字的时候不需要等待一段时间才能关闭 newsk->linger=0; newsk->destroy = 0; init_timer(&newsk->timer); newsk->timer.data = (unsigned long)newsk; newsk->timer.function = &net_timer; init_timer(&newsk->retransmit_timer); newsk->retransmit_timer.data = (unsigned long)newsk; newsk->retransmit_timer.function=&retransmit_timer; // 记录端口,发送ack和get_sock的时候用 newsk->dummy_th.source = skb->h.th->dest; newsk->dummy_th.dest = skb->h.th->source; // 记录ip,发送ack和get_sock的时候用 newsk->daddr = saddr; newsk->saddr = daddr; // 放到tcp的sock哈希表 put_sock(newsk->num,newsk); // tcp头 newsk->dummy_th.res1 = 0; newsk->dummy_th.doff = 6; newsk->dummy_th.fin = 0; newsk->dummy_th.syn = 0; newsk->dummy_th.rst = 0; newsk->dummy_th.psh = 0; newsk->dummy_th.ack = 0; newsk->dummy_th.urg = 0; newsk->dummy_th.res2 = 0; newsk->acked_seq = skb->h.th->seq + 1; newsk->copied_seq = skb->h.th->seq + 1; newsk->socket = NULL; newsk->ip_ttl=sk->ip_ttl; newsk->ip_tos=skb->ip_hdr->tos; rt=ip_rt_route(saddr, NULL,NULL); if(rt!=NULL && (rt->rt_flags&RTF_WINDOW)) newsk->window_clamp = rt->rt_window; else newsk->window_clamp = 0; if (sk->user_mss) newsk->mtu = sk->user_mss; else if(rt!=NULL && (rt->rt_flags&RTF_MSS)) newsk->mtu = rt->rt_mss - HEADER_SIZE; else { #ifdef CONFIG_INET_SNARL /* Sub Nets Are Local */ if ((saddr ^ daddr) & default_mask(saddr)) #else if ((saddr ^ daddr) & dev->pa_mask) #endif newsk->mtu = 576 - HEADER_SIZE; else newsk->mtu = MAX_WINDOW; } // mtu等于设备的mtu减去ip头和tcp头的大小 newsk->mtu = min(newsk->mtu, dev->mtu - HEADER_SIZE); // 解析tcp选项 tcp_options(newsk,skb->h.th); // 分配一个skb buff = newsk->prot->wmalloc(newsk, MAX_SYN_SIZE, 1, GFP_ATOMIC); // skb和sock关联,4个字节是用于tcp mss选项,告诉对端自己的mss buff->len = sizeof(struct tcphdr)+4; buff->sk = newsk; buff->localroute = newsk->localroute; t1 =(struct tcphdr *) buff->data; // 构造ip和mac头 tmp = sk->prot->build_header(buff, newsk->saddr, newsk->daddr, &ndev, IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl); buff->len += tmp; // tcp头 t1 =(struct tcphdr *)((char *)t1 +tmp); memcpy(t1, skb->h.th, sizeof(*t1)); buff->h.seq = newsk->write_seq; t1->dest = skb->h.th->source; t1->source = newsk->dummy_th.source; t1->seq = ntohl(newsk->write_seq++); // 是个ack包,即第二次握手 t1->ack = 1; newsk->window = tcp_select_window(newsk); newsk->sent_seq = newsk->write_seq; t1->window = ntohs(newsk->window); t1->res1 = 0; t1->res2 = 0; t1->rst = 0; t1->urg = 0; t1->psh = 0; t1->syn = 1; t1->ack_seq = ntohl(skb->h.th->seq+1); t1->doff = sizeof(*t1)/4+1; ptr =(unsigned char *)(t1+1); ptr[0] = 2; ptr[1] = 4; ptr[2] = ((newsk->mtu) >> 8) & 0xff; ptr[3] =(newsk->mtu) & 0xff; tcp_send_check(t1, daddr, saddr, sizeof(*t1)+4, newsk); // 发送ack,即第二次握手 newsk->prot->queue_xmit(newsk, ndev, buff, 0); reset_xmit_timer(newsk, TIME_WRITE , TCP_TIMEOUT_INIT); // skb关联的socket为newsk,accept的时候摘取skb时即拿到该socket返回给应用层 skb->sk = newsk; // 把skb中数据的大小算在newsk中 sk->rmem_alloc -= skb->mem_len; newsk->rmem_alloc += skb->mem_len; // 插入监听型socket的接收队列,accept的时候摘取 skb_queue_tail(&sk->receive_queue,skb); // 连接队列节点个数加1 sk->ack_backlog++; release_sock(newsk); tcp_statistics.TcpOutSegs++; }
这个函数主要是生成一个sock结构体,挂载到skb中,然后把skb插入队列中。最后发送ack完成第二次握手。

我们继续来看第三次握手。前面说过tcp_rcv是处理tcp数据包的。所以我们还是回到这个函数。
// daddr,saddr是ip头的字段,len为tcp头+数据长度 int tcp_rcv( struct sk_buff *skb, struct device *dev, struct options *opt, unsigned long daddr, unsigned short len, unsigned long saddr, int redo, struct inet_protocol * protocol ){ // 从tcp的socket哈希链表中找到对应的socket结构 sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); // 省略大量代码 if(sk->state==TCP_SYN_RECV) { tcp_set_state(sk, TCP_ESTABLISHED); } }
这里的逻辑很简单,就是设置sock结构体的状态为建立。但是我们发现,get_sock的时候,拿到的不再是listen型的那个sock结构体了,而是tcp_conn_request中生成的那个。因为tcp_conn_request生成的sock里设置了源ip、端口、目的ip、端口。get_sock匹配的时候会全匹配到这个新的sock。 三次握手的过程中,第一次握手的时候,在监听型的sock结构体的接收队列里插入了一个sock节点。在第三次握手的时候,修改这个sock状态为已连接。我们看看accept函数是怎么摘取这个队列中的节点的。
// 返回一个完成的连接 static struct sk_buff *tcp_dequeue_established(struct sock *s) { struct sk_buff *skb; unsigned long flags; save_flags(flags); cli(); skb=tcp_find_established(s); if(skb!=NULL) skb_unlink(skb); /* Take it off the queue */ restore_flags(flags); return skb; } // 找出已经完成三次握手的socket static struct sk_buff *tcp_find_established(struct sock *s) { struct sk_buff *p=skb_peek(&s->receive_queue); if(p==NULL) return NULL; do { if(p->sk->state == TCP_ESTABLISHED || p->sk->state >= TCP_FIN_WAIT1) return p; p=p->next; } while(p!=(struct sk_buff *)&s->receive_queue); return NULL; }
就是找到状态为TCP_ESTABLISHED的节点返回。另外监听型socket和通信型socket他的接收队列意义是不一样的,前者是已完成连接或者正在建立连接的队列,后者是数据包队列。这一版的linux把正在连接和已经完成连接的sock都放到一个队列里维护,现在的版本据说已经分为两个队列了。