【Linux 内核网络协议栈源码剖析】connect 函数剖析(一)
linux-dash
A beautiful web dashboard for Linux
项目地址:https://gitcode.com/gh_mirrors/li/linux-dash
免费下载资源
·
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 函数),考虑到篇幅问题,我们将在下篇继续剖析。
GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e
added ecosystem file for PM2 4 年前
5def40a3
Add host customization support for the NodeJS version 4 年前
更多推荐
已为社区贡献8条内容
所有评论(0)