让你彻底了解Linux内核网络协议栈《connect函数剖析》

335 阅读12分钟
  • TCP客户用 connect 函数来建立与 TCP 服务器的连接,其实是客户利用 connect 函数向服务器端发出连接请求。

1、应用层——connect 函数

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
/*sockfd是由socket函数返回的套接口描述字,第二、第三个参数分别是一个指向套接口地址结构的指针和该结构的大小。套接口地址结构必须含有服务器的IP地址和端口号*/
  • 上面的 sockfd 套接字描述符是客户端的套接字。

2、BSD Socket 层——sock_connect 函数

/*
首先将要连接的源端地址从用户缓冲区复制到内核缓冲区,之后根据套接字目前所处状态
 *  采取对应措施,如果状态有效,转调用connect函数
 */
 //这是客户端,表示客户端向服务器端发送连接请求
static int sock_connect(int fd, struct sockaddr *uservaddr, int addrlen)
{
	struct socket *sock;
	struct file *file;
	int i;
	char address[MAX_SOCK_ADDR];
	int err;
    //参数有效性检查
	if (fd < 0 || fd >= NR_OPEN || (file=current->files->fd[fd]) == NULL)
		return(-EBADF);
	//给定文件描述符返回socket结构以及file结构指针
	if (!(sock = sockfd_lookup(fd, &file)))
		return(-ENOTSOCK);
    //用户地址空间数据拷贝到内核地址空间
	if((err=move_addr_to_kernel(uservaddr,addrlen,address))<0)
	  	return err;
    //根据状态采取对应措施
	switch(sock->state) 
	{
		case SS_UNCONNECTED:
			/* This is ok... continue with connect */
			break;
		case SS_CONNECTED:
			/* Socket is already connected */
			if(sock->type == SOCK_DGRAM) /* Hack for now - move this all into the protocol */
				break;
			return -EISCONN;
		case SS_CONNECTING:
			/* Not yet connected... we will check this. */
			/*
			 *	FIXME:  for all protocols what happens if you start
			 *	an async connect fork and both children connect. Clean
			 *	this up in the protocols!
			 */
			break;
		default:
			return(-EINVAL);
	}
	//调用下层函数(inet_connect())
	i = sock->ops->connect(sock, (struct sockaddr *)address, addrlen, file->f_flags);
	if (i < 0) 
	{
		return(i);
	}
	return(0);
}

  • 该函数比较简单,主要是调用下层函数来实现,该函数则负责下层函数调用前的准备工作。

3、INET Socket 层——inet_connect 函数

  • 客户端套接字的端口号是在这个函数中绑定的。

    /*

    • Connect to a remote host. There is regrettably still a little

    • TCP 'magic' in here. */ //完成套接字的连接请求操作,这是客户端主动向服务器端发送请求 //sock是客户端套接字,后面的uaddr,addr_len则是对端服务器端的地址信息 static int inet_connect(struct socket *sock, struct sockaddr * uaddr, int addr_len, int flags) { struct sock *sk=(struct sock )sock->data; int err; sock->conn = NULL; //正在与远端取得连接,且tcp对应的状态 if (sock->state == SS_CONNECTING && tcp_connected(sk->state)) { sock->state = SS_CONNECTED;//直接设置字段为已经连接 / Connection completing after a connect/EINPROGRESS/select/connect / return 0; / Rock and roll / } //正在取得连接,且是tcp协议,非阻塞 if (sock->state == SS_CONNECTING && sk->protocol == IPPROTO_TCP && (flags & O_NONBLOCK)) { if (sk->err != 0) { err=sk->err; sk->err=0; return -err; } //返回正在进行状态 return -EALREADY; / Connecting is currently in progress / } //不是处于正在连接处理状态(现在进行时态) if (sock->state != SS_CONNECTING) { / We may need to bind the socket. */ //自动绑定一个端口号,客户端自动绑定端口号是在connect函数中实现的 if(inet_autobind(sk)!=0) return(-EAGAIN); if (sk->prot->connect == NULL) //不支持该项操作,没有指定操作函数 return(-EOPNOTSUPP); //转调用connect函数(传输层 tcp_connect函数) err = sk->prot->connect(sk, (struct sockaddr_in )uaddr, addr_len); if (err < 0) return(err); sock->state = SS_CONNECTING;//设置状态字段,表示正在连接过程中 } //这个状态下,这是关闭信号。 //清楚,这里有两个state,一个是socket的state(套接字所处的连接状态),一个是sock的state(涉及到协议,比如tcp的状态) //上面调用下层connect函数,会更新sk->state,如果出现>TCP_FIN_WAIT2,表明连接过程出现了异常 if (sk->state > TCP_FIN_WAIT2 && sock->state==SS_CONNECTING) { sock->state=SS_UNCONNECTED;//连接未建立 cli(); err=sk->err; sk->err=0; sti(); return -err; } //没有建立,就是在正在建立的路上 if (sk->state != TCP_ESTABLISHED &&(flags & O_NONBLOCK)) return(-EINPROGRESS);//过程正在处理 cli(); / avoid the race condition / //这里的while实则是等待下层函数(前面的connect调用)的返回 //正常退出while循环,表示连接成功 while(sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) { interruptible_sleep_on(sk->sleep);//添加到sk中的等待队列中,直到资源可用被唤醒 if (current->signal & ~current->blocked) { sti(); return(-ERESTARTSYS); } / This fixes a nasty in the tcp/ip code. There is a hideous hassle with icmp error packets wanting to close a tcp or udp socket. / if(sk->err && sk->protocol == IPPROTO_TCP) { sti(); sock->state = SS_UNCONNECTED; err = -sk->err; sk->err=0; return err; / set by tcp_err() */ } } sti(); sock->state = SS_CONNECTED;//成功建立连接 if (sk->state != TCP_ESTABLISHED && sk->err) //出错处理 { sock->state = SS_UNCONNECTED; err=sk->err; sk->err=0; return(-err); } return(0); }

  • 实质操作落到了下一层函数(tcp_connect函数)

4、传输层——tcp_connect 函数

  • tcp_connect 函数是由客户端调用的,客户端通过这个函数获得对端的地址信息(ip地址和端口号),另外本地ip地址也是在这个函数中指定的。三次握手阶段起于 connect 函数,自然地,在该函数指定目的地址,以及设置标志字段,定时器以后,就需要向服务器端发送连接请求数据包,对应操作在该函数最后。

    /*

    • This will initiate an outgoing connection. */ //同accept; connect->sock_connect->inet_connect->tcp_connect //connect就是客户端向服务器端发出连接请求 //参数:sk:客户端套接字;usin和addrlen分别是一个指向服务器端套接口地址结构的指针和该结构的大小 static int tcp_connect(struct sock *sk, struct sockaddr_in *usin, int addr_len) { struct sk_buff *buff; struct device *dev=NULL; unsigned char *ptr; int tmp; int atype; struct tcphdr *t1;//tcp首部 struct rtable rt;//ip路由表 if (sk->state != TCP_CLOSE) //不是关闭状态就是表示已经建立连接了 { return(-EISCONN);//连接已建立 } //地址结构大小检查 if (addr_len < 8) return(-EINVAL); //地址簇检查,INET域 if (usin->sin_family && usin->sin_family != AF_INET) return(-EAFNOSUPPORT); /
      • connect() to INADDR_ANY means loopback (BSD'ism). / if(usin->sin_addr.s_addr==INADDR_ANY)//指定一个通配地址 usin->sin_addr.s_addr=ip_my_addr();//本地ip地址(dev_base设备) /

      • Don't want a TCP connection going to a broadcast address */ //检查ip传播地址方式。广播、多播均不可行 if ((atype=ip_chk_addr(usin->sin_addr.s_addr)) == IS_BROADCAST || atype==IS_MULTICAST) return -ENETUNREACH; //sk已经具备本地地址信息,这里在赋值目的地址信息,这样sock套接字就具备了本地与对端两者的地址信息 //知道住哪了,就知道怎么去了 sk->inuse = 1;//加锁 sk->daddr = usin->sin_addr.s_addr;//远端地址,即要请求连接的对端服务器地址 sk->write_seq = tcp_init_seq();//初始化一个序列号,跟当前时间挂钩的序列号 sk->window_seq = sk->write_seq;//窗口大小,用write_seq初始化 sk->rcv_ack_seq = sk->write_seq -1;//目前本地接收到的对本地发送数据的应答序列号,表示此序号之前的数据已接收 sk->err = 0;//错误标志清除 sk->dummy_th.dest = usin->sin_port;//端口号赋值给tcp首部目的地址 release_sock(sk);//重新接收暂存的数据包 buff = sk->prot->wmalloc(sk,MAX_SYN_SIZE,0, GFP_KERNEL);//分配一个网络数据包结构 if (buff == NULL) { return(-ENOMEM); } sk->inuse = 1; buff->len = 24;//指定数据部分长度(头+数据) buff->sk = sk;//绑定套接字 buff->free = 0;//发送完数据包后,不立即清除,先缓存起来 buff->localroute = sk->localroute;//路由类型 //buff->data 是指向数据部分的首地址(包括首部),这里是传输层,对应的数据部分为 // TCP Hearder | data;buff->data则是指向其首地址 t1 = (struct tcphdr ) buff->data;//tcp首部数据 //buff->data中保存的是数据包的首部地址,在各个层对应不同的首部 /

      • Put in the IP header and routing stuff. / //查找合适的路由表项 rt=ip_rt_route(sk->daddr, NULL, NULL); /

      • We need to build the routing stuff from the things saved in skb. */ //这里是调用ip_build_header(ip.c),结合前面可以看出prot操作函数调用的一般都是下一层的函数 //build mac header 然后 build ip header,该函数返回时,buff的data部分已经添加了ip 首部和以太网首部 //返回这两个首部大小之和 tmp = sk->prot->build_header(buff, sk->saddr, sk->daddr, &dev, IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl); if (tmp < 0) { sk->prot->wfree(sk, buff->mem_addr, buff->mem_len); release_sock(sk); return(-ENETUNREACH); } //connect 函数是向指定地址的网络端发送连接请求数据包,最终数据包要被对端的硬件设备接收 //所以需要对端的ip地址 mac地址。 buff->len += tmp;//数据帧长度更新,即加上创建的这两个首部长度 t1 = (struct tcphdr *)((char *)t1 +tmp);//得到tcp首部 //t1指针结构中对应的内存布局为:mac首部+ip首部+tcp首部+数据部分 //t1是该结构的首地址,然后偏移mac首部和ip首部大小位置,定位到tcp首部 memcpy(t1,(void *)&(sk->dummy_th), sizeof(t1));//拷贝缓存的tcp首部 t1->seq = ntohl(sk->write_seq++);//32位序列号,序列号字节序转换 //下面为tcp保证可靠数据传输使用的序列号 sk->sent_seq = sk->write_seq;//将要发送的数据包的第一个字节的序列号 buff->h.seq = sk->write_seq;//该数据包的ack值,针对tcp协议而言 //tcp首部控制字设置 t1->ack = 0; t1->window = 2;//窗口大小 t1->res1=0;//首部长度 t1->res2=0; t1->rst = 0; t1->urg = 0; t1->psh = 0; t1->syn = 1;//同步控制位 t1->urg_ptr = 0; t1->doff = 6; / use 512 or whatever user asked for / //窗口大小,最大传输单元设置 if(rt!=NULL && (rt->rt_flags&RTF_WINDOW)) sk->window_clamp=rt->rt_window;//窗口大小钳制值 else sk->window_clamp=0; if (sk->user_mss) sk->mtu = sk->user_mss;//mtu最大传输单元 else if(rt!=NULL && (rt->rt_flags&RTF_MTU)) sk->mtu = rt->rt_mss; else { #ifdef CONFIG_INET_SNARL if ((sk->saddr ^ sk->daddr) & default_mask(sk->saddr)) #else if ((sk->saddr ^ sk->daddr) & dev->pa_mask) #endif sk->mtu = 576 - HEADER_SIZE; else sk->mtu = MAX_WINDOW; } /

      • but not bigger than device MTU / if(sk->mtu <32) sk->mtu = 32; / Sanity limit / sk->mtu = min(sk->mtu, dev->mtu - HEADER_SIZE);//mtu取允许值 /

      • Put in the TCP options to say MTU. */ //这里不是很清楚 ptr = (unsigned char )(t1+1); ptr[0] = 2; ptr[1] = 4; ptr[2] = (sk->mtu) >> 8; ptr[3] = (sk->mtu) & 0xff; //计算tcp校验和 tcp_send_check(t1, sk->saddr, sk->daddr,sizeof(struct tcphdr) + 4, sk); /

      • This must go first otherwise a really quick response will get reset. / //connect发起连接请求时,开始tcp的三次握手,这是第一个状态 tcp_set_state(sk,TCP_SYN_SENT);//设置tcp状态 sk->rto = TCP_TIMEOUT_INIT;//延迟时间值 #if 0 / we already did this / init_timer(&sk->retransmit_timer); #endif //重发定时器设置 sk->retransmit_timer.function=&retransmit_timer; sk->retransmit_timer.data = (unsigned long)sk; reset_xmit_timer(sk, TIME_WRITE, sk->rto); / Timer for repeating the SYN until an answer */ sk->retransmits = TCP_SYN_RETRIES; //前面地址信息,标识字段,查询路由表项等事务都已经完成了,那么就是发送连接请求数据包的时候了 //下面这个函数将转调用ip_queue_xmit 函数(ip层),这是个数据包发送函数 sk->prot->queue_xmit(sk, dev, buff, 0);
        reset_xmit_timer(sk, TIME_WRITE, sk->rto); tcp_statistics.TcpActiveOpens++; tcp_statistics.TcpOutSegs++; //那么下面就是一个数据包接收函数了,(可能有的名字已经占用了,就勉强用这个不相关的名字) //这个函数将内部调用 tcp_rcv 函数 release_sock(sk);//重新接收数据包 return(0); }

  • 上面函数最后调用了queue_xmit 函数(ip_queue_xmit 函数)和 release_sock 函数,进行数据包的发送和接收。

  • 另外,在inet_connect 函数中调用了 build_header 函数(ip层的 ip_build_header 函数),考虑到篇幅问题,我们将在下篇继续剖析。

5、网络层——ip_build_header 函数

  • tcp_connect 函数内部调用了 build_header函数,实则是ip层的 ip_build_header 函数,该函数的主要功能是创建合适的 mac和ip头部

    /*

    • This routine builds the appropriate hardware/IP headers for

    • the routine. It assumes that if *dev != NULL then the

    • protocol knows what it's doing, otherwise it uses the

    • routing/ARP tables to select a device struct. */ //创建合适的 mac/ip 首部 int ip_build_header(struct sk_buff *skb, unsigned long saddr, unsigned long daddr, struct device **dev, int type, struct options *opt, int len, int tos, int ttl) { static struct options optmem; struct iphdr *iph;//ip首部 struct rtable *rt;//ip路由表 unsigned char buff; unsigned long raddr; int tmp; unsigned long src; buff = skb->data;//得到数据部分(首部和有效负载) /

      • See if we need to look up the device. */ #ifdef CONFIG_INET_MULTICAST //多播处理 if(MULTICAST(daddr) && *dev==NULL && skb->sk && *skb->sk->ip_mc_name) *dev=dev_get(skb->sk->ip_mc_name); #endif //对dev初始化,并且获得下一站ip地址 if (*dev == NULL) { if(skb->localroute)//路由表查询 //该函数完成对本地链路上主机或者网络地址的路由查询工作 //查询就是对链表中每个元素进行检查,检查的根据就是对表项中目的地址和实际要发送数据包中的目的地址进行网络号(和子网络号)的比较 rt = ip_rt_local(daddr, &optmem, &src); else rt = ip_rt_route(daddr, &optmem, &src);//这个函数和上面那个类似 if (rt == NULL) { ip_statistics.IpOutNoRoutes++; return(-ENETUNREACH); } dev = rt->rt_dev;//路由路径出站的接口设备 /
        • If the frame is from us and going off machine it MUST MUST MUST
        • have the output device ip address and never the loopback / if (LOOPBACK(saddr) && !LOOPBACK(daddr))//回路检查 saddr = src;/rt->rt_dev->pa_addr;/ raddr = rt->rt_gateway;//下一站ip地址,网关或路由器地址 opt = &optmem; } else//已经指定了发送接口设备,仍需要进行路由表查询,寻找下一站ip地址 { /
        • We still need the address of the first hop. / if(skb->localroute) rt = ip_rt_local(daddr, &optmem, &src); else rt = ip_rt_route(daddr, &optmem, &src); /
        • If the frame is from us and going off machine it MUST MUST MUST
        • have the output device ip address and never the loopback / if (LOOPBACK(saddr) && !LOOPBACK(daddr))//回路检查 saddr = src;/rt->rt_dev->pa_addr;/ raddr = (rt == NULL) ? 0 : rt->rt_gateway;//下一站地址 } /
      • No source addr so make it our addr / //如果没有指定本地地址,就设置源端地址为本地接口地址 if (saddr == 0) saddr = src; /
      • No gateway so aim at the real destination / // if (raddr == 0) raddr = daddr; /
      • Now build the MAC header. */ //创建 MAC 头,返回MAC头部大小tmp tmp = ip_send(skb, raddr, len, dev, saddr); //MAC header | IP header | TCP header | payload buff += tmp;//buff指针偏移tmp,移到ip首部首地址 len -= tmp; /
      • Book keeping */ skb->dev = dev;//接口设备 skb->saddr = saddr;//源端ip地址 if (skb->sk) skb->sk->saddr = saddr;//本地地址 /
      • Now build the IP header. / /
      • If we are using IPPROTO_RAW, then we don't need an IP header, since
      • one is being supplied to us by the user */ if(type == IPPROTO_RAW) return (tmp); //获取ip首部,及初始化 iph = (struct iphdr )buff;//获取ip首部 iph->version = 4; iph->tos = tos; iph->frag_off = 0; iph->ttl = ttl; iph->daddr = daddr;//ip地址 iph->saddr = saddr; iph->protocol = type; iph->ihl = 5; skb->ip_hdr = iph; / Setup the IP options. / #ifdef Not_Yet_Avail build_options(iph, opt); #endif //普通的ip首部长为20个字节长 return(20 + tmp); / IP header plus MAC header size / } 内部调用了一个ip_send函数,用于创建填充MAC头部(这函数名取得。。) /
    • Take an skb, and fill in the MAC header. */ static int ip_send(struct sk_buff skb, unsigned long daddr, int len, struct device dev, unsigned long saddr) { int mac = 0; skb->dev = dev;//指定设备接口 skb->arp = 1; if (dev->hard_header) { / * Build a hardware header. Source address is our mac, destination unknown * (rebuild header will sort this out) / //创建mac 头部,调用下层函数 eth_header(eth.c) mac = dev->hard_header(skb->data, dev, ETH_P_IP, NULL, NULL, len, skb); if (mac < 0)//返回负值,表示创建未成功 { mac = -mac; skb->arp = 0;//设置arp为0,表示六安路曾首部中缺少下一站主机硬件地址 skb->raddr = daddr; / next routing address 数据包下一站ip地址/ } } return mac;//返回mac头部长度 }

6、链路层——eth_header 函数

  • 承接上面函数,完成创建MAC首部工作

    /*

    • Create the Ethernet MAC header for an arbitrary protocol layer
    • saddr=NULL means use device source address如果传值源地址为空,则使用设备的地址作为源地址
    • daddr=NULL means leave destination address (eg unresolved arpARP地址解析获得目的地址) */ //创建一个mac 头(链路层),并返回头部长度 int eth_header(unsigned char *buff, struct device *dev, unsigned short type, void *daddr, void *saddr, unsigned len, struct sk_buff *skb) { struct ethhdr *eth = (struct ethhdr )buff;//获得以太网头 /
      • Set the protocol type. For a packet of type ETH_P_802_3 we put the length

      • in here instead. It is up to the 802.2 layer to carry protocol information. / //设置协议类型 if(type!=ETH_P_802_3) eth->h_proto = htons(type); else eth->h_proto = htons(len); /

      • Set the source hardware address. / //源端地址设置 if(saddr) memcpy(eth->h_source,saddr,dev->addr_len); else//传参为空,则使用设备的地址作为源地址 memcpy(eth->h_source,dev->dev_addr,dev->addr_len); /

      • Anyway, the loopback-device should never use this function... */ //如果是一个回路网络,设置目的地址为空,不然信息会无终止传输,引起广播风暴 if (dev->flags & IFF_LOOPBACK) { memset(eth->h_dest, 0, dev->addr_len); return(dev->hard_header_len); } if(daddr)//设置目的地址,传参为NULL,即这里不会去设置目的地址 { memcpy(eth->h_dest,daddr,dev->addr_len); return dev->hard_header_len; } return -dev->hard_header_len;//返回负值,表示创建未成功 }

  • 至此,connect 函数基本上算是分析完了,中间涉及到数据包的发送与接收我们另外剖析。