4-从tcp连接建立的角度分析connect函数

  本文主要是从tcp连接建立的角度来分析客户端程序是如何调用connect函数和服务端程序建立tcp连接的,了解connect函数在建立连接的过程中做了那些事情。


1. 了解tcp连接建立过程

  在正式介绍connect函数时,我们先来看一下tcp三次握手的过程,下面这个实验是通过客户端通过telnet远程登录服务端为例,因为telnet协议是基于tcp协议的正好我们可以通过wireshark抓包工具看到客户端和服务端之间三次握手的过程,12.1.1.1是客户端的ip地址,12.1.1.2是服务端的ip地址。

这里写图片描述
图1 - 客户端和服务端

下面是我们通过wireshark抓取到的tcp三次握手的数据包:

这里写图片描述
图2 - tcp三次握手的数据包

  我们看到客户端远程登录服务端时,首先发送了一个SYN报文,其中目标端口为23(远程登录telnet协议使用23端口),初始序号seq = 0,并设置自己的窗口rwnd = 4128(rwnd是一个对端通告的接收窗口,用于流量控制)。

  然后服务端回复了一个SYN + ACK报文,初始序号seq = 0,ack = 1(这个ack的值是在前一个包的seq的基础上+1计算出来的),同时也设置自己的窗口rwnd = 4128。

  客户端收到服务端的SYN + ACK报文后,回复了一个ACK报文,表示确认建立tcp连接,序号为seq = 1, ack = 1, 设置窗口rwnd = 4128,此时客户端和服务端之间已经建立tcp连接。


2. 网络编程中的connect函数

  前面我们在介绍tcp三次握手的时候说过,客户端在跟服务端建立tcp连接时,通常是由客户端主动向目标服务端发起tcp连接建立请求,服务端被动接受tcp连接请求;同时服务端也会发起tcp连接建立请求,表示服务端希望和客户端建立连接,然后客户端会接受连接并发送一个确认,这样双方就已经建立好连接,可以开始通信。

  这里说明一下:可能有的小伙伴会感到疑惑,为啥服务端也要跟客户端建立连接呢?其实这跟tcp采用全双工通信的方式有关。对于全双工通信,简单来说就是两端可以同时收发数据,如下图所示:

这里写图片描述

  我们再回到正题,那么在网络编程中,肯定也有对应的函数做到跟上面一样的事情,没错,就是connect(连接)。顾名思义,connect函数就是用于客户端程序和服务端程序建立tcp连接的。

  一般来说,客户端使用connect函数跟服务端建立连接,肯定要绑定一个ip地址和端口号(相当于客户端的身份标识),要不然服务端都不知道你是谁?凭什么跟你建立连接。同时还得指明服务端的ip地址和端口号,也就是说,你要跟谁建立连接。

  如果socket类型是 SOCK_STREAM(SOCK_STREAM表示使用tcp通信方式)的,调用connect 函数会做两件事:
1 . 发起请求连接。

2 . connect函数会自动检测是否有绑定套接字(ip地址和端口号),如果没有,会自动绑定一个可用的套接字地址。

函数原型:

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:
sockdf:客户端的套接字文件描述符
addr:要连接的套接字地址,这是一个传入参数,指定了要连接的套接字地址信息(例如IP地址和端口号)
addrlen:是一个传入参数,参数addr的大小,即sizeof(addr)

返回值说明:连接建立成功返回0,失败返回-1并设置errno

  connect函数在建立tcp连接的过程中用到了一个非常重要的队列,那就是未决连接队列,这个队列用来管理tcp的连接,包括已完成三次握手的tcp连接和未完成三次握手的tcp连接,下面我们就来详细介绍一下未决连接队列。


3. 未决连接队列

  未决连接队列是指服务器接收到客户端的连接请求,但是尚未被处理(也就是未被accept,后面会说)的连接,可以理解为未决连接队列是一个容器,这个容器存储着这些尚未被处理的链接,一旦容器中的一条未决连接被accept,就会从容器中踢出掉

  当一个客户端进程使用 connect 函数发起请求后,服务器进程就会收到连接请求,然后检查未决连接队列是否有空位,如果未决队列满了,就会拒绝连接,那么客户端调用的connect 函数返回失败。

  如果未决连接队列有空位,就将该连接加入未决连接队列,也就是说当客户端的tcp会从SYN_SENT转换为ESTABLISHED状态,同时服务端的tcp也会从SYN_RCVD转换为ESTABLISHED状态,这表明tcp的“三次握手”连接已完成,而这条tcp连接会从未完成队列加入到已完成队列中,然后accept函数获取到一个客户端连接并返回(注意:此时未决连接被accept),同时将这个未决连接从未决队列中删除

这里写图片描述
图3 - 未决连接队列

  另外,从图3可知,未决连接包括了已完成“三次握手”的tcp连接和尚未完成“三次握手”的tcp连接。

在图3中,在未决连接队列中又分为2个队列:
  未完成连接队列:即客户端已经发出SYN报文并到达服务器,但是在tcp三次握手连接完成之前,这些套接字处于SYN_RCVD状态。

  已完成连接队列:即刚刚完成tcp三次握手的tcp连接,这些套接字处于ESTABLISHED状态,服务器会将这些套接字加入到已完成队列。


4. tcp连接的状态变迁

我们来看一下连接建立的具体过程,如图所示:

这里写图片描述
图4 - tcp连接建立过程

  服务端首先调用listen函数监听客户端的连接请求,然后调用accpet函数阻塞等待取出未决连接队列中的客户端连接,如果未决连接队列一直为空,这意味着没有客户端和服务器建立连接,那么accpet就会一直阻塞。

  当客户端一调用connect函数发起连接时,如果完成tcp三次握手,那么accpet函数会取出一个客户端连接(注意:是已经建立好的连接)然后立即返回。

  上面就是客户端和服务端在网络中的状态变迁的具体过程,前面我们在学习tcp三次握手的过程中还知道,服务端和客户端在建立连接的时候会设置自己的一个接收缓冲区窗口rwnd的大小。

  服务端在发送SYN + ACK数据报文时会设置并告知对方自己的接收缓冲区窗口大小,客户端在发送ACK数据报文时也会设置并告知对方自己的接收缓冲区窗口大小。

  注意:accpet函数调用成功,返回的是一个已经完成tcp三次握手的客户端连接,如果此时服务端没有接收到客户端的ACK,则说明三次握手还没有建立完成,未决连接队列为空,accept函数依然会阻塞,换句话说,accept函数跟tcp连接建立过程没有任何关系,accept函数只关注未决队列是否有可用的未决连接(参考:3-listen函数(主动socket和被动socket))。

  下面是关于tcp三次握手连接建立的几种状态:SYN_SENT,SYN_RCVD,ESTABLISHED。

  SYN_SENT:当客户端调用connect函数向服务端发送SYN包时,客户端就会进入 SYN_SENT状态,并且还会等待服务器发送第二个SYN + ACK包,因此SYN_SENT状态就是表示客户端已经发送SYN包。

  SYN_RCVD:当服务端接收到客户端发送的SYN包并确认时,服务端就会进入 SYN_RCVD状态,这是tcp三次握手建立的一个很短暂的中间状态,一般很难看到, SYN_RCVD状态表示服务端已经确认收到客户端发送的SYN包。

  ESTABLISHED:该状态表示tcp连接建立成功(比如当客户端收到SYN+ACK包的时候就会从 SYN_SENT转换为ESTABLISHED状态,说明客户端建立连接成功,connect函数调用返回)。


对于这两个队列需要注意几点注意:
  1 . 未完成队列和已完成队列的总和不超过listen函数的backlog参数的大小

  2 . 一旦该连接的tcp三次握手完成,就会从未完成队列加入到已完成队列中

  3 . 如果未决连接队列已满,当又接收到一个客户端SYN时,服务端的tcp将会忽略该SYN,也就是不会理客户端的SYN,但是服务端并不会发送RST报文,原因是:客户端tcp可以重传SYN,并期望在计时器超时前从未决连接队列中找到空位与服务端建立连接,这显然是我们所希望看到的。但如果让服务端直接发送一个RST的话,那么客户端的connect函数将会立即返回一个错误,而不会让tcp有机会重传SYN,但是我们并不建议这样做。

  但是不排除有些linux实现在未决连接队列满时,的确会发送RST。但是这种做法是不正确的,因此我们最好忽略这种情况,处理这种额外情况的代码也会降低客户端程序的健壮性。


5. connect函数出错情况

  网络编程的难点在于网络出错的异常处理,熟悉网络编程接口的出错和处理对我们进行网络排错也有很大的帮助。

  由于connect函数是在建立tcp连接成功或失败才返回,返回成功的情况我们就直接跳过了,这里我们针对connect函数返回失败的几种情况了解下,一般来说,connect函数返回失败,有以下几种情况。


5.1 第一种出错情况

  当客户端发送了SYN报文后,没有收到确认则返回ETIMEDOUT错误,值得注意的是,并不会马上返回ETIMEDOUT错误。比如说:当你调用了connect函数,客户端发送了一个SYN报文,没有收到确认就等6s后再发一个SYN报文,还没有收到就等24s再发一个(不同的linux系统设置的时间可能有所不同,这里以BSD系统为主)。这个时间是累加的,如果总共等了75s后还是没收到确认,那么客户端将返回ETIMEDOUT错误。

  这种情况一般是发生在服务端的可能性比较大,也就是服务端当前所处网络环境流量负载过高,网络拥塞了,然后服务端收到了客户端的SYN报文却来不及响应,或者发送的响应报文在网络传输过程中老是丢失,导致客户端迟迟收不到确认,最后返回ETIMEDOUT错误。

  我们可以简单复现一下这种情况,这个实验是基于CentOS系统进行的,具体过程如下所示:

  1 . 首先通过iptables -F把Centos上的防火墙规则清理掉,然后再通过iptables -I INPUT -p tcp –syn -i lo -j DROP命令把本地的所有SYN包都过滤掉(模拟服务端当前网络不稳定)。

执行以下命令:

iptables -F
iptables -I INPUT -p tcp --syn -i lo -j DROP



  2 . 然后通过nc命令向本地的环回地址127.0.0.1发起tcp连接请求(相当于自己跟自己发起tcp连接),来模拟客户端跟服务端发起tcp连接,但是服务器端就是不响应,最后导致客户端的tcp连接建立请求超时,并终止tcp连接。

这里写图片描述
图5



   3 . 然后再通过tcpdump工具把客户端和服务端建立tcp连接过程中的数据报都抓取下来,可以通过tcpdump -i any port 10086命令来过滤所有网卡的10086端口的数据包。

这里写图片描述
图6

  如图6所示,localhost.39299代表客户端,localhost.10086代表服务端,客户端总共向服务端发送了6个YSN报文,每一次重传SYN包的间隔时间分别是1s,2s,4s,8s,16s,这些时间累积加起来总共为31s,因此我们可以知道重传的时间间隔按照指数退避算法增长。其实客户端在发送最后一个SYN报文时还等待了一段时间,然后才超时。也就是说,客户端在发送了第一个SYN报文时,会设置了一个计时器并开始计时,在最后一个SYN报文还没收到服务端的确认时,最后一次等待了32 s,也就是说当等待了1s + 2s + 4s+ 8s+ 16s + 32s = 63 s后,这个计时器就会超时,然后关闭这条tcp连接。


5.2 第二种出错情况

  如果客户端发送了一个SYN报文后,然后服务端回复了一个RST报文,说明这是一个异常的tcp连接,服务端发送了RST报文重置这个异常的tcp连接。引发这种情况是有可能客户端向服务端发起连接的端口号,服务端可能根本就没有listen。

  这种情况一般为拒绝连接请求,比如:客户端想和服务端建立tcp连接,但是客户端的连接请求中使用了一个不存在的端口(比如:这个端口超出65535的范围),那么服务端就可以发送RST报文段拒绝这个请求。

这里写图片描述
图7

  拒绝连接一般是由服务器主动发起的,因为客户端发起请求连接携带的目的端口,可能服务器并没有开启LISTEN状态。因此服务器在收到这样的报文段后会发送一个RST报文段,在这个报文里把RST和ACK都置为1,它确认了SYN报文段并同时重置了该tcp连接,然后服务器等待另一个连接。客户端在收到RST+ACK报文段后就会进入CLOSED状态。

这里以通过20000不存在的端口远程登录为例:

这里写图片描述
图8

tcpdump抓取到的数据包如下:

13:35:08.609549 IP 192.168.98.137.49057 > 192.168.0.102.dnp: Flags [S], seq 2919679902, win 14600, options [mss 1460,sackOK,TS val 39134059 ecr 0,nop,wscale 6], length 0
13:35:09.610018 IP 192.168.98.137.49057 > 192.168.0.102.dnp: Flags [S], seq 2919679902, win 14600, options [mss 1460,sackOK,TS val 39135059 ecr 0,nop,wscale 6], length 0
13:35:09.610115 IP 192.168.0.102.dnp > 192.168.98.137.49057: Flags [R.], seq 1766537774, ack 2919679903, win 64240, length 0
13:35:10.610188 IP 192.168.0.102.dnp > 192.168.98.137.49057: Flags [R.], seq 3482791532, ack 1, win 64240, length 0

通过分析tcpdump工具抓取的数据发现,RST报文段不携带数据。


5.3 第三种出错情况

  如果客户端调用了connect函数向服务端发送了一个SYN报文,这个SYN报文在网络传输过程中经过某个路由器时,正好这个路由器出问题了,缺少到达目的地的路由,不能把这个SYN报文转发给目的地址,那么该路由器会丢弃这个SYN报文,并同时给客户端发送一个Destination unreachable(主机不可达)的ICMP差错报文。客户端的linux内核会保存这个Destination unreachable的ICMP差错报文,同时按第一种情况继续发送SYN报文,如果在规定的时间超时后还没收到服务端的响应报文,那么linux内核会把保存的ICMP差错报文作为EHOSTUNREACH或ENETUNREACH错误返回给客户端的应用进程。

  下面的这个实验就是用来说明第三种情况,帮助理解,大家能看明白就行了,可以不用去做这个实验,当然,有兴趣的同学可以去模拟一下。对于这种出错情况,可能并不需要我们来进行处理,但是我们需要知道这种出错情况的原因所在。

这里写图片描述
图9



然后client通过telnet命令远程登录server:
这里写图片描述



这是抓取到的数据包,client在远程登录server时,发起了SYN连接请求。
这里写图片描述



现在我们来模拟client设备出故障,删除R1设备到server的路由信息

no ip route 12.1.3.0 255.255.255.0 12.1.2.2

下面是wireshark抓取到的数据包:
这里写图片描述

  此时client再登录server时就会失败,我们从抓取到的数据包可以发现,client发送了一个SYN报文,然后R1设备收到这个SYN报文时,发现自己不能到达server,于是会把这个SYN报文丢弃掉,并向client发送了一个目标主机不可达的ICMP差错报文,于是client发送了RST报文来关闭这条异常的tcp连接。

   关于connect函数的调用过程和出错情况,到这里就分析完毕了。

6.总结

   1. 熟悉tcp“三次握手”过程和状态变迁

   2. 掌握connect函数的用法

   3. 了解未决连接队列

  4. 熟悉connect函数几种常见的出错情况

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值