深入理解TCP/IP协议的实现之connect(基于linux1.2.13)

分析完了服务器端,我们继续分析客户端,在socket编程中,客户端的流程是比较简单的,申请一个socket,然后调connect去发起连接就行。我们先看一下connect函数的定义。

/*
	socket 通过socket函数申请的结构体
	address 需要连接的目的地地址信息
*/
int connect(int socket, const struct sockaddr *address,socklen_t address_len);

我们通过层层调用揭开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);
	if (!(sock = sockfd_lookup(fd, &file)))
		return(-ENOTSOCK);

	i = sock->ops->connect(sock, (struct sockaddr *)address, addrlen, file->f_flags);
	if (i < 0) 
	{
		return(i);
	}
	return(0);
}

没有太多逻辑,通过fd找到关联的socket结构体。然后调底层函数。底层的函数是inet_connect,这个函数逻辑比较多,我们分开分析。

	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) 
	{
		// 如果绑过就不需要绑了
		if(inet_autobind(sk)!=0)
			return(-EAGAIN);
		// 调用底层的连接函数,发一个syn包
		err = sk->prot->connect(sk, (struct sockaddr_in *)uaddr, addr_len);
		if (err < 0) 
			return(err);
		// 发送成功设置状态为连接中
  		sock->state = SS_CONNECTING;
	}

继续调用底层的函数,这里是tcp,所以是发送一个sync包(一会分析)。然后把socket状态修改为连接中。

	if (sk->state != TCP_ESTABLISHED &&(flags & O_NONBLOCK)) 
	  	return(-EINPROGRESS);

还没建立连接成功并且是非阻塞的方式,直接返回。

	// 连接建立中,阻塞当前进程
	while(sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) 
	{	
		// 可中断式睡眠,即可被信号唤醒
		interruptible_sleep_on(sk->sleep);
		// 被唤醒后,判断是因为被信号唤醒的还是因为建立建立了。
		if (current->signal & ~current->blocked) 
		{
			sti();
			return(-ERESTARTSYS);
		}
		// 连接失败
		if(sk->err && sk->protocol == IPPROTO_TCP)
		{
			sti();
			sock->state = SS_UNCONNECTED;
			err = -sk->err;
			sk->err=0;
			return err; /* set by tcp_err() */
		}
	}

connect的时候如果没有设置阻塞标记,则进程会被挂起。tcp层建立连接后会唤醒进程。

// 连接建立
	sock->state = SS_CONNECTED;

	if (sk->state != TCP_ESTABLISHED && sk->err) 
	{
		sock->state = SS_UNCONNECTED;
		err=sk->err;
		sk->err=0;
		return(-err);
	}

最后被连接建立唤醒后,设置socket的状态。connect就完成了。

下面我们看一下tcp层的connect的实现,其实就是从客户端视角看三次握手的过程。代码比较多,只看一下核心的。

 
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;
	struct rtable *rt;

	if (usin->sin_family && usin->sin_family != AF_INET) 
		return(-EAFNOSUPPORT);

  	// 不传ip则取本机ip
  	if(usin->sin_addr.s_addr==INADDR_ANY)
		usin->sin_addr.s_addr=ip_my_addr();
	// 禁止广播和多播
	if ((atype=ip_chk_addr(usin->sin_addr.s_addr)) == IS_BROADCAST || atype==IS_MULTICAST) 
		return -ENETUNREACH;
  
	sk->inuse = 1;
	// 连接的远端地址
	sk->daddr = usin->sin_addr.s_addr;
	// 第一个字节的序列号
	sk->write_seq = tcp_init_seq();
	sk->window_seq = sk->write_seq;
	sk->rcv_ack_seq = sk->write_seq -1;
	sk->err = 0;
	// 远端端口
	sk->dummy_th.dest = usin->sin_port;
	release_sock(sk);
	// 分配一个skb
	buff = sk->prot->wmalloc(sk,MAX_SYN_SIZE,0, GFP_KERNEL);
	sk->inuse = 1;
	// tcp头和选项,告诉对方自己的接收窗口大小1
	buff->len = 24;
	buff->sk = sk;
	buff->free = 0;
	buff->localroute = sk->localroute;
	t1 = (struct tcphdr *) buff->data;
	// 查找路由
	rt=ip_rt_route(sk->daddr, NULL, NULL);
	// 构建ip和mac头
	tmp = sk->prot->build_header(buff, sk->saddr, sk->daddr, &dev,
					IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl);
	buff->len += tmp;
	t1 = (struct tcphdr *)((char *)t1 +tmp);
	memcpy(t1,(void *)&(sk->dummy_th), sizeof(*t1));
	// 序列号为初始化的序列号
	t1->seq = ntohl(sk->write_seq++);
	// 下一个数据包中第一个字节的序列号 
	sk->sent_seq = sk->write_seq;
	buff->h.seq = sk->write_seq;
	t1->ack = 0;
	t1->window = 2;
	t1->res1=0;
	t1->res2=0;
	t1->rst = 0;
	t1->urg = 0;
	t1->psh = 0;
	// 是一个syn包
	t1->syn = 1;
	t1->urg_ptr = 0;
	// TCP头包括24个字节,因为还有4个字节的选项
	t1->doff = 6;
	
	// 执行tcp头后面的第一个字节
	ptr = (unsigned char *)(t1+1);
	// 选项的类型是2,通知对方TCP报文中数据部分的最大值
	ptr[0] = 2;
	// 选项内容长度是4个字节
	ptr[1] = 4;
	// 组成MSS
	ptr[2] = (sk->mtu) >> 8;
	ptr[3] = (sk->mtu) & 0xff;
	// tcp头的校验和
	tcp_send_check(t1, sk->saddr, sk->daddr,sizeof(struct tcphdr) + 4, sk);

	// 设置套接字为syn_send状态
	tcp_set_state(sk,TCP_SYN_SENT);
	// 设置数据包往返时间需要的时间
	sk->rto = TCP_TIMEOUT_INIT;
	// 设置超时回调
	sk->retransmit_timer.function=&retransmit_timer;
	sk->retransmit_timer.data = (unsigned long)sk;
	// 设置超时时间
	reset_xmit_timer(sk, TIME_WRITE, sk->rto);	
	// 设置syn包的重试次数
	sk->retransmits = TCP_SYN_RETRIES;
	// 发送
	sk->prot->queue_xmit(sk, dev, buff, 0);  
	reset_xmit_timer(sk, TIME_WRITE, sk->rto);
	release_sock(sk);
	return(0);
}

代码很长,主要是构建一个sync包发出去。在这个代码里我们大概能看到tcp协议的相关实现。上面的代码完成了第一次握手。下面再看一下第二次握手的代码。

		// 发送了syn包
		if(sk->state==TCP_SYN_SENT)
		{
			// 发送了syn包,收到ack包说明可能是建立连接的ack包
			if(th->ack)
			{
				// 尝试连接但是对端回复了重置包
				if(th->rst)
					return tcp_std_reset(sk,skb);
				// 建立连接的回包 
				syn_ok=1;	
				// 期待收到对端下一个的序列号
				sk->acked_seq=th->seq+1;
				sk->fin_seq=th->seq;
				// 发送第三次握手的ack包,进入连接建立状态
				tcp_send_ack(sk->sent_seq,sk->acked_seq,sk,th,sk->daddr);
				tcp_set_state(sk, TCP_ESTABLISHED);
				// 解析tcp选项
				tcp_options(sk,th);
				// 记录对端地址
				sk->dummy_th.dest=th->source;
				// 可以读取但是还没读取的序列号
				sk->copied_seq = sk->acked_seq;
				// 唤醒阻塞在connect函数的进程
				if(!sk->dead)
				{
					sk->state_change(sk);
					sock_wake_async(sk->socket, 0);
				}
				
			}
		}

上面代码完成了第二次握手。tcp_send_ack完成第三次握手。这里不打算深入分析tcp层的代码,后续再深入分析。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值