Linux网络——连接、TCP全连接队列TCPdump抓包
直接做实验——输出一个结论,Listen的第二个参数作用是什么?
TcpServer.cc 就是和我们之前写过的TCPdemo基本上一模一样,为了方便实验,将listen的第二个参数的默认值设为1,注释掉TCP中连接的那部分。
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>const static int default_backlog = 1;enum
{Usage_Err = 1,Socket_Err,Bind_Err,Listen_Err
};#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)class TcpServer
{
public:TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 都是固定套路void Init(){// 1. 创建socket, file fd, 本质是文件_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){exit(0);}int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));// 2. 填充本地网络信息并bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = htonl(INADDR_ANY);// 2.1 bindif (bind(_listensock, CONV(&local), sizeof(local)) != 0){exit(Bind_Err);}// 3. 设置socket为监听状态,tcp特有的if (listen(_listensock, default_backlog) != 0){exit(Listen_Err);}}void ProcessConnection(int sockfd, struct sockaddr_in &peer){uint16_t clientport = ntohs(peer.sin_port);std::string clientip = inet_ntoa(peer.sin_addr);std::string prefix = clientip + ":" + std::to_string(clientport);std::cout << "get a new connection, info is : " << prefix << std::endl;while (true){char inbuffer[1024];ssize_t s = ::read(sockfd, inbuffer, sizeof(inbuffer)-1);if(s > 0){inbuffer[s] = 0;std::cout << prefix << "# " << inbuffer << std::endl;std::string echo = inbuffer;echo += "[tcp server echo message]";write(sockfd, echo.c_str(), echo.size());}else{std::cout << prefix << " client quit" << std::endl;break;}}}void Start(){_isrunning = true;while (_isrunning){sleep(1);// 4. 获取连接// struct sockaddr_in peer;// socklen_t len = sizeof(peer);// int sockfd = accept(_listensock, CONV(&peer), &len);// if (sockfd < 0)// {// continue;// }// ProcessConnection(sockfd, peer);}}~TcpServer(){}private:uint16_t _port;int _listensock; // TODObool _isrunning;
};using namespace std;void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n"<< std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);tsvr->Init();tsvr->Start();return 0;
}TcpClient.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>int main(int argc, char **argv)
{if (argc != 3){std::cerr << "\nUsage: " << argv[0] << " serverip serverport\n"<< std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket < 0){std::cerr << "socket failed" << std::endl;return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(serverport); // 替换为服务器端口serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址int result = connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));if (result < 0){std::cerr << "connect failed" << std::endl;::close(clientSocket);return 1;}while (true){std::string message;//std::cout << "Please Enter@ ";std::getline(std::cin, message);if (message.empty())continue;send(clientSocket, message.c_str(), message.size(), 0);char buffer[1024] = {0};int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);if (bytesReceived > 0){buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾std::cout << "Received from server: " << buffer << std::endl;}else{std::cerr << "recv failed" << std::endl;}}::close(clientSocket);return 0;
}Makefile
.PHONY:all
all:tcp_server tcp_clienttcp_server:TcpServer.ccg++ -o $@ $^ -std=c++14
tcp_client:TcpClient.ccg++ -o $@ $^ -std=c++14.PHONY:clean
clean:rm -rf tcp_server tcp_client
运行服务器,然后让客户端在后台访问,再用
netstat -natp查看进程状态

那么我们可以看到不论是客户端的还是服务器的连接都处于ESTABLISHED状态。
首先我们的TCP是支持全双工的,所以如果你的客户端与服务器都部署在同一台机器上,启动完建立好连接后,再去查那么你会查到两条连接。因为TCP双方建立连接的关系是对等的,所以就要服务器到客户端,客户端到服务器这两条连接。
![]()
即便我们的服务器不调用accept,我们的客户端依旧可以进行连接

三次握手建立连接的过程,和用户是否accept无关。三次握手会在底层自动完成,自动把连接建立好,随时等待我们去获取。
所以accept的本质呢,其实就是把一个已经建立好的连接通过accept以文件描述符的方式给用户返回。
此时启动2个客户端,发现没什么问题。一旦启动第3个客户端是就会出现异常了。

服务器的状态还是ESTABLISHED状态,可是有的客户端的状态变成了SYN_SENT
SYN_SENT 叫同步发送

客户端发送了一个SYN但是服务器对这个SYN请求不做响应,那么它也就无法进行ACK,也就进不了ESTABLISHED,他就只能一直在那个SYN_SENT那里,那么三次握手根本就没有完成。
为什么呢?
在服务器来不及进行accept的时候,底层的TCP listen sock 允许用户继续三次握手,建立连接成功,但是不能建立成功太多。
那么具体是多少呢? ——backlog+1
所有建立成功的连接会在操作系统内部维护起来,形成一个队列方便我们上层未来去进行accept——我们把维护好的这个连接队列称为全连接队列
所以listen中第二个参数backlog的作用就是:全连接队列中已经建立三次握手成功的连接的个数 = backlog+1
Linux 内核协议栈为一个 tcp 连接管理使用两个队列:
1. 半链接队列(用来保存处于 SYN_SENT 和 SYN_RECV 状态的请求)
2. 全连接队列(accpetd 队列)(用来保存处于 established 状态,但是应用层没有调用 accept 取走的请求)
理解全连接队列(原理)

操作系统传输层中会维护一个队列为accept_queue,客户端连接服务器时首先要进行三次握手,那么作为一款服务器一定会有很多客户端访问他,那么自然服务器内部就会建立很多连接,服务器就需要对这些连接进行维护,就需要操作系统对所有已经建立的连接做管理。
那么怎么管理呢?先描述再组织。所以所谓的连接一定是操作系统内部的一个结构体对象。
那么如果三次握手成功,就会给它构建一个结构体对象,然后就把结构体对象链入队列里——这就代表了:有一个连接正在连我。
那么对于应用层往往是:先获取连接,再处理连接。获取连接一般是掉用accept,经过我们的系统调用接口accept就可以把这个连接拿上来,拿上来并不是直接把数据结构拿上来,而是拿上来之后给我们一个文件描述符,将来能够通过这个文件描述符将来能够以某种方式找到这个连接,然后就能够和这个客户端通信。
我们模拟的是:
如果上层非常忙,来不及进行accept,可是客户端可不管,客户端一直要求要和服务器连接。那么握手成功就放到连接队列里,握手不成功就过一段时间关掉。那么只要全连接队列积压有节点了,就证明服务器很忙了,如果服务器不忙的话,全连接队列一有节点就会被快速的accept掉。
这并不代表服务器只能同时处理backlog+1个连接,全连接队列积压的是来不及处理的连接
3、为什么全连接队列系统设置时不能为空,也不能太长
上层应用层不断处理连接,然后再从全连接队列里拿走;而上层在处理连接的同时,还有许多客户端在访问服务器建立连接,所以同时操作系统不断将建立好的连接插入全连接队列里。
是不是很眼熟,全连接的本质就是——生产者消费者模型
那么为什么不能为空呢?如果全连接队列为空,然后服务器遇到非常忙的情况,这时又有很多的客户端来访问服务器,那服务器就没办法只能将这些客户端请求的连接给拒绝了。服务器闲下来就只能等下一批客户端的连接了,这就会增加服务器的闲置率,减少给用户提供服务的效率和体验
那如果全连接队列太长了呢?前面我们提到全连接队列如果积压有一些连接,那么就说明我们的服务器处于一个非常忙的状态,压力已经非常大了。这时后还让客户端去排长队,客户端发起连接请求等个几秒没无所谓,过一会断开连接不就得了,但是服务器维护这个连接是要成本的,你这不就浪费了内存资源了吗。——原因就是:浪费空间;用户体验不好
使用 TCP dump 进行抓包,分析 TCP 过程
安装 tcpdump
tcpdump 通常已经预装在大多数 Linux 发行版中。如果没有安装,可以使用包管理器 进行安装。例如 Ubuntu,可以使用以下命令安装
sudo apt-get update
sudo apt-get install tcpdump常见使用
使用以下命令可以捕获所有网络接口上传输的 TCP 报文
sudo tcpdump -i any tcp-i any 指定捕获所有网络接口上的数据包,tcp 指定捕获 TCP 协议的数据 包。i 可以理解成为 interface 的意思
如果你只想捕获某个特定网络接口(如 eth0)上的 TCP 报文,可以使用以下命令
sudo tcpdump -i eth0 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.200
and 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 文件中的数据包并进行分析
运行tcpclient.cc和tcpserver.cc,对他们进行抓包
由于我用的是一台云服务器,而不是两台机器;所以抓包应该在本地环回上抓。
sudo tcpdump -n -i lo port 8888 and tcp

不难看出SYN、SYN、ACK
最后客户端到服务器的ACK只需要ACK置为1
客户端向服务器发消息,length为5的那个报包

前面的获取连接注释掉了

客户端CTRL + C退出,[F.]FIN。[.]就是ACK两次挥手,那么怎么没有4次挥手。

服务器代码里边有一个bug,当我们获取一个新连接之后处理连接,处理完没有关闭

在抓一次

这下怎么成三次挥手了,客户端和服务器几乎是同时退出的,中间两次挥手捎带应答了;可以让服务器休眠1秒后再断开就可以看到4次挥手了。
如何理解连接,再理解全连接队列(内核)

启动服务器本质也就是启动了一个进程。每一个进程启动时就会有一个文件描述符表,其中就包含了listen_sock.listen_sock既然是个文件描述符那么底层就一定会有一个文件对象struct file,struct file的地址就会被放到3号文件描述符中。Linux下一切皆文件,也可以理解按照文件一样去处理我们的这个套接字。
当我们在内核当中创建套接字时,操作系统会为我们创建一个数据结构struct socket。

结构体里面包含了一个struct file *,那么也就是说将来这个结构体要回指向我们的文件。
还包含了一个struct sock的结构;
从文件找到socket还没讲呢,文件创建时有个private data的指针到时候会有这个指针指向socket。
那么这个socket的是什么呢?它是我们网络socket的入口,怎么证明呢?
socket中有一个const struct proto_ops *ops;
它里面包含着一个方法族,其中有着大量的函数指针

所以未来我们的上层在进行读写套接字时就可以使用这个套接字内所对应的函数指针指向不同的方法。

网络中不是还有网络细节TCP不是还要连接吗?UDP不是无连接吗?这个怎么证明呢。
刚才说了TCP的连接是一种数据结构,那么这个数据结构具体叫什么呢?tcp_sock,这是我们在内核当中创建的一个真实的TCP套接字
而这个tcp套接字就是我们三次握手完成之后操作系统为我们创建一个数据结构表示一个连接——就是tcp_sock,也是我们accept队列中排队的数据结构
tcp不是面向连接 的吗?它连接呢?
而struct tcp_sock中的第一个成员struct inet_connection_sock inet_conn;//inet网络connection连接,sock套接字;这里面就包含了很多连接相关的信息:连接状态与协议控制、拥塞控制与重传机制、内存管理与队列......
struct inet_connection_sock里面有一个字段:struct request_sock_queue icsk_accept_queue;//全连接队列,一旦连接成功那么在底层他就会自动建立tcp_sock,就会进入connection套接字中的icsk_accept_queue全连接队列当中
inet_connection_sock的第一个成员struct inet_sock isck_inet,inet网络,sock套接字,连接建立好不得又源ip目的ip源port目的port;
struct inet_sock isck_inet里面的第一个成员为struct sock
struct sock又是个什么东西呢?struct sock里面包含的更多的是关于报文的信息。

在Linux中,服务器或客户端内部呢报文要走协议栈,OS内部可能同时存在多个报文,所以OS要对报文做管理怎么管理先描述在组织,OS中每一个报文其实是由一个sk_buff的结构构成。
我读写一个套接字的时候tcp不是有读缓冲区和写缓冲区吗,上图的两个队列就是读和写缓冲区。
前面提到的struct socket结构体里边有一个struct sock的指针,指向的就是sock结构体。可是我要的是tcp套接字,你这个指针和我要指向的东西不一样啊,tcp不久就sock的最外层吗。到时候要调用哪一层外层的结构体的内容,直接把sock强转为想要的结构体对象就行了。这种通过嵌套的方式,使用公共指针指向对应的头部结构体对象的过程称为——C风格的多态
除了TCP套接字,我们有时候也要创建UDP套接字啊

udp 里面第一个成员是struct inet_sock isck_inet,因为UDP不需要连接所以就没有inet_connection_sock,而inet_connection_sock里面的头一个成员不还是struct sock吗。那么socket依然可以通过指针指向sock从而访问udp的内容。
而socket中const struct proto_ops *ops里面不就有一堆的方法集吗,你是TCP就调用tcp的方法,你是UDP就调用udp的方法。

我怎么知道socket指向的到底是tcp还是udp呢?socket里边不是有short type类型吗
一般我们把struct socket称为BSD socket ——通用套接字接口
在系统层面上已经分了3层了

第一层虚拟文件层(所有的套接字都可以变成文件)
第二层struct socket层:通用套接字层
第三层通用网络层,因为不止可以通过网络来进行套接字通信,这一层还有其他种类的套接字
再往下走其实还有第四层:inet_device网络设备层
我们上面不是提到了全连接队列吗?它创建好了其实是创建了个什么?刚才的套接字是listen套接字,所以我们正在调用accept获取连接我们在建立连接时其实是得到了这个东西:

当我们进行三次握手时,握手成功会创建tcp_sock,构建好后这个结构会被链入我们的accept_queue队列当中,也就是:

链入后你自己不是还要进行accept,当你进行accept的时候OS做什么呢?
操作系统为我们创建struct file 创建socket,然后在文件描述符表中创建新的fd,比如说4,然后将新的文件描述符4指向struct file,然后未来就把4返回上去了,这就是进行IO

这是对上的,那么对下的呢?我们的accept会把tcp_sock,从全连接队列里拿到4号文件描述符创建的struct file所对应的socket中,把socket的sock指针指向tcp_sock中的sock。

过程是这样的

