分析完了服务器端,我们继续分析客户端,在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层的代码,后续再深入分析。