深圳专业制作网站哪个公司好,wordpress添加下载地址,手机php网站开发,微信小程序界面设计模板【Linux深入浅出】之全连接队列及抓包介绍 理解listen系统调用函数的第二个参数简单实验实验目的实验设备实验代码实验现象 全连接队列简单理解什么是全连接队列全连接队列的大小 从Linux内核的角度理解虚拟文件、sock、网络三方的关系回顾虚拟文件部分的知识struct socket结构… 【Linux深入浅出】之全连接队列及抓包介绍 理解listen系统调用函数的第二个参数简单实验实验目的实验设备实验代码实验现象 全连接队列简单理解什么是全连接队列全连接队列的大小 从Linux内核的角度理解虚拟文件、sock、网络三方的关系回顾虚拟文件部分的知识struct socket结构体介绍struct tcp_sock与struct udp_sock介绍struct tcp_sockstruct inet_connection_sock结构体struct inet_sock结构体总结 struct udp_sockTcp接收缓冲区与发送缓冲区 分层介绍 tcp抓包介绍Linux中使用tcp dump进行抓包并分析tcp过程tcp dump的安装tcp dump的简单使用实验 windows中使用wireshark进行抓包wireshark的安装使用telnet作为客户端访问云服务器上的服务器程序设置wireshark过滤规则使用wireshark进行抓包 理解listen系统调用函数的第二个参数
listen函数是在进行TCP socket编程时的系统调用函数它的功能是将普通套接字设置为监听状态也就是将普通的套接字变成监听套接字以便它能收到来自客户端的连接请求。 第一个参数是我们之前创建的socket描述符那么第二个参数应该如何理解呢直接输出结论backlog规定了全连接队列的最大长度全连接队列是用于维护三次握手成功但是系统来不及接收的连接backlog1是这个队列的长度。
简单实验
实验目的
下面我们将做一个小实验这个实验主要会验证如下几个点
三次握手成功建立连接并不需要accept的参与因为它是系统自动完成的accept只是负责从全连接队列中取走已经建立好的连接。backlog1 全连接队列的长度。
因为accept函数会取走全连接队列中的连接而且我们的实验就是模拟系统非常忙的情况所以 TCP server端是不需要调用accept函数的。
实验设备
虚拟机一台云服务器一台。
在同一台设备上会影响实验效果因为TCP连接是双向的从服务器-客户端客户端-服务器都会维护一个连接所以如果在一台设备上做实验会有干扰。
实验代码 TcpServer.cc: #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.hconst 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);}}~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_ptrTcpServer tsvr make_uniqueTcpServer(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.hint 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;
}实验现象 验证即使服务器端没有调用accpet函数三次握手也能建立连接成功 结论accept系统调用函数并不参与三次握手它只负责从下层取走连接socket文件描述符。 验证Tcp全连接最多维护backlog1个连接
可以看到我们在虚拟机中同时运行了多个TcpClient客户端但最终只有两个成功建立连接这是因为全连接的大小不够了所以不会接收来自客户端的连接只有上层调用accept拿走在全连接队列中已经建立的连接全连接的空间才会腾出来。
全连接队列简单理解
什么是全连接队列
全连接队列就是我们内核传输层中某个结构体中维护的一个队列每一个listen套接字都有一个全连接队列 在Linux内核中所谓的连接和全连接队列都是struct结构体后面我们会结合Linux内核重点介绍。
注意全连接队列的大小并不是代表TcpServer服务器只能同时处理这么多而是表示它来不及处理来不及调用accept的连接的最大数量。
全连接队列的大小
全连接队列的本质其实是生产者消费者模型全连接队列作为生产者一直生产连接而上层的accept作为消费者一直从全连接队列中取走连接。
全连接队列的大小不能太大也不能太小
如果backlog为0会增加服务器的闲置率如果有全连接队列那么可能只需要等待一会服务器就有空闲可以把连接取走处理了如果backlog为0直接就是三次握手建立不成功用户就以为你的服务已经崩溃短时间内就不会访问了。如果backlog过大那么处于全连接队列结尾的用户就可能需要等待很久来能享受到服务这样用户体验不好还不如直接连接失败而且维护连接也会占用内存用多余的内存去给服务器处理数据可能效率更高。
过去常用的默认值可能是5或者50左右但是现代Linux系统的默认值通常要高得多。
通常这个值有一个上限我的Linux系统中为4096 /proc/sys/net/core/somaxconn这个内核参数定义了系统范围内每个端口的最大监听队列长度。它设置了listen()函数中backlog参数的上限值。tcp_max_syn_backlog这个文件中规定的是未完成连接请求的最大数量半连接队列。
从Linux内核的角度理解虚拟文件、sock、网络三方的关系
回顾虚拟文件部分的知识
我们都知道在运行服务器程序后系统会给这个进程创建一个task_struct结构体它是用来描述进程的这个结构体中又会有一个file_struct*的指针它指向了一个file_struct的对象这个file_struct结构体是用来管理打开的文件的它里面有一个文件描述符表这个文件描述符表中的每一个下标都指向struct file*对象。
但是我们的网络socket是怎么和struct file这个结构体挂上联系的呢这是我们今天要解决的问题之一因为文件描述表中的文件描述符不仅仅有普通文件的还有网络套接字文件。 struct socket结构体介绍
struct socket {socket_state state;unsigned long flags;struct proto_ops *ops;struct fasync_struct *fasync_list;struct file *file;struct sock *sk;wait_queue_head_t wait;short type;unsigned char passcred;
};struct socket结构体是我们网络socket的入口它是一个通用的套接字类型。 short type:表示套接字的类型是流式套接字还是数据报式的套接字 struct proto_ops *ops它是一个保存各种函数方法的类型可以通过type字段让其指向不同的方法。 struct file*指向虚拟文件层的struct file对象但是我们不是需要通过socket文件描述符找到struct socket嘛怎么顺序反过来别担心其实struct file对象中也有一个开放字段是可以指向struct socket对象的它就是void*类型的private_data字段。
所以经过对struct socket结构体的学习我们上面的图可以继续完善 并且调用socket系统调用的同时就创建了struct socket、struct file并在文件描述表中申请了空间然后还让struct socket与struct file互相指向。
那Tcp Socket与Udp Socket岂不是没有区别了既然调用socket都会创建struct socket的话别急我们继续往下学习。
struct tcp_sock与struct udp_sock介绍
struct tcp_sock tcp_sock结构体中有很多关于tcp的字段譬如
int tcp_header_len即将要发送的TCP报文的头部的长度以字节为单位。rcv_nxt, snd_nxt: 分别表示期望接收的下一个序列号和发送方即将发送的下一个序列号。snd_ssthresh, snd_cwnd: 慢启动阈值和拥塞窗口大小是拥塞控制的重要参数。
但我们更想知道它的第一个字段struct inet_connection_sock是什么
struct inet_connection_sock inet_conn;:光看其名称这个结构体肯定与连接有关。
struct inet_connection_sock结构体
这个结构体是描述的TCP与连接相关的属性里面包括了全连接队列。全连接队列中不仅维护三次握手已经建立好的连接也会维护只进行了二次或者一次的半连接但是半连接的生命周期一般很短。 struct request_sock_queue icsk_accept_queue;这个字段就是我们之前一直在谈的全连接队列它由listensock维护用于管理监听套接字上的半连接SYN_RCVD状态和全连接ESTABLISHED状态但未被accept()接受队列。
但我们最好奇的是它的第一个属性字段
struct inet_sock inet这是一个 struct inet_sock 类型的成员包含了通用的因特网套接字信息。tcp_sock 以此为基础添加TCP特定的信息。
struct inet_sock结构体 struct inet_sock结构体中存储的是与网络通信相关的信息例如
_u32 daddr外部IPv4地址。_u32 rcv_saddr本地IPv4地址。_u32 dport目的端口号。
我们进行Tcp网络通信调用bind系统调用函数bindIP地址和端口号不就是在往这个struct inet_sock结构体中写数据吗
我们惊奇的发现这个inet_sock结构体的第一个字段的类型居然是struct sock我们之前不是在struct socket里面见过这个字段吗让struct socket指向它不就可以通过通用套接字访问到Tcpsock了吗
所以我们预测udp_sock中一定也存在struct sock字段而且一定是在最前面。 总结
看了这么多结构体我们可以画图总结一下它们的关系了 后续只需要通过socket中的struct sock*字段通过强制类型转化我们就可以访问到struct sock、struct inet_sock、struct inet_connection_sock、struct tcp_sock结构体的内容因为它们的初始地址都是相同的这样通过结构体嵌套我们就实现了C风格的多态。
那么当我们客户端和服务器经过三次握手后建立了一个新的连接内核会帮助我们做哪些事情呢 最最重要的是创建struct inet_connection_sock这表示一个新的连接里面的inet_sock字段存储着这个连接相关的属性字段IP地址、端口号。 然后就是struct tcp_sock对象三次握手完成内核实际上已经为这个新建立的连接创建了完整的struct tcp_sock结构体。它不仅包含了inet_connection_sock中的所有字段还添加了许多TCP特有的属性和方法例如序列号管理、窗口缩放、重传机制等。每当一个新的TCP连接被接受即完成了三次握手就会创建一个tcp_sock实例来管理这个连接的状态和行为。 除此之外内核还会将这个连接队尾的next的struct sock*指向新连接的struct sock加入listen套接字的全连接队列中做类似链表的操作然后需要将队列的元素个数加1。 内核中会有实现上述功能的方法
如果全连接队列中没有空间了三次握手根本就不会完成也就不会创建上述的结构体。
当调用accept函数时它会做如下事情 创建struct socket对象三次握手完成时并没有创建这个通用的套接字类型然后从全连接队列中取出队头连接的struct sock*然后赋值给struct socket的struct sock*变量就相当与让struct socket指向了struct tcp_sock因为struct tcp_sock的最开始的字段是struct sock*。 创建struct file并在文件描述符表中开辟一个新的空间指向这个struct file对象。 最后让struct file与struct socket互相引用。 返回文件描述符给上层。 内核中的方法sock_map_fd就是实现类似功能的。
自此之后我们就可以通过socket fd找到struct file然后通过struct file中的private_data字段找到struct socket对象而通用套接字的sk又指向struct tcp_sock的首地址空间的struct sock对象。然后通过强制类型转换可以访问tcp这个连接相关的任何信息包括报文、拥塞控制属性、滑动窗口属性、确认应答相关属性序号、确认序号。
struct udp_sock 由于udp协议比tcp协议要简单所以udp_sock结构体的字段也要少一些。
而且由于udp是无连接的协议所以它没有连接相关的字段它的第一个结构体对象就直接是struct inet_sock结构体。这和tcp的inet_sock是一样的因为网络套接字部分两者有很多相同的部分所以可以复用。
对于udp_sock就是这样 Tcp接收缓冲区与发送缓冲区
我们前面不是一直谈到TCP存在接收缓冲区和发送缓冲区吗它们在内核中是否有体现呢
当然有在struct sock结构体中存在着这两个字段 它们就是接收缓冲区与发送缓冲区每个连接都有单独的struct sock也就意味着有单独的接收缓冲区与发送缓冲区。
sk_buff_head是这个缓冲区的类型它是一个类似队列的结构体 struct sk_buff是描述报文的也就是解析出来或者即将发送的应用层的报文 分层介绍
自此之后虚拟文件、socket、网络三者的关系我们就清楚了我们也清楚了如何通过文件描述符找到关于套接字的各种信息。
它们自上而下是有层次的可以分为虚拟文件层、通用套接字层和网络套接字层。其中通用套接字就像是一个基类它提供了一种通用的方式来创建各种类型的套接字但是当网络真的建立起来又会有其它细微的不同。
tcp抓包介绍
Linux中使用tcp dump进行抓包并分析tcp过程
tcp dump的安装
ubuntu下
sudo apt update
sudp apt install -y tcpdump通过检查版本号验证是否安装成功
tcpdump --versiontcp dump的简单使用 捕获所有网络接口中的报文 sudo tcpdump -i any tcp-iinterface是接口的意思any代表任何-i any的意思就是捕获所以网络接口中的报文。tcp只捕获tcp报文。 捕获指定网络接口的报文 sudo tcpdump -i [本机某网络接口名称] tcp我们可以通过命令ifconfig查看本主机的所有网络接口 捕获指定源IP的报文 sudo tcpdump src host 192.168.0.1 and tcp上述命令的含义是捕获源IP地址为192.168.0.1的到达本主机的tcp报文 现在的一般后端服务器都会使用反向代理来实现负载均衡技术在大型应用或服务中通常会部署多个反向代理服务器以提高性能、增加可用性和提供冗余。所以可能就会出现多次ping qq.com这个相同的域名得到来自不同公网IP服务器的回复这也不用惊讶所以上面的实验存在一定的运气的成分。 上面的显示的公网IP可能是反向代理服务器的IP地址而不是后端服务真正的公网IP。 捕获指定目的IP地址的报文 sudo tcpdump dst host 192.168.0.1 and tcp上面命令的含义是捕获目的IP地址为192.168.0.1的tcp报文 注意这个目的IP为什么是iZt8qyfqyfs47mZ呢云服务提供商使用类似的随机字符串作为实例ID或设备标识 你也可以去云服务网站的控制台修改这个实例名称。 但是如果我们希望它显示IP地址而不是显示云服务器的实例名称该怎么办呢加上选项-n即可 捕获特定端口号的TCP报文 使用port关键字可以捕获特定端口号的报文例如捕获80端口的TCP报文通常是http请求 sudo tcpdump port 80 and tcp实验
使用tcpdump工具一般以捕获特定端口的形式居多代码和上述的验证listen系统调用函数的第二个参数的代码一样简单的tcp echo服务器 服务器不给客户端发送数据也不accept接受连接 将客户端在虚拟机上运行观察抓包现象 三次握手 因为三次握手是没有发送数据的所以length为0。 当我们虚拟机客户端给服务器发送数据报文但是服务器收到该报文发送的ack报文数据为0没有发送数据报文我们有理由相信服务器根本没有将这个连接拿上来给用户但是三次握手肯定成功了并且ack报文是OS自动发送的不需要用户参与 看了一下代码果然没有将连接拿上来。 将accept函数注释取消后继续实验 三次握手部分依旧正常Flags中的S代表SYN标志位win是窗口大小用于滑动窗口中确定窗口大小可以看到双方还协商了mss的大小。 服务器接收数据发送数据 现在收发数据都正常了。 四次挥手部分客户端主动退出 就只有客户端给服务器发送了FIN报文服务器OS自动给它回复了一个ACK报文服务器并没有断开连接我们有理由相信服务器端忘记close关闭socket描述符了。 服务器端在客户端关闭连接后也要正常关闭连接修改代码后继续测试四次挥手的过程 不是说四次挥手吗为什么只有三次呢我们有理由相信在客户端给服务器发送FIN报文后服务器立马就给客户端发送了FIN报文并且这个时间和系统自动发送ACK报文的时间几乎是同时所以触发了捎带应答如果我们让服务器sleep上1s再关闭socketfd就可以看到四次挥手 sleep后的结果
windows中使用wireshark进行抓包
wireshark的安装
wireshark-4.4.3-x64.exe
下载好之后直接安装即可没有太多要注意的地方。
使用telnet作为客户端访问云服务器上的服务器程序
默认windows上telnet服务是没有打开的我们可以手动打开打开telnet教程
设置wireshark过滤规则 首先选择你想捕获哪个网卡的流量上行和下行 选择好之后顶部工具栏点捕获点开始就可以开始捕获该网卡的流量 默认是捕获经过该网络接口的流量 在顶部可以设置过滤规则我们设置ip为服务器ip只关心服务器所在的端口号8888 ip.addr 121.40.68.117 tcp.port 8888顶部过滤栏是绿色说明语法没有问题
使用wireshark进行抓包 启动服务器程序 启动windows上的telnet服务 telnet [服务器公网ip] [端口号]进入这个界面就代表启动成功了。 观察报文 三次握手 telnet发送1字节的数据 点击某一个包下面可以看到更详细的信息 四次挥手telnet输入ctrl ]进入命令行模式然后点quit就可退出
红色的报文为超时重传。