socket连接和通信过程解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/THEANARKH/article/details/89442680

网络通信的标准流程是,服务端新建一个socket,然后在该socket中绑定一个地址,再设置该socket为监听socket,然后阻塞在accept等待连接。客户端新建一个socket,然后connect到一个服务端的地址。下面分析一下这个过程。看多个客户端或者多个连接是如何在一个监听的socket中完成通信的。
服务器收到一个syn包的时候,在tcp_rcv中进行处理。该函数根据tcp数据包中的目的端口和ip,源端口和ip找到一个最匹配的socket

sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr);
下面是get_socket的代码。
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;

		if(s->dead && (s->state == TCP_CLOSE))
			continue;
		/* local address matches? */
		if (s->saddr) {
			if (s->saddr != laddr)
				continue;
			score++;
		}
		/* remote address matches? */
		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中有本机的ip和监听的端口。所以根据tcp数据包,可以找到对应的socket。接着判断找到的socket的状态。

if(sk->state!=TCP_ESTABLISHED)	{
	
		// 是监听socket则可能是一个syn包	
		if(sk->state==TCP_LISTEN)
		{	// 不存在收到ack包的可能,发送重置包
			if(th->ack)	
				tcp_reset(daddr,saddr,th,sk->prot,opt,dev,sk->ip_tos, sk->ip_ttl);

			// 不存在这种可能的各种情况,直接丢包			   
			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());
                }
}

tcp_conn_request函数很长,下面只列出关键代码。sock结构体是tcp层的表示,socket结构体是更上层的抽象,比如unix域套接字,也是使用socket结构体,然后在unix域实现的时候,使用unix_proto_data结构体。socket和sock结构体是互相指向的。tcp维护了一个哈希链表,以监听的端口号为因子进行哈希。

// 分配一个新的sock结构用于连接连接
newsk = (struct sock *) kmalloc(sizeof(struct sock), GFP_ATOMIC);
// 从listen套接字复制内容,再覆盖某些字段
memcpy(newsk, sk, sizeof(*newsk));
// 进入第二次握手状态
newsk->state = TCP_SYN_RECV;
// 记录ip
newsk->dummy_th.source = skb->h.th->dest;
newsk->dummy_th.dest = skb->h.th->source;
newsk->daddr = saddr;
newsk->saddr = daddr;
// 放到tcp的socket哈希队列
put_sock(newsk->num,newsk);
// 分配一个skb
buff = newsk->prot->wmalloc(newsk, MAX_SYN_SIZE, 1, GFP_ATOMIC);
// 发送ack,即第二次握手
newsk->prot->queue_xmit(newsk, ndev, buff, 0);
// skb关联的sock为newsk,accept的时候摘取skb时即拿到该sock对应的socket结构体返回给应用层
skb->sk = newsk;
skb_queue_tail(&sk->receive_queue,skb);
至此完成了tcp的两次握手。
等收到客户端的ack的时候,在tcp_ack中完成第三次握手。
       if(sk->state==TCP_SYN_RECV){
		tcp_set_state(sk, TCP_ESTABLISHED);
	}

此时的内存视图如下。
在这里插入图片描述
第一个sock结构体就是负责监听的,第二个就是建立连接后,新建的,其中新建的sock和监听sock有很多字段是一样的。不过新建的sock里记录了目的端口、ip、源端口、源ip。这时候accept函数就会返回新建的sock,我们就会对这个sock进行读写操作(其实是对上层的sockect结构体进行操作)。当我们阻塞在read的时候,如果收到了一个数据包,处理函数还是tcp_rcv,第一步还是根据tcp包的源ip、端口、目的ip、目的端口到sock池子里查找最匹配的sock结构体,这时候找到的就是刚才我们新建的那个sock。因为他四个都匹配上了。接着在tcp_data函数里把数据包对应的skb结构体放到新建sock的receive_queue。read从阻塞中返回,并拿到数据包的数据。
通过上面的分析,我们可以知道几点,第一,监听的sock和数据通信的sock结构是一样的,但是他们的receive_queue里的数据包含义不一样,监听sock的receive_queue中的节点代表的是即将或者已经建立连接的节点,而数据通信的sock的receive_queue中的节点是通信的数据。我们知道的第二点是多个客户端或者连接,是如何在一个监听的sock中完成tcp的建立,又是从监听的sock中过渡到数据通信sock,最后在新加的sock中完成数据通信的。

猜你喜欢

转载自blog.csdn.net/THEANARKH/article/details/89442680