目录
- `listen第二个参数详解`
- `全连接队列与半连接队列`
- `半开放连接队列(SYN队列)`
- `全连接队列(接受队列)`
- `队列溢出的影响`
- `在内核中理解套接字,连接`
- `完整链路图`
- `总述`
- `使用 TCP dump 进行抓包,分析 TCP 过程`
- `注意事项`
listen第二个参数详解
注意:
当客户端通过connect函数发起连接请求的时候,服务端只要在监听状态(即已经通过listen函数进入监听状态),就能接收并处理客户端的请求开始三次握手过程。但需要注意的是,虽然三次握手的过程本身与accept调用在逻辑上并不直接相关,即accept不是三次握手的一部分,但它是确认连接已完全建立并将新的连接从全连接队列
中取出,返回一个文件描述符给应用层
的关键步骤。- 没有accept调用,即使连接在三次握手后已经建立,这个连接也会在全连接队列中等待,直到被accept处理或队列满后超时被丢弃。
参数含义:
backlog:
这个参数定义了系统内核中未完成连接(即等待服务器accept()处理的连接)队列的最大长度。当客户端发起连接请求时,如果服务器当前无法接受该连接(如正在处理其他连接),则该连接请求会被放入这个队列中等待。
常见误解:
- 有时backlog被误解为SYN队列(半连接队列)和ACCEPT队列(全连接队列)的总大小,但实际上其含义可能因系统实现而异。
在Linux系统中
,backlog主要影响的是全连接队列的大小
,而半连接队列的大小通常由/proc/sys/net/ipv4/tcp_max_syn_backlog
控制。
系统限制:
backlog的上限值受到系统全局设定的限制。
在Linux系统中,这个上限存储在/proc/sys/net/core/somaxconn文件中。如果设置的backlog值大于系统限制,系统会自动将其调整为上限值。
全连接队列与半连接队列
在TCP/IP网络中,当一个客户端尝试与服务器建立TCP连接时,会经历三次握手(SYN-SYN/ACK-ACK)。在这个过程中,服务器需要维护两种队列来管理即将到来的连接请求:半开放连接队列(也称为SYN队列)和全连接队列(也称为接受队列)
。
半开放连接队列(SYN队列)
当服务器接收到一个来自客户端的SYN包时,它知道客户端想要建立一个连接,但此时服务器还未发送SYN/ACK包进行响应。服务器会将这个请求暂时存放在一个叫做半开放连接队列(SYN队列)的地方。这个队列的大小受限于系统参数,如tcp_max_syn_backlog(在Linux中)。如果SYN队列满了,服务器将无法处理更多的连接请求,这可能导致客户端超时并重新发送SYN包,或者在达到重试次数后放弃连接。
全连接队列(接受队列)
当服务器发送SYN/ACK包给客户端,并接收到客户端的ACK响应后,三次握手完成,连接建立
。但此时,连接并没有被应用层接受(例如,还没有被accept()系统调用处理)。这些已建立的连接会被暂时存放在一个叫做全连接队列(接受队列)
的地方。
这个队列的大小也受到系统参数的限制,如backlog参数(在调用listen()函数时指定)
和系统的somaxconn值(在Linux中,它限制了单个监听socket可以排队的最大连接数)
。如果没有超过somaxconn的话,全连接队列的大小是backlog+1
;
队列溢出的影响
SYN队列溢出:
当SYN队列满时,服务器将不再响应新的SYN包,客户端可能会超时并重试连接,这可能导致网络拥堵或连接延迟。全连接队列溢出:
当全连接队列满时,服务器会丢弃新的连接请求(即使它们已经完成了三次握手),并向客户端发送RST包以终止连接。这通常表现为客户端看到“连接被拒绝”的错误。
在内核中理解套接字,连接
当客户端去连接服务器时,经历三次握手建立连接成功后,如果服务端特别忙,没有及时通过accept调用处理这个连接,那么这个连接(本质是一个内核数据结构,包含连接的相关属性,通常称为struct sock,在TCP中可能是struct tcp_sock的一个实例,具体取决于系统实现,后面会详细讲解)会被放在全连接队列中。将来在处理这个连接时,应用层只需调用accept将该连接从队列中取出,并返回一个文件描述符,将来可以根据该文件描述符来操作这个连接。最多能够连接的最大数量就是 backlog+1。
全连接队列的作用:可以避免server闲忙不均,提高效率。
服务器在操作系统中通常作为一个进程运行,它拥有自己的task_struct结构体,该结构体内部维护了一个文件描述符表struct files_struct,里面包含了一个struct file*类型的数组fd_array[ ],用于存储打开文件的引用(在UNIX/Linux系统中,套接字也被视为一种特殊的文件
)。
当我们通过系统调用(如socket)创建一个套接字时(以监听套接字为例),系统会为用户程序返回一个文件描述符。与此同时,操作系统内部会创建一个struct file结构体来代表这个套接字文件。这个struct file结构体是内核中用于表示打开文件的通用结构体,套接字是其中的一种特殊情况。
在创建套接字的过程中,操作系统还会创建一个struct socket结构体来专门管理套接字相关的信息(如连接状态、IP地址、端口号等)。struct socket结构体中包含一个指向struct file的指针file,该指针指向了前面提到的与套接字相关联的struct file对象。同时,struct file结构体中有一个void* private_data字段,该字段在套接字的情况下被用来指向对应的struct socket结构体。这样的设计允许内核通过文件描述符(一个整数索引,指向fd_array[]中的某个struct file*)间接地访问到套接字的详细信息(即struct socket结构体),从而实现了文件描述符与套接字之间的关联。
struct socket结构体是网络socket的核心,它封装了网络套接字所需的各种信息和方法。
我们通常需要通过socket套接字来访问网络相关信息,而struct socket结构体内部包含了一系列的方法(通常称为函数指针或操作集)
,这些方法用于执行套接字的各种操作,如发送数据、接收数据、绑定地址等
。
未来,当上层应用需要与网络进行交互时,它可以通过文件描述符来找到对应的struct socket结构体。文件描述符是一个非负整数,它作为索引指向内核中每个进程的文件描述符表中的一个条目,该条目又指向了struct file结构体,而struct file结构体中的private_data字段则指向了struct socket结构体。这样,上层应用就可以通过文件描述符间接地访问到struct socket结构体,进而调用其内部的方法族来执行网络操作。
一个TCP连接的本质是一个内核数据结构
,当TCP三次握手完成以后,操作系统会在内核中为这个新建立的连接创建一个相应的struct tcp_sock(或相关)数据结构
来管理它。这个数据结构包含了连接的状态信息、缓冲区、序列号等关键数据。在全连接队列中排队的也是它。
该结构体的第一个成员
,又是一个结构体,类型是 inet_connection_sock
。
在inet_connection_sock结构体中包含了很多跟tcp连接相关的信息
,如下:
并且在这里面 struct request_sock_queue
就是管理TCP全连接队列的
。
并且, 在inet_connection_sock结构体中的第一个成员
,也是一个结构体,叫struct inet_sock icsk_inet
,里面有源ip,目的ip,源port,目的port的字段
。
在inet_sock中,第一个字段还是一个结构体,struct sock sk
;里面包含的更多的是报文的信息
。
其中,struct sock在tcp_sock结构中,属于最内侧的
。并且我们可以发现在前面说的struct socket对象中,还包含一个字段,struct sock *sk
;指向的就是tcp_sock中的sock这个结构体。所以,当我们拿着struct socket结构体想要访问tcp_sock结构的内容,只需要进行强转即可,就可以访问tcp_sock里面的所有内容了
。这就是C风格的多态
。
其中,上面所说的是tcp套接字,那么udp呢?
udp与tcp类似,只是没有嵌套struct inet_connection_sock结构体而已,因为udp不需要连接,不需要连接队列
。
完整链路图
总述
-
半连接状态:
当客户端发送一个SYN(同步序列编号)请求到服务器时,服务器会接收到这个请求并将其放入一个称为“半连接队列”的数据结构中。此时,服务器尚未发送SYN-ACK(同步序列编号确认)响应给客户端,因此连接尚未完全建立。在这个阶段,服务器正在等待确认客户端的SYN请求是有效的,并且准备发送自己的SYN-ACK响应。 -
TCP三次握手完成后,操作系统实际上会创建tcp_sock结构体(以TCP为例),并将其放入半连接队列(而非全连接队列,通常半连接队列用于存放尚未完全完成三次握手的连接请求)。如果连接成功完成三次握手,它将被移动到全连接队列中等待被accept调用处理。
-
需要注意的是,虽然从逻辑上看,三次握手成功完成后连接应该直接放入全连接队列,但实际上操作系统在处理这些连接时需要进行一系列的内部检查和资源分配。因此,将连接先放入半连接队列再转移到全连接队列是一种有效的管理策略,它可以帮助操作系统更好地处理并发连接请求并避免资源竞争。
当accept函数被调用时,操作系统会执行以下操作
:
- 从全连接队列中取出一个连接(即tcp_sock结构体)。
- 创建一个新的struct socket结构体和struct file结构体,并初始化它们。这两个结构体会互相指向,形成关联。
- 在文件描述符表中申请一个新的文件描述符,并将这个新的文件描述符与刚创建的struct file结构体关联起来。
- 将这个新的文件描述符返回给上层应用程序,以便应用程序可以使用这个描述符来读写数据。
- 更新struct socket结构体中的字段,使其指向从全连接队列中取出的tcp_sock结构体,从而建立起socket与TCP连接之间的关联。
这样,通过accept函数,应用程序就获得了一个可以用来与远程主机进行通信的文件描述符。”
使用 TCP dump 进行抓包,分析 TCP 过程
TCPDump
是一款强大的网络分析工具,主要用于捕获和分析网络上传输的数据包。
安装 tcpdump
- tcpdump 通常已经预装在大多数 Linux 发行版中。如果没有安装,可以使用包管理器
进行安装。例如 Ubuntu,可以使用以下命令安装:
sudo apt-get update
sudo apt-get install tcpdump
- 在 Red Hat 或 CentOS 系统中,可以使用以下命令:
sudo yum install tcpdump
常见使用
- 捕获所有网络接口上的 TCP 报文
使用以下命令可以捕获所有网络接口上传输的 TCP 报文:
sudo tcpdump -i any tcp
注意:-i any
指定捕获所有网络接口上的数据包,tcp指定捕获 TCP协议的数据包。i可以理解成为 interface的意思
- 捕获指定网络接口上的 TCP报文,如果你只想捕获某个特定网络接口(如 eth0)上的 TCP报文,可以使用以下命令:
sudo tcpdump -i eth0 tcp
ifconfig
3. 捕获特定源或目的 IP地址的 TCP报文
使用 host关键字
可以指定源或目的 IP地址
。例如,要捕获源 IP地址为192.168.1.100的 TCP报文,可以使用以下命令:
sudo tcpdump src host 192.168.1.100 and tcp
要捕获目的 IP地址为 192.168.1.200的 TCP报文,可以使用以下命令:
sudo tcpdump dst host 192.168.1.200 and tcp
同时指定源和目的 IP地址,可以使用 and关键字连接两个条件:
sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200and tcp
- 捕获特定端口的 TCP报文使用 port关键字可以指定端口号。例如,要捕获端口号为 80的 TCP报文(通常是HTTP请求),可以使用以下命令:
sudo tcpdump port 80 and tcp
- 保存捕获的数据包到文件使用
-w选项
可以将捕获的数据包保存到文件中,以便后续分析。例如:
sudo tcpdump -i eth0 port 80 -w data.pcap
这将把捕获到的 HTTP流量保存到名为 data.pcap的文件中。
- pcap后缀的文件通常与 PCAP(Packet Capture)文件格式相关,这是一种用于捕获网络数据包的文件格式
- 从文件中读取数据包进行分析使用
-r 选项
可以从文件中读取数据包进行分析。例如:
tcpdump -r data.pcap
这将读取 data.pcap文件中的数据包并进行分析。
注意事项
- 使用 tcpdump时,请确保你有足够的权限来捕获网络接口上的数据包。通常,你需要以 root用户身份运行 tcpdump。
- 使用 tcpdump的时候,有些主机名会被云服务器解释成为随机的主机名,如果不想要,就用-n选项
- 主机观察三次握手的第三次握手,不占序号;