socket API与系统调用的关系在上篇文章中已经分析得很清楚https://www.cnblogs.com/wzzgeorge/p/12068455.html,那么本文主要深入TCP协议,分析connect及bind、listen、accept背后的三次握手机制。
一、TCP概述
1. TCP的概念
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(MTU)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
2. 三次握手的概念
握手过程中传送的包里不包含数据,三次握手完毕后,客户端不服务器才正式开始传送 数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主劢关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主劢发起断开TCP连接的请求。
- 第一次握手:建立连接。客户端发送连接 请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端迚入 SYN_SEND状态,等待服务器的确认。即 A发送信息给B。
- 第二次握手:服务器收到客户端的SYN报 文段,需要对这个SYN报文段迚行确认。 即B收到连接信息后向A返回确认信息。
- 第三次握手:客户端收到服务器的 (SYN+ACK)报文段,并向服务器发送 ACK报文段。即A收到确认信息后再次向B 返回确认连接信。
二、源代码分析
客户端请求连接服务器时,经历的三次握手从用户程序的角度看就是客户端connect和服务端accept建立起连接时背后完成的工作,上一篇文章说到,在内核socket接口层这两个socket API函数对应着sys_connect和sys_accept函数:
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args) { ...... switch (call) { case SYS_SOCKET: err = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = __sys_listen(a0, a1); break; case SYS_ACCEPT: err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break ......
进一步查看每个子函数:
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) { ...... err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,sock->file->f_flags); ...... } int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,int __user *upeer_addrlen, int flags) { err = sock->ops->accept(sock, newsock, sock->file->f_flags, false); } int __sys_listen(int fd, int backlog) { err = sock->ops->listen(sock, backlog);//如果是TCP套接字,sock->ops指向的是inet_stream_ops,sock->ops是在inet_create()函数中初始化,所以listen接口调用的是inet_listen()函数。 } int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen) { err = sock->ops->bind(sock,(struct sockaddr *)&address, addrlen); }
在文件/net/ipv4/tcp_ipv4.c文件中的结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数,例如__sys_accept4,SOCK_STREAM套接口的TCP层操作函数集实例为tcp_prot,对应连接的接收函数为inet_csk_accept()。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
......
}
当创建了TCP协议套接字后,客户端再调用connect()函数请求建立TCP链接。该函数通过系统调用触发tcp_v4_connect函数的执行,产生一个包含SYN标志和一个32位的序号的连接请求包,并发送给服务器端。 这是TCP三次握手的第一步。
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { struct inet_opt *inet = inet_sk(sk); //变量inet指向套接字struct sock中的inet选项 struct tcp_opt *tp = tcp_sk(sk); //变量tp指向套接字struct sock中的TCP选项 struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; //将通用地址结构转换为IPv4的地址结构 struct rtable *rt; //记录路由表项 u32 daddr, nexthop; //daddr记录目的地址,nesthop记录下一条地址 int tmp; int err; //判断地址长度是否合法,应该要大于或者等于其地址的长度 if (addr_len < sizeof(struct sockaddr_in)) return -EINVAL; //判断是否为inet协议族 if (usin->sin_family != AF_INET) return -EAFNOSUPPORT; //初始化目的地址和下一条地址 nexthop = daddr = usin->sin_addr.s_addr; if (inet->opt && inet->opt->srr) { if (!daddr) return -EINVAL; nexthop = inet->opt->faddr; } //查找路由表项,并通过变量rt记录下来 tmp = ip_route_connect(&rt, nexthop, inet->saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, inet->sport, usin->sin_port, sk); if (tmp < 0) return tmp; if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { ip_rt_put(rt); return -ENETUNREACH; } if (!inet->opt || !inet->opt->srr) daddr = rt->rt_dst;//如果源地址为0,则把rt_ src赋给源地址
if (!inet->saddr) inet->saddr = rt->rt_src; inet->rcv_saddr = inet->saddr; //初始化TCP选项 if (tp->ts_recent_stamp && inet->daddr != daddr) { /* Reset inherited state */ tp->ts_recent = 0;//Time stamp to echo next tp->ts_recent_stamp = 0;//Time we stored ts_recent (for aging) tp->write_seq = 0;//序号初始化为0 } if (sysctl_tcp_tw_recycle && !tp->ts_recent_stamp && rt->rt_dst == daddr) { struct inet_peer *peer = rt_get_peer(rt); if (peer && peer->tcp_ts_stamp + TCP_PAWS_MSL >= xtime.tv_sec) { tp->ts_recent_stamp = peer->tcp_ts_stamp; tp->ts_recent = peer->tcp_ts; } } inet->dport = usin->sin_port;//目的端口地址 inet->daddr = daddr;//目的IP地址 tp->ext_header_len = 0; if (inet->opt) tp->ext_header_len = inet->opt->optlen;//inet的opt选项的长度 tp->mss_clamp = 536; //把套接字结构sk的状态置为TCP_SYN_SENT tcp_set_state(sk, TCP_SYN_SENT); //为套接字绑定一个端口,并记录在TCP的哈希表中 err = tcp_v4_hash_connect(sk); if (err) goto failure; err = ip_route_newports(&rt, inet->sport, inet->dport, sk); if (err) goto failure; /* OK, now commit destination to socket. */ //设置套接字的路由出口信息 __sk_dst_set(sk, &rt->u.dst); tcp_v4_setup_caps(sk, &rt->u.dst); tp->ext2_header_len = rt->u.dst.header_len; //生成一个序号 if (!tp->write_seq) tp->write_seq = secure_tcp_sequence_number(inet->saddr, inet->daddr, inet->sport, usin->sin_port); inet->id = tp->write_seq ^ jiffies; //调用tcp_connect(sk)函数,为请求包设置SYN标志,并发出请求包 err = tcp_connect(sk); rt = NULL;//rt指针指向内存0开始处 if (err) goto failure; return 0; failure: //如果连接失败,则需把套接字状态设置为:TCP_CLOSE /* This unhashes the socket and releases the local port, if necessary. */ tcp_set_state(sk, TCP_CLOSE); ip_rt_put(rt); sk->sk_route_caps = 0;//sk_route_caps,网络驱动特征标志 inet->dport = 0; return err;
}
而另一头服务端调用inet_csk_accept函数会请求队列中取出一个连接请求,如果队列为空则通过inet_csk_wait_for_connect阻塞住等待客户端的连接。
/********************************************** sk:监听sock flags:这些是文件标志, 例如 O_NONBLOCK err:传出参数 用于接收错误 ******************************************************/ struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) { struct inet_connection_sock *icsk = inet_csk(sk); struct sock *newsk; int error; //获取sock锁将sk->sk_lock.owned设置为1 //此锁用于进程上下文和中断上下文 lock_sock(sk); /* We need to make sure that this socket is listening, * and that it has something pending. */ //用于accept的sock必须处于监听状态 error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; /* Find already established connection */ //在监听套接字上的连接队列如果为空 if (reqsk_queue_empty(&icsk->icsk_accept_queue)) { //设置接收超时时间,若调用accept的时候设置了O_NONBLOCK,表示马上返回不阻塞进程 long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); /* If this is a non blocking socket don't sleep */ error = -EAGAIN; if (!timeo)//如果是非阻塞模式timeo为0 则马上返回 goto out_err; //将进程阻塞,等待连接的完成 error = inet_csk_wait_for_connect(sk, timeo); if (error)//返回值为0说明监听套接字的完全建立连接队列不为空 goto out_err; } //在监听套接字建立连接的队列中删除此request_sock连接项 并返回建立连接的sock //三次握手的完成是在tcp_v4_rcv中完成的 newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk); //此时sock的状态应为TCP_ESTABLISHED WARN_ON(newsk->sk_state == TCP_SYN_RECV); out: release_sock(sk); return newsk; out_err: newsk = NULL; *err = error; goto out; }