【Linux 內核網絡協議棧源碼剖析】connect 函數剖析(一)

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;//設置狀態字段,表示正在連接過程中
	}
	//這個狀態下,這是關閉信號。各個狀態描述,參考下面鏈接
	http://blog.csdn.net/wenqian1991/article/details/40110703
	//清楚,這裏有兩個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 函數),考慮到篇幅問題,我們將在下篇繼續剖析。










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章