TCP全连接和tcpdump抓包实现
1.全连接队列
listen函数的第二个参数backlog+1,就是TCP全连接队列的长度。
客服端进行连接进入established状态后,服务器如果处于忙碌状态没有调用accept函数将连接取走,这个连接就会呆在TCP全连接队列中,直到上层调用函数accept将连接取走。
全连接队列中可以维持一段时间,如果上层没有取走且没有主动关闭,全连接队列如果满了,新来的连接就无法进入established状态,OS会建立一个临时的数据结构存储,数据结构里的很多字段都是没有初始化完,会被OS保存在半连接队列中,在半连接队列中保存时间短。
全连接队列意义
服务器忙碌状态时,来不及处理来到的连接,就需要一个容器来存储,等空闲了就来处理队列中要连接的状态,不存储等空闲状态但连接又没有就减少了用户体验,处理效率低下。
全连接队列不能为空,增加服务的闲置率,队列也不能太长,不如等待时间就会提高,也会占用资源,需要合适大小,上层处理越快队列就不用太长。
全连接模式就像生产者消费者模型,服务器充当消费者模型,客服端充当生产者,消费者需要处理连接,生产者则是创造连接,那么全连接队列就是任务队列。
TCP三次握手完成后,操作系统实际上会创建tcp_sock结构体(以TCP为例),并将其放入半连接队列(而非全连接队列,通常半连接队列用于存放尚未完全完成三次握手的连接请求)。如果连接成功完成三次握手,它将被移动到全连接队列中等待被accept调用处理。
需要注意的是,虽然从逻辑上看,三次握手成功完成后连接应该直接放入全连接队列,但实际上操作系统在处理这些连接时需要进行一系列的内部检查和资源分配。因此,将连接先放入半连接队列再转移到全连接队列是一种有效的管理策略,它可以帮助操作系统更好地处理并发连接请求并避免资源竞争。
2.内核层面理解socket和连接
连接也是对象需要被管理起来。
服务器是一个进程,有自己的task_struct结构体,成员有一个文件描述符表struct files_struct 里面有一个struct file* fd_array[]文件描述符数组。进程启动时,OS会默认打开标准输入,标准输出和标准错误,位于fd_array[]数组0,1,2下标位置。创建listen套接字的时候,就会开辟3这个文件描述符。有文件描述符就会有自己的struct file结构体。
创建套接字时,内核会创建一个struct socket结构体,可以看到里面有一个指针指向了struct file结构体,也就是回指指针。
struct file结构体有一个private_data指针
创建套接字时,private_data会指向struct socket,产生了关联关系,互相指向对方。
struct proto_ops*指向了一组方法簇
proto_ops
的作用
定义协议操作接口:为网络协议提供了一组标准化的操作接口,使得不同的网络协议可以遵循统一的框架进行实现和管理。
协议实现的抽象:将网络协议的具体实现细节抽象成一组函数指针,方便内核对不同协议进行统一管理和调用。
支持多种协议:通过定义不同的
proto_ops
结构体,可以支持多种网络协议,如TCP、UDP、SCTP等。下图为proto_ops常用方法
上层读写套接字时,就使用里面的函数方法。创建套接字时,OS会在底层创建一个stuct tcp_sock
三次握手后就会创建一个tcp_sock结构体,第一个成员就是inet_connection_sock结构体。
inet_connection_sock结构体包含了很多tcp连接相关的。
里面的struct request_sock_queue icsk_acceot_queue就是管理TCP全连接队列的。
然后这个inet_connection_sock的第一个成员还是结构体,inet_sock类型的,
下图就看到了于网络相关字段,地址,端口号,ip等。
tcp_sock里面嵌套了很多个结构体。
然后再inet_sock中第一个成员也是sock对象结构体。
而这个结构体被一开始的socket结构体成员的指针指向。
下图可以看到sk_buff_head型的sk_receive_queue和sk_write_queue,这两个就是接收缓冲区和发送缓冲区。
sk_buff_head里面也有成员,tail和end就是指向应用数据的起始位置,封装时就会移动指针填充各层协议的报文进去。
所以一个TCP协议的连接就会创建这么多结构体,通过强制类型转换可以访问指定的区域,体现了C风格的多态。而UDP的结构是没有inet_connection_sock,因为UDP协议不需要连接,直接发送的。
区分是UDP型的还是TCP型的,但调用listen函数时,填入的参数就有是哪一个类型的了,或者再socket中有一个type变量可以得知。
总结上面分层:
struct file属于第一层,虚拟文件层,套接字会变成文件
struct socket属于第二层,通用套接字层
inet_sock属于第三层,通用网络层
inet_device属于第四层,网络设备层。
三次握手成功后,OS会创建一个tcp_sock结构体,然后把这个结构体放到listen函数打开的文件描述符3中的全连接队列里。
调用accept后,OS创建一个struct file和一个struct socket,此时文件描述符就是4,并将struct socket里面的struct sock*指向刚刚拿上来的tcp_sock,形成关联。就可以通过4号文件描述符,对这个套接字进行读和写操作。
一个服务器通常只有一个监听套接字,用于接收所有的连接,每一个成功建立连接的就会创建一个新的已连接套接字,来进行读写这个套接字实现通信。
在Linux网络编程中,创建两个文件描述符(一个用于监听套接字,另一个用于已连接套接字)是为了将监听和数据传输的操作分离,提高网络程序的效率和可管理性。
创建两个文件描述符(一个用于监听套接字,另一个用于已连接套接字)是为了将监听和数据传输的操作分离,提高网络程序的效率和可管理性。监听套接字的文件描述符用于接受新的连接请求,而已连接套接字的文件描述符用于与客户端进行数据传输。这种设计使得服务器能够同时处理多个客户端连接,并且每个连接都有独立的文件描述符,方便管理和操作。
完整链路图
3.TCPdump实现抓包
Linux下需要安装tcpdump(ubuntu)
sudo yum-get update
sudo apt-get install tcpdump
指令
sudo tcpdump -i any tcp
-i any 指定捕获所有网络接口上的数据包,tcp则是指定捕获TCP协议的数据报。
sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200
src host 是指定源IP地址,dst host表示的是指定的目的IP地址。
sudo tcpdump port 8080 and tcp
捕获端口号为8080的报且是TCP协议的
sudo tcpdump -i eth0 port 80 -w data.pcap
将抓的包保存在data.pcap文件中。
三次握手抓包实例
四次挥手抓包
这里只有三次挥手,是因为捎带应答了,有一个FIN+ACK。