从Linux源码看Socket(TCP)Client端的Connect的示例详解

前言

笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情。
今天笔者就来从Linux源码的角度看下Client端的Socket在进行Connect的时候到底做了哪些事情。由于篇幅原因,关于Server端的Accept源码讲解留给下一篇博客。
(基于Linux 3.10内核)

一个最简单的Connect例子

int clientSocket;
if((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
	// 创建socket失败失败
 	return -1;
}
......
if(connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
	// connect 失败
	return -1;
}
.......

首先我们通过socket系统调用创建了一个socket,其中指定了SOCK_STREAM,而且最后一个参数为0,也就是建立了一个通常所有的TCP Socket。在这里,我们直接给出TCP Socket所对应的ops也就是操作函数。

如果你想知道上图中的结构是怎么来的,可以看下笔者以前的文章:

https://www.jb51.net/article/106563.htm

值得注意的是,由于socket系统调用操作做了如下两个代码的判断

sock_map_fd
	|->get_unused_fd_flags
			|->alloc_fd
				|->expand_files (ulimit)
	|->sock_alloc_file
		|->alloc_file
			|->get_empty_filp (/proc/sys/fs/max_files)

第一个判断,ulmit超限:

int expand_files(struct files_struct *files, int nr
{
	......
	if (nr >= current->signal->rlim[RLIMIT_NOFILE].rlim_cur)
		return -EMFILE;
	......
}

这边的判断即是ulimit的限制!在这里返回-EMFILE对应的描述就是
"Too many open files"

第二个判断max_files超限

struct file *get_empty_filp(void)
{
 ......
	/*
	 * 由此可见,特权用户可以无视文件数最大大小的限制!
	 */
	if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) {
		/*
		 * percpu_counters are inaccurate. Do an expensive check before
		 * we go and fail.
		 */
		if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
			goto over;
	}

 ......
}

所以在文件描述符超过所有进程能打开的最大文件数量限制(/proc/sys/fs/file-max)的时候会返回-ENFILE,对应的描述就是"Too many open files in system",但是特权用户确可以无视这一限制,如下图所示:

connect系统调用

我们再来看一下connect系统调用:

int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen)

这个系统调用有三个参数,那么依据规则,它肯定在内核中的源码长下面这个样子

SYSCALL_DEFINE3(connect, ......

笔者全文搜索了下,就找到了具体的实现:

socket.c
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
		int, addrlen)
{
 ......
	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);
	......
}

前面图给出了在TCP下的sock->ops == inet_stream_ops,然后再陷入到更进一步的调用栈中,即下面的:

SYSCALL_DEFINE3(connect
	|->inet_stream_ops
		|->inet_stream_connect
			|->tcp_v4_connect
				|->tcp_set_state(sk, TCP_SYN_SENT);设置状态为TCP_SYN_SENT
			 	|->inet_hash_connect
				|->tcp_connect

首先,我们来看一下inet_hash_connect这个函数,里面有一个端口号的搜索过程,搜索不到可用端口号就会导致创建连接失败!内核能够建立一个连接也是跋涉了千山万水的!我们先看一下搜索端口号的逻辑,如下图所示:

获取端口号范围

首先,我们从内核中获取connect能够使用的端口号范围,在这里采用了Linux中的顺序锁(seqlock)

void inet_get_local_port_range(int *low, int *high)
{
	unsigned int seq;

	do {
		// 顺序锁
		seq = read_seqbegin(&sysctl_local_ports.lock);

		*low = sysctl_local_ports.range[0];
		*high = sysctl_local_ports.range[1];
	} while (read_seqretry(&sysctl_local_ports.lock, seq));
}

顺序锁事实上就是结合内存屏障等机制的一种乐观锁,主要依靠一个序列计数器。在读取数据之前和之后,序列号都被读取,如果两者的序列号相同,说明在读操作的时候没有被写操作打断过。
这也保证了上面的读取变量都是一致的,也即low和high不会出现low是改前值而high是改后值得情况。low和high要么都是改之前的,要么都是改之后的!内核中修改的地方为:

cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000 

通过hash决定端口号起始搜索范围

在Linux上进行connect,内核给其分配的端口号并不是线性增长的,但是也符合一定的规律。
先来看下代码:

int __inet_hash_connect(...)
{
		// 注意,这边是static变量
		static u32 hint;
		// 这边的port_offset是用对端ip:port hash的一个值
		// 也就是说对端ip:port固定,port_offset固定
		u32 offset = hint + port_offset;
		for (i = 1; i <= remaining; i++) {
			port = low + (i + offset) % remaining;
			/* port是否占用check */
			....
			goto ok;
		}
		.......
ok:
		hint += i;
		......
}

这里面有几个小细节,为了安全原因,Linux本身用对端ip:port做了一次hash作为搜索的初始offset,所以不同远端ip:port初始搜索范围可以基本是不同的!但同样的对端ip:port初始搜索范围是相同的!

在笔者机器上,一个完全干净的内核里面,不停的对同一个远端ip:port,其以2进行稳定增长,也即38742->38744->38746,如果有其它的干扰,就会打破这个规律。

端口号范围限制

由于我们指定了端口号返回ip_local_port_range是不是就意味着我们最多创建high-low+1个连接呢?当然不是,由于检查端口号是否重复是将(网络命名空间,对端ip,对端port,本端port,Socket绑定的dev)当做唯一键进行重复校验,所以限制仅仅是在同一个网络命名空间下,连接同一个对端ip:port的最大可用端口号数为high-low+1,当然可能还要减去ip_local_reserved_ports。如下图所示:

检查端口号是否被占用

端口号的占用搜索分为两个阶段,一个是处于TIME_WAIT状态的端口号搜索,另一个是其它状态端口号搜索。

TIME_WAIT状态端口号搜索

众所周知,TIME_WAIT阶段是TCP主动close必经的一个阶段。如果Client采用短连接的方式和Server端进行交互,就会产生大量的TIME_WAIT状态的Socket。而这些Socket由占用端口号,所以当TIME_WAIT过多,打爆上面的端口号范围之后,新的connect就会返回错误码:

C语言connect返回错误码为
-EADDRNOTAVAIL,对应描述为Cannot assign requested address
对应Java的异常为
java.net.NoRouteToHostException: Cannot assign requested address (Address not available)

ip_local_reserved_ports。如下图所示:

由于TIME_WAIT大概一分钟左右才能消失,如果在一分钟内Client端和Server建立大量的短连接请求就容易导致端口号耗尽。而这个一分钟(TIME_WAIT的最大存活时间)是在内核(3.10)编译阶段就确定了的,无法通过内核参数调整。 如下代码所示:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
				 * state, about 60 seconds	*/

Linux自然也考虑到了这种情况,所以提供了一个tcp_tw_reuse参数使得在搜索端口号时可以在某些情况下重用TIME_WAIT。代码如下:

__inet_hash_connect
	|->__inet_check_established
static int __inet_check_established(......)
{
	......
	/* Check TIME-WAIT sockets first. */
	sk_nulls_for_each(sk2, node, &head->twchain) {
		tw = inet_twsk(sk2);
		// 如果在time_wait中找到一个match的port,就判断是否可重用
		if (INET_TW_MATCH(sk2, net, hash, acookie,
					saddr, daddr, ports, dif)) {
			if (twsk_unique(sk, sk2, twp))
				goto unique;
			else
				goto not_unique;
		}
	}
	......
}

如上面代码中写的那样,如果在一堆TIME-WAIT状态的Socket里面能够有当前要搜索的port,则判断是否这个port可以重复利用。如果是TCP的话这个twsk_unique的实现函数是:

int tcp_twsk_unique(......)
{
	......
	if (tcptw->tw_ts_recent_stamp &&
	 (twp == NULL || (sysctl_tcp_tw_reuse &&
			 get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
		tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2
		......
		return 1;
	}
	return 0;
}

上面这段代码逻辑如下所示:

在开启了tcp_timestamp以及tcp_tw_reuse的情况下,在Connect搜索port时只要比之前用这个port的TIME_WAIT状态的Socket记录的最近时间戳>1s,就可以重用此port,即将之前的1分钟缩短到1s。同时为了防止潜在的序列号冲突,直接将write_seq加上在65537,这样,在单Socket传输速率小于80Mbit/s的情况下,不会造成序列号冲突。
同时这个tw_ts_recent_stamp设置的时机如下图所示:

所以如果Socket进入TIME_WAIT状态后,如果一直有对应的包发过来,那么会影响此TIME_WAIT对应的port是否可用的时间。我们可以通过下面命令开始tcp_tw_reuse:

echo '1' > /proc/sys/net/ipv4/tcp_tw_reuse

ESTABLISHED状态端口号搜索

ESTABLISHED的端口号搜索就简单了许多

/* And established part... */
	sk_nulls_for_each(sk2, node, &head->chain) {
		if (INET_MATCH(sk2, net, hash, acookie,
					saddr, daddr, ports, dif))
			goto not_unique;
	}

以(网络命名空间,对端ip,对端port,本端port,Socket绑定的dev)当做唯一键进行匹配,如果匹配成功,表明此端口无法重用。

端口号迭代搜索

Linux内核在[low,high]范围按照上述逻辑进行port的搜索,如果没有搜索到port,即port耗尽,就会返回-EADDRNOTAVAIL,也即Cannot assign requested address。但还有一个细节,如果是重用TIME_WAIT状态的Socket的端口的话,就会将对应的TIME_WAIT状态的Socket给销毁。

__inet_hash_connect(......)
{
		......
		if (tw) {
			inet_twsk_deschedule(tw, death_row);
			inet_twsk_put(tw);
		}
		......
}

寻找路由表

在我们找到一个可用端口号port后,就会进入搜寻路由阶段:

ip_route_newports
	|->ip_route_output_flow
			|->__ip_route_output_key
				|->ip_route_output_slow
					|->fib_lookup

这也是一个非常复杂的过程,限于篇幅,就不做详细阐述了。如果搜索不到路由信息的话,会返回。

-ENETUNREACH,对应描述为Network is unreachable

Client端的三次握手

在前面一大堆前置条件就绪后,才进入到真正的三次握手阶段。

tcp_connect
 |->tcp_connect_init 初始化tcp socket
 |->tcp_transmit_skb 发送SYN包
 |->inet_csk_reset_xmit_timer 设置SYN重传定时器

tcp_connect_init初始化了一大堆TCP相关的设置,例如mss_cache/rcv_mss等一大堆。而且如果开启了TCP窗口扩大选项的话,其窗口扩大因子也在此函数里进行计算:

tcp_connect_init
	|->tcp_select_initial_window
int tcp_select_initial_window(...)
{
	......
	(*rcv_wscale) = 0;
	if (wscale_ok) {
		/* Set window scaling on max possible window
		 * See RFC1323 for an explanation of the limit to 14
		 */
		space = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);
		space = min_t(u32, space, *window_clamp);
		while (space > 65535 && (*rcv_wscale) < 14) {
			space >>= 1;
			(*rcv_wscale)++;
		}
	}
	......
}	

如上面代码所示,窗口扩大因子取决于Socket最大可允许的读缓冲大小和window_clamp(最大允许滑动窗口大小,动态调整)。搞完了一票初始信息设置后,才开始真正的三次握手。
在tcp_transmit_skb中才真正发送SYN包,同时在紧接着的inet_csk_reset_xmit_timer里设置了SYN超时定时器。如果对端一直不发送SYN_ACK,将会返回-ETIMEDOUT。

重传的超时时间和

/proc/sys/net/ipv4/tcp_syn_retries

息息相关,Linux默认设置为5,建议设置成3,下面是不同设置的超时时间参照图。

在设置了SYN超时重传定时器后,tcp_connnect就返回,并一路返回到最初始的inet_stream_connect。在这里我们就等待对端返回SYN_ACK或者SYN定时器超时。

int __inet_stream_connect(struct socket *sock,...,)
{
	// 如果设置了O_NONBLOCK则timeo为0
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
	......
	// 如果timeo=0即O_NONBLOCK会立刻返回
	// 否则等待timeo时间
	if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
		goto out;
}

Linux本身提供一个SO_SNDTIMEO来控制对connect的超时,不过Java并没有采用这个选项。而是采用别的方式进行connect的超时控制。仅仅就C语言的connect系统调用而言,不设置SO_SNDTIMEO,就会将对应用户进程进行睡眠,直到SYN_ACK到达或者超时定时器超时才将次用户进程唤醒。

如果是NON_BLOCK的话,则是通过select/epoll等多路复用机制去捕获超时或者连接成功事件。

对端SYN_ACK到达

在Server端SYN_ACK到达之后会按照下面的代码路径传递,并唤醒用户态进程:

tcp_v4_rcv
	|->tcp_v4_do_rcv
		|->tcp_rcv_state_process
			|->tcp_rcv_synsent_state_process
				|->tcp_finish_connect
					|->tcp_init_metrics 初始化度量统计
					|->tcp_init_congestion_control 初始化拥塞控制
					|->tcp_init_buffer_space 初始化buffer空间
					|->inet_csk_reset_keepalive_timer 开启包活定时器
					|->sk_state_change(sock_def_wakeup) 唤醒用户态进程
				|->tcp_send_ack 发送三次握手的最后一次握手给Server端
			|->tcp_set_state(sk, TCP_ESTABLISHED) 设置为ESTABLISHED状态

总结

Client(TCP)端进行Connect的过程真是跋山涉水,从一开始文件描述符的限制到端口号的搜索再到路由表的搜索再到最后的三次握手,任何一个环节有问题就会导致创建连接失败,笔者详细的描述了这些机制的源码实现。希望本篇文章可以对读者在以后遇到Connect失败问题时候有所帮助。

到此这篇关于从Linux源码看Socket(TCP)Client端的Connect的文章就介绍到这了,更多相关Linux源码看Socket内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言实现Linux下的socket文件传输实例

    本文实例讲述了C语言实现Linux下的socket文件传输.分享给大家供大家参考.具体如下: server.c如下: //////////////////////////////////// //服务器代码 /////////////////////////////////// //本文件是服务器的代码 #include <netinet/in.h> // for sockaddr_in #include <sys/types.h> // for socket #include &

  • Linux网络编程之UDP Socket程序示例

    在网络传输协议中,TCP协议提供的是一种可靠的,复杂的,面向连接的数据流(SOCK_STREAM)传输服务,它通过三段式握手过程建立连接.TCP有一种"重传确认"机制,即接收端收到数据后要发出一个肯定确认的信号,发送端如果收到接收端肯定确认的信号,就会继续发送其他的数据,如果没有,它就会重新发送. 相对而言,UDP协议则是一种无连接的,不可靠的数据报(SOCK_DGRAM)传输服务.使用UDP套接口不用建立连接,服务端在调用socket()生成一个套接字并调用bind()绑定端口后就可

  • Linux 出现telnet: 127.0.0.1: Connection refused错误解决办法

    Linux 出现telnet: connect to address 127.0.0.1: Connection refused错误解决办法 没有xinetd服务: 1./etc/init.d目录中放置了系统中各个daemon服务的脚本,xinetd是其中之一. 2.xinetd是一种特殊的daemon服务(super daemon),它本身管理了一系列的daemon服务,这些服务只有在用户调用时才由xinetd启动,它们启动速度稍慢于独立的daemon服务,这些服务在/etc/xinetd.c

  • linux下开启php的sockets扩展支持实例

    下个相同版本的php源码,进行编译安装,再按照上面步骤搞,生成的so.copy到rpm装的那个,修改php.ini进行扩展就行了, 或者到网上找相同版本,相同系统 的编译好的so文件. 在linux下给PHP安装socket扩展,参考方法如下: #cd /usr/soft/php/ext/sockets (进入原php安装文件下的sockets目录) #/usr/local/php/bin/phpize (运行安装后的php安装文件下的phpize) #./configure --prefix=

  • Linux UDP socket 设置为的非阻塞模式与阻塞模式区别

    Linux UDP socket 设置为的非阻塞模式与阻塞模式区别 UDP socket 设置为的非阻塞模式 Len = recvfrom(SocketFD, szRecvBuf, sizeof(szRecvBuf), MSG_DONTWAIT, (struct sockaddr *)&SockAddr,&ScokAddrLen); UDP socket 设置为的阻塞模式 Len = recvfrom(SocketFD, szRecvBuf, sizeof(szRecvBuf), 0, (

  • 详解Linux的SOCKET编程

    本篇文章对Linux的SOCKET编程进行了详细解释,文章后面分享了一个编程实例供大家学习. 1. 网络中进程之间如何通信 进程通信的概念最初来源于单机系统.由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如 UNIX BSD有:管道(pipe).命名管道(named pipe)软中断信号(signal) UNIX system V有:消息(message).共享存储区(shared memory)和信号量(semap

  • linux中高并发socket最大连接数的优化详解

    首先我们可以通过ulimit –a命令来查看系统的一些资源限制情况,如下: # ulimit -a core file size (blocks, -c) 1024 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 127422 max locked memory (kbytes, -l) 64 max memo

  • linux上TCP connection timeout问题解决办法

     linux上TCP connection timeout问题解决办法 最近在产线上经常出现connection timeout的问题,先看看Java 中关于connection timeout 的异常如何产生 JAVA中的timeout java.net.SocketTimeoutException: connect timed out 客户端异常:connect timed out at java.net.PlainSocketImpl.socketConnect(Native Method

  • 从Linux源码看Socket(TCP)Client端的Connect的示例详解

    前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看下Client端的Socket在进行Connect的时候到底做了哪些事情.由于篇幅原因,关于Server端的Accept源码讲解留给下一篇博客. (基于Linux 3.10内核) 一个最简单的Connect例子 int clientSocket; if((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

  • 详解从Linux源码看Socket(TCP)的bind

    目录 一.一个最简单的Server端例子 二.bind系统调用 2.1.inet_bind 2.2.inet_csk_get_port 三.判断端口号是否冲突 四.SO_REUSEADDR和SO_REUSEPORT 五.SO_REUSEADDR 六.SO_REUSEPORT 七.总结 一.一个最简单的Server端例子 众所周知,一个Server端Socket的建立,需要socket.bind.listen.accept四个步骤. 代码如下: void start_server(){ // se

  • Python Socket TCP双端聊天功能实现过程详解

    SOCKET编程 socket(套接字):是一个网络通信的端点,能实现不同主机的进程通信, -通过IP+端口定位对方并发送消息的通信机制 分为UDP和TCP 客户端Client: 发起访问的一-方 服务器端Server: 接受访问的一方 UDP编程 Server端流程 1.建立socket,socket是负贵具体通信的一个实例 2.绑定,为创建的socket指派固定的端口和ip地址 3.接受对方发送内容 4.给对方发送反馈,此步骤为非必须步骤 Client端流程 1.建立通信的socket 2.

  • 分析从Linux源码看TIME_WAIT的持续时间

    目录 一.前言 二.首先介绍下Linux环境 三.TIME_WAIT状态转移图 四.持续时间真如TCP_TIMEWAIT_LEN所定义么? 五.TIME_WAIT定时器源码 5.1.inet_twsk_schedule 5.2.具体的清理函数 5.3.先作出一个假设 5.4.如果一个slot中的TIME_WAIT<=100 5.5.如果一个slot中的TIME_WAIT>100 5.6.PAWS(Protection Against Wrapped Sequences)使得TIME_WAIT延

  • php源码之将图片转化为data/base64数据流实例详解

    php源码之将图片转化为data/base64数据流 这里我们分享一个将图片转换为base64编码格式的方法: <?php $img = 'test.jpg'; $base64_img = base64EncodeImage($img); echo '<img src="' . $base64_img . '" />'; /* 作者:http://www.manongjc.com */ function base64EncodeImage ($image_file)

  • 通过字节码看java中this的隐式传参详解

    前言 从字节码看java中 this 隐式传参具体体现(和python中的self如出一辙,但是比python中藏得更深),也发现了 static 与 非 static 方法的区别所在! static与非static方法都是存储java的方法区.在static 方法中,没有this引用,因此无法使用当前类中所定义的变量,而非static方法则会默认传入this. 概述 this关键字,是一个隐式参数,另外一个隐式参数是super. this用于方法里面,用于方法外面无意义. this关键字一般用

  • Nodejs之TCP服务端与客户端聊天程序详解

    TCP是用来计算机之间进行通信的,通过编写客户端和服务端聊天的代码,对于服务器与客户端的工作步骤有了深刻的了解,在这里根据了不起的Node.js一书来总结一下. TCP聊天程序需要用到Nodejs中的net模块,net模块是对TCP的封装,用于创建TCP服务端与客户端的. 服务器端 count:连接的客户端个数: users:用于存储客户端用户昵称的对象,对象值都为该客户端的Socket,Socket是一个接口,用于服务端与客户端通信. net.createServer:是用来创建TCP服务器,

  • node.js中TCP Socket多进程间的消息推送示例详解

    前言 前段时间接到了一个支付中转服务的需求,即支付数据通过http接口传到中转服务器,中转服务器将支付数据发送到异构后台(Lua)的指定tcp socket. 一开始评估的时候感觉蛮简单的,就是http server和tcp server间的通信,不是一个Event实例就能解决的状态管理问题吗?注册一个事件A用于消息传递,在socket连接时注册唯一的ID,然后在http接收到数据时,emit事件A:在监听到事件A时,在tcp server中寻找指定ID对应的socket处理该数据即可. 尽管n

  • 解析Linux源码之epoll

    目录 一.前言 二.简单的epoll例子 2.1.epoll_create 2.2.struct eventpoll 2.3.epoll_ctl(add) 2.4.ep_insert 2.5.tfile->f_op->poll的实现 2.6.回调函数的安装 2.7.epoll_wait 2.8.ep_send_events 三.事件到来添加到epoll就绪队列(rdllist)的过程 3.1.可读事件到来 3.2.可写事件到来 四.关闭描述符(close fd) 五.总结 一.前言 在linu

  • 解析从小程序开发者工具源码看原理实现

    如何查看小程序开发者工具源码 下面我们通过微信小程序开发者工具的源码来说说小程序的底层实现原理.以开发者工具版本号State v1.02.1904090的源码来窥探小程序的实现思路.如何查看微信源码,对于mac用户而言,查看微信小程序开发者工具的包内容,然后进入Contents/Resources/app.nw/js/core/index.js,注释掉如下代码就可以查看开发者工具渲染后的代码. // 打开 inspect 窗口 if (nw.App.argv.indexOf('inspect')

随机推荐