当前位置: 首页 > news >正文

网络基础1(应用层、传输层)

目录

一、应用层

1.1 序列化和反序列化        

1.2 HTTP协议        

1.2.1 URL

1.2.2 HTTP协议格式       

1.2.3 HTTP服务器示例       

二、传输层        

2.1 端口号       

2.1.1 netstat       

2.1.2 pidof

2.2 UDP协议

2.2.1 UDP的特点        

2.2.2 基于UDP的应用层协议

2.3 TCP协议

2.3.1 确认应答(ACK)机制       

2.3.2 超时重传机制    

2.3.3 连接管理机制        

2.3.4 TIME_WAIT状态       

2.3.5 CLOSE_WAIT 状态       

2.3.6 滑动窗口       

2.3.7 流量控制        

2.3.8 拥塞控制

2.3.9 延迟应答        

2.3.10 面向字节流

2.3.11 粘包问题

2.3.12 TCP异常情况

2.3.13 TCP小结

extra 用UDP实现可靠传输

1. 引入序列号

2. 引入确认应答

3. 引入超时重传

4. 数据包的格式

5. 重传机制

6. 简单示例代码(C++)



一、应用层

        在 OSI 七层模型中,应用层是最顶层,它直接与用户应用程序互动。程序员写的网络应用程序,像是 HTTP 客户端、FTP 客户端等,都工作在应用层,解决具体的业务需求。而协议是通信双方在应用层交换数据时所遵循的规则和约定,它定义了数据如何组织、如何发送、如何接收。通常,网络通信中的数据是以字节流的形式传输的,这些字节流需要在发送端和接收端之间按某种约定进行解析。网络接口(如 socket API)在发送和接收数据时,默认处理的是“字节流”或“字符串”格式。但是如果我们需要传输更复杂的结构化数据(比如一个对象、一个数组,或者包含多个字段的复杂数据)该怎么办呢?     

1.1 序列化和反序列化        

        例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。

方案一:直接发送表达式字符串

        在这个方案中,客户端将加法表达式(例如 "1+1")发送给服务器,服务器解析字符串并计算结果。然后,服务器将结果以某种格式返回给客户端。

方案二:使用结构体序列化和反序列化

        在这个方案中,我们不直接传输字符串,而是定义一个结构体来表示加法操作的信息。然后,我们将结构体序列化为字符串进行传输,接收方接收到字符串后进行反序列化,恢复原始数据结构进行加法计算。

struct AddRequest {int num1;  // 第一个加数int num2;  // 第二个加数
};struct AddResponse {int result;  // 计算结果
};// 客户端代码
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>struct AddRequest {int num1;int num2;
};int main() {int sockfd;struct sockaddr_in server_addr;AddRequest request = {1, 1};  // 客户端传递的加法请求// 创建 socketsockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {std::cerr << "Error creating socket!" << std::endl;return 1;}// 设置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_port = htons(12345);server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");// 连接到服务器if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "Connection failed!" << std::endl;return 1;}// 发送结构体数据send(sockfd, &request, sizeof(request), 0);// 接收结果char buffer[1024];int n = recv(sockfd, buffer, sizeof(buffer), 0);buffer[n] = '\0';std::cout << "Server result: " << buffer << std::endl;close(sockfd);return 0;
}// 服务器端代码
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>struct AddRequest {int num1;int num2;
};struct AddResponse {int result;
};int main() {int sockfd, new_sockfd;struct sockaddr_in server_addr, client_addr;socklen_t addr_size;AddRequest request;AddResponse response;// 创建 socketsockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {std::cerr << "Error creating socket!" << std::endl;return 1;}// 设置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_port = htons(12345);server_addr.sin_addr.s_addr = INADDR_ANY;// 绑定服务器地址if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "Bind failed!" << std::endl;return 1;}// 监听端口if (listen(sockfd, 10) == 0) {std::cout << "Server listening on port 12345..." << std::endl;} else {std::cerr << "Listen failed!" << std::endl;return 1;}// 接受客户端连接addr_size = sizeof(client_addr);new_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_size);// 接收数据recv(new_sockfd, &request, sizeof(request), 0);// 进行加法运算response.result = request.num1 + request.num2;// 发送结果send(new_sockfd, &response, sizeof(response), 0);close(new_sockfd);close(sockfd);return 0;
}

        无论是采用方案一、方案二,还是其他方案,只要确保通信双方在发送和接收数据时遵循一定的规则(即协议),就能够确保数据在两端能够正确地解析和理解。这个规则或约定就被称为 应用层协议。        

                

1.2 HTTP协议        

        虽然作为程序员,我们可以自定义应用层协议,但其实很多情况下,已经有很多成熟且经过广泛使用的应用层协议可以供我们直接参考和使用,例如,HTTP(HyperText Transfer Protocol)就是当前互联网应用中最广泛使用的协议之一。

1.2.1 URL

        平时我们常说的“网址”其实就是指 URL(统一资源定位符,Uniform Resource Locator)。URL是用来表示互联网上资源位置的字符串,它告诉浏览器如何找到某个特定的网页、文件或其他网络资源。 例如:

        在 URL 中,某些字符具有特定的意义。例如,/ 用于路径分隔,? 用于分隔查询参数,& 用于连接多个查询参数,# 用于锚点。因此,当这些字符出现在 URL 的某个部分时,如果需要作为数据的一部分而不是特殊意义的分隔符,就必须进行 转义,以确保它们不会引起歧义。urlencodeurldecode 是常见的两个操作,它们用于处理 URL 中的特殊字符。                  urlencode 是将字符串中的特殊字符转换为 URL 编码格式。它的规则是将字符转换为 百分号编码(Percent Encoding),也称为 URL 编码。这个过程将字符转化为它们对应的 ASCII 码的十六进制表示,并在前面加上 % 符号。urldecode 是将 URL 编码的字符串解码回原始的字符串。这个过程会把 % 后跟着的十六进制值转换为对应的字符。

原字符:Hello World!
URL编码:Hello%20World%21空格" " 转换为 %20/ 转换为 %2F? 转换为 %3F: 转换为 %3A! 被编码为 %21
编码规则:取字符的 ASCII 码值(例如字符 "A" 的 ASCII 码是 65,十六进制是 41)。将该 ASCII 码值转为两位十六进制表示(即 %41)。对每个字符进行类似的编码。

        

1.2.2 HTTP协议格式       

HTTP请求格式

  • 首行: [方法] + [url] + [版本]
  • Header: 由多个键值对组成,每一组键值对由冒号(:)分隔,且每一组属性之间使用换行符(\n)分隔,遇到空行表示Header部分结束
  • Body: 空行后面的内容都是Body,Body允许为空字符串,如果Body存在,则在Header中会有一个Content-Length属性来标识Body的长度
POST /submit HTTP/1.1    //首行
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html, application/xhtml+xml
Content-Length: 45name=John&age=30

HTTP响应格式        

  • 首行: [版本号] + [状态码] + [状态码解释]
  • Header: 由多个键值对组成,每一组键值对由冒号(:)分隔,且每一组属性之间使用换行符(\n)分隔,遇到空行表示Header部分结束
  • Body: 空行后面的内容都是Body,Body允许为空字符串,如果Body存在,则在Header中会有一个Content-Length属性来标识Body的长度;如果服务器返回了一个html页面,那么html页面内容就是在body中
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Server: Apache/2.4.1<html><head><title>Example</title></head><body><h1>Hello, World!</h1></body>
</html>

HTTP的方法

方法说明支持的HTTP协议版本
GET获取资源1.0、1.1
POST传输实体主体1.0、1.1
PUT传输文件1.0、1.1
HEAD获得报文首部1.0、1.1
DELETE删除文件1.0、1.1
OPTIONS询问支持的方法1.1
TRACE追踪路径1.1
CONNECT要求用隧道协议连接代理1.1
LINK建立和资源之间的联系1.0
UNLINE断开连接关系1.0

HTTP的状态码        

类别原因短语
1XXInformational(信息性状态码)
2XXSuccess(成功状态码)
3XXRedirection(重定向状态码)
4XXClient Error(客户端错误状态码)
5XXServer Error(服务器错误状态码)

        常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway).

        
HTTP常见Header

  • Content-Type: 数据类型(text/html等);
  • Content-Length: Body的长度;
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;
  • location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能

1.2.3 HTTP服务器示例       

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void Usage() {printf("usage: ./server [ip] [port]\n");
}
int main(int argc, char* argv[]) {if (argc != 3) {Usage();return 1;}int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("socket");return 1;}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(argv[1]);addr.sin_port = htons(atoi(argv[2]));int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return 1;}ret = listen(fd, 10);if (ret < 0) {perror("listen");return 1;}for (;;) {struct sockaddr_in client_addr;socklen_t len;int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);if (client_fd < 0) {perror("accept");continue;}char input_buf[1024 * 10] = { 0 }; // 用一个足够大的缓冲区直接把数据读完.ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);if (read_size < 0) {return 1;}printf("[Request] %s", input_buf);char buf[1024] = { 0 };const char* hello = "<h1>hello world</h1>";sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello), hello);write(client_fd, buf, strlen(buf));}return 0;
}

        如果是在云服务器上,可以先复制ssh渠道,在一端启动服务器,另一端使用curl命令进行测试。        

        


二、传输层        

传输层负责数据能够从发送端传输到接收端。        

2.1 端口号       

        端口号(Port)标识了一个主机上进行通信的不同的应用程序; 在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过
netstat -n查看).

端口号范围划分

  • 0 - 1023: 常见端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 它们的端口号都是固定的. 我们写一个程序使用端口号时, 要避开这些知名端口号
    • ssh服务器, 使用22端口
    • ftp服务器, 使用21端口
    • telnet服务器, 使用23端口
    • http服务器, 使用80端口
    • https服务器, 使用443
  • 1024 - 65535: 操作系统动态分配的端口号.  客户端程序的端口号, 就是由操作系统从这个范围分配的

注意:

        一个进程可以 bind 多个端口号。你可以通过创建多个套接字,每个套接字绑定到不同的端口来实现。

        一个端口号通常只能被一个进程绑定。但是,在某些特定的条件下,多个进程可以绑定到相同的端口,尤其在高并发或多进程/多线程环境下。

        

2.1.1 netstat       

        netstat是一个用来查看网络状态的重要工具.

语法:netstat [选项]        

功能:查看网络状态

常用选项:

  • -t:显示 TCP 连接。
  • -u:显示 UDP 连接。
  • -l:仅显示在监听状态的端口。
  • -p:显示哪个进程在使用相应的端口。
  • -n:以数字方式显示地址和端口号,而不是将其解析为主机名和服务名。
  • -a:显示所有的连接和监听端口。
  • -r:显示路由信息。
  • -i:显示网络接口的信息。
  • -s:显示网络统计信息。
//netstat 的输出示例如下(部分):
Proto  Recv-Q  Send-Q   Local Address    Foreign Address    State       PID/Program name
tcp       0       0     0.0.0.0:22       0.0.0.0:*          LISTEN      1234/sshd
tcp6      0       0     :::80            :::*               LISTEN      5678/nginx

        

2.1.2 pidof

        pidof 是一个用于查找给定程序名称对应的进程 ID (PID) 的命令

语法:pidof [进程名]
功能:通过进程名, 查看进程id

        有时,你可能希望查看某个端口正在被哪个进程使用,这时候 netstatpidof 可以结合使用。例如,假设你想要查看端口 8080 被哪个进程占用,你可以使用以下步骤:        

$ netstat -tulnp | grep :8080
tcp6       0      0 :::8080    :::*    LISTEN      1234/nginx$ pidof nginx
1234

        

2.2 UDP协议

        源端口号(Source Port):16 位,表示发送端的端口号;如果不需要返回信息,源端口可以设置为 0。

        目的端口号(Destination Port):16 位,表示接收端的端口号。此字段由应用程序根据目标服务来设置。

        长度(Length):16 位,表示 UDP 数据报的总长度(包括头部和数据部分);UDP 最小长度为 8 字节(只包含头部),最大长度为 65535 字节(最大 16 位长度)。

        校验和(Checksum):16 位,用于错误检测;如果校验和出错, 就会直接丢弃。

        数据(Data):数据部分是可变长度的,具体的大小由 Length 字段指定;包含应用程序需要传输的内容。

        

2.2.1 UDP的特点        

  • 无连接:UDP 在数据传输前不需要建立连接,发送方知道接收方的 IP 地址和端口号后,就可以直接发送数据;
  • 不可靠:UDP 不提供数据传输的确认机制,也没有重传机制,若数据包丢失或发生错误,UDP 不会向应用层报告错误;
  • 面向数据报:UDP 以独立的数据报形式发送数据,每个数据报都是一个独立的单位,不能控制数据的读取次数和数量,且数据长度固定。

        在 UDP 中,应用层的数据是作为一个整体(数据报)直接传输的,UDP 协议会原样发送应用层传递的报文,并不会对数据进行拆分或合并,具体来说:如果应用层发送 100 字节的数据,UDP 就会以 100 字节的完整数据报形式发送出去。UDP 不会将其拆分成多个小数据包,且接收端必须接收与发送端相同大小的数据。例如,发送端调用 sendto 发送 100 字节数据,那么接收端必须通过 recvfrom 一次性接收 100 字节数据,不能分多次调用 recvfrom 来接收数据。        

        

2.2.2 基于UDP的应用层协议

  • NFS:允许客户端通过网络访问和共享远程服务器上的文件;
  • TFTP:用于通过网络传输小文件,通常用于设备启动和固件升级;
  • DHCP:自动为网络中的设备分配 IP 地址和其他配置信息;
  • BOOTP:为无盘工作站等设备提供启动所需的网络配置信息;
  • DNS:将域名解析为 IP 地址,便于用户访问互联网资源。

        

2.3 TCP协议

字段长度说明
源端口16位表示发送端的端口号
目标端口16位表示接收端的端口号
序列号32位包含数据流中的字节序列号,表示数据段的开始位置
确认号32位如果ACK标志位设置为1,确认号表示接收到的下一个期望字节的序列号
数据偏移(首部长度)4位表示TCP头部的长度,单位是4字节
保留字段6位保留为0,未来扩展使用
标志位(Flags)6位URG(紧急指针有效)、ACK(确认序列号有效)、PSH(推送功能)、RST(重置连接)、SYN(同步序列号)、FIN(结束连接)
窗口大小16位用于流量控制,表示接收窗口的大小
校验和16位用于数据的校验,确保数据传输过程中没有发生错误
紧急指针16位如果URG标志位为1,紧急指针指出紧急数据的结束位置
选项可变长度可选字段,通常用于TCP的扩展功能(如最大段大小MSS、时间戳等)
数据可变长度TCP段中实际传输的应用数据

        

2.3.1 确认应答(ACK)机制       

        确认应答(ACK)机制是TCP协议用来确保数据可靠传输的方式。简单来说,就是接收方在收到数据后,会给发送方一个确认信号(ACK),表示它已成功接收到数据。

2.3.2 超时重传机制    

        那TCP如何确定超时的时间呢?TCP的超时重传机制动态调整超时时间,以适应不同的网络环境。理想情况下,超时时间应确保确认应答能够在指定时间内返回,但实际应用中,网络条件的变化会影响这一时间。如果超时时间设置过长,可能降低重传效率;如果设置过短,可能导致重复包的发送。为保证高效的通信,TCP动态计算最大超时时间。在Linux(以及BSD Unix和Windows)系统中,超时以500ms为单位,每次重传的超时时间按2的指数倍递增(500ms、1000ms、2000ms等)。如果多次重传后仍未收到应答,TCP会认为网络或对端主机存在问题,最终强制关闭连接。        

2.3.3 连接管理机制        

服务端状态转化:

  • [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
  • [LISTEN -> SYN_RCVD] 一旦监听到连接请求, 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文, 响应客户端的连接请求;
  • [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了;
  • [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
  • [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN);
  • [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.

客户端状态转化:

  • [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
  • [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
  • [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
  • [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
  • [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
  • [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.        

2.3.4 TIME_WAIT状态       

        做一个测试,首先启动server,然后启动client,接着用Ctrl-C使server终止,最后马上再运行server,结果会报出这条错误信息:

        bind error: Address already in use
        出现该错误的原因是,尽管服务器应用程序已经终止,但TCP协议层的连接未完全断开,导致端口仍然被占用。这通常是因为TCP连接关闭后,主动关闭连接的一方会进入 TIME_WAIT 状态,等待一定时间(通常为2个最大报文生存时间,MSL),以确保网络中的延迟报文被清除。在此期间,端口无法重新绑定或监听。可以使用 netstat 命令查看端口占用情况,确认是否有连接仍在 TIME_WAIT 状态,或是否有其他进程在使用该端口。

        根据TCP协议,主动关闭连接的一方会进入 TIME_WAIT 状态,并必须等待两个MSL后才能返回 CLOSED 状态。当通过Ctrl-C终止服务器时,服务器是主动关闭连接的一方,因此在 TIME_WAIT 状态期间,无法重新监听相同的端口。默认的MSL值在RFC 1122中规定为两分钟,但不同操作系统的实现有所不同。例如,在CentOS 7中,默认的MSL值为60秒。

可以通过命令

        cat /proc/sys/net/ipv4/tcp_fin_timeout 查看当前MSL的配置值。
        注意,TIME_WAIT状态持续2个MSL(最大报文生存时间),是为了确保所有可能迟到的报文段在两个传输方向上都已经消失。MSL定义了TCP报文在网络中的最大生存时间,因此,在TIME_WAIT期间,能够确保即使服务器重启,也不会收到来自上一个连接的过期数据,这些数据很可能是无效的。其次,TIME_WAIT状态还保证了最后一个报文的可靠到达。如果最后一个ACK丢失,服务器会重新发送FIN报文,即使客户端的进程已经结束,TCP连接仍然存在,允许重发LAST_ACK,以确保连接的正常关闭。        

        

        在服务器的TCP连接未完全断开之前,无法重新监听端口,但在某些情况下,这种限制可能不合理。例如,服务器需要处理大量客户端连接,虽然每个连接的生存时间较短,但请求量非常大,且每秒都有大量客户端请求。在这种情况下,如果服务器主动关闭连接(如清理不活跃的客户端),会产生大量的 TIME_WAIT 连接。

        由于请求量庞大,TIME_WAIT 状态的连接数量可能很高,每个连接占用一个通信五元组(源IP、源端口、目标IP、目标端口和协议)。当新客户端连接时,如果其目标IP、目标端口与某个 TIME_WAIT 连接的五元组重复,就会出现端口占用问题。

        为了解决这个问题,可以通过使用 setsockopt() 设置 socket 描述符的选项 SO_REUSEADDR 为 1,允许创建端口号相同但IP地址不同的多个 socket 描述符,从而避免端口被 TIME_WAIT 状态占用。

int opt = l;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

2.3.5 CLOSE_WAIT 状态       

        在作者的《网络编程套接字》博客中的4.3实现了一个TCP通用服务器,如果在代码中删去 new_sock.Close(); 这条语句,然后再编译并运行服务器,启动客户端进行连接,检查 TCP 状态,发现客户端和服务器均处于 ESTABLISHED 状态,正常运行。当关闭客户端程序时,观察到服务器进入 CLOSE_WAIT 状态。结合四次挥手的流程图分析,可以推测四次挥手未能正确完成。

        服务器出现大量 CLOSE_WAIT 状态的原因是服务器未正确关闭 socket,导致四次挥手未能完成。这个问题是一个 BUG,通过在代码中添加适当的 close 操作即可解决。

//tcp_server.hpp#pragma once
#include <functional>
#include "tcp_socket.hpp"// 定义一个 Handler 类型,用于处理客户端请求和生成响应
typedef std::function<void(const std::string& req, std::string* resp)> Handler;class TcpServer {
public:// 构造函数,初始化服务器的 IP 和端口TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}// 启动服务器,处理客户端请求bool Start(Handler handler) {// 1. 创建监听用的 socketCHECK_RET(listen_sock_.Socket());// 2. 绑定服务器 IP 和端口到监听 socketCHECK_RET(listen_sock_.Bind(ip_, port_));// 3. 设置监听队列大小为 5CHECK_RET(listen_sock_.Listen(5));// 4. 进入事件循环,不断接受客户端连接for (;;) {// 5. 等待并接受客户端的连接TcpSocket new_sock;std::string ip;uint16_t port = 0;if (!listen_sock_.Accept(&new_sock, &ip, &port)) {continue;  // 接受失败则跳过}// 输出客户端连接信息printf("[client %s:%d] connect!\n", ip.c_str(), port);// 6. 进入与客户端的读写循环for (;;) {std::string req;// 7. 从客户端接收请求数据,若失败则断开连接bool ret = new_sock.Recv(&req);if (!ret) {printf("[client %s:%d] disconnect!\n", ip.c_str(), port);// [注意!] 客户端断开连接时需要关闭与客户端的 socket// new_sock.Close();    // !!!!!! 删除这条语句 break;  // 退出循环,处理下一个客户端}// 8. 处理请求并生成响应std::string resp;handler(req, &resp);// 9. 将响应数据发送回客户端new_sock.Send(resp);// 输出请求和响应数据printf("[%s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());}}return true;  // 启动成功}private:TcpSocket listen_sock_;  // 监听用的 TcpSocket 对象std::string ip_;         // 服务器 IP 地址uint64_t port_;          // 服务器端口号
};

2.3.6 滑动窗口       

        在刚才的讨论中,我们提到了一种确认应答策略,即每发送一个数据段都需要等待一个 ACK 确认应答,然后再发送下一个数据段。该策略的一个主要缺点是性能较差,尤其是在数据传输往返时间较长时。

        为了提高性能,我们可以一次性发送多个数据段,而不需要等待每个数据段的确认应答。通过重叠等待时间,能够显著提升吞吐量。图中的窗口大小表示无需等待确认应答即可继续发送数据的最大量(例如,4000 字节,即四个数据段)。在发送前四个数据段时,无需等待任何 ACK,直接发送;在收到第一个 ACK 后,滑动窗口向后移动,继续发送下一个数据段,依此类推。

        操作系统内核会通过发送缓冲区来维护滑动窗口,记录哪些数据段还没有收到 ACK。只有在接收到对应的 ACK 后,数据才会从缓冲区中删除。窗口越大,能够并行发送的数据就越多,从而提高网络的吞吐率。

        

当发生丢包时,重传机制可以通过以下两种情况进行处理:

情况一:ACK丢失
        如果数据包已成功到达接收端,但部分 ACK 丢失,通常不会造成问题,因为发送端可以通过后续的 ACK 确认已收到的数据包。

情况二:数据包丢失
        当某一数据包丢失时,接收端会持续向发送端发送相同的 ACK(例如 "1001"),表示接收端期待重新接收该数据。若发送端连续收到三次相同的 ACK(如 "1001"),则会重新发送丢失的数据段(例如 1001 至 2000)。接收端在收到重传数据后,会返回新的 ACK(如 "7001"),这表示接收端已成功接收到之前的 2001 至 7000 的数据,这些数据已被存储在接收缓冲区中。

        这种机制被称为“快重传”或“高速重发控制”,它通过快速识别丢包并触发重传,有效提高数据传输的可靠性和效率。

2.3.7 流量控制        

        接收端的处理速度是有限的。如果发送端发送数据过快,可能导致接收端的缓冲区被填满,从而引发丢包、重传等一系列问题。为了避免这种情况,TCP采用流量控制机制(Flow Control),根据接收端的处理能力来调整发送端的发送速率。

        在流量控制机制中,接收端将自身可用的缓冲区大小放在 TCP 头部的“窗口大小”字段中,通过 ACK 报文通知发送端。窗口大小越大,表示接收端能够处理的数据量越多,网络的吞吐量也越高。

        当接收端发现缓冲区快满时,会将窗口大小设置为较小的值并通知发送端,要求发送端减缓数据发送速度。如果缓冲区已满,接收端会将窗口大小设置为0,发送端此时暂停数据发送。然而,发送端仍会定期发送窗口探测数据段,以便接收端告知当前的窗口大小,并恢复正常的数据传输。

        接收端通过 TCP 头部中的 16 位窗口字段来向发送端告知窗口大小。该字段存储的是窗口大小的值,最大可表示 65535。然而,这并不意味着 TCP 窗口的最大大小就是 65535 字节。实际上,TCP 头部的 40 字节选项部分还包含一个窗口扩大因子 M。实际的窗口大小是通过将窗口字段的值左移 M 位来计算的。        

2.3.8 拥塞控制

        拥塞控制的核心目标是 在保证可靠性和高效性的同时,尽量避免过载网络。通过慢启动算法快速探索带宽,避免一开始就向网络发送过多数据;通过慢启动阈值、指数增长和线性增长相结合的方式,平衡网络的吞吐量和稳定性;在发生丢包或超时时,及时调整拥塞窗口,以适应当前的网络状况。

        

  1. 拥塞窗口(Congestion Window,cwnd):拥塞窗口是 TCP 协议中控制数据流量的一个重要参数。它决定了发送方每次可以发送多少数据。该值动态变化,根据网络的拥塞状态来调整。

  2. 慢启动:TCP 连接初始阶段,拥塞窗口从 1 开始,每收到一个确认应答(ACK),拥塞窗口大小增加 1(或按某些实现为增长一段更大的值),以指数方式增长。初期阶段,TCP 慢启动算法尝试快速探索网络的带宽容量,以便尽快达到一个适合的发送速度。

  3. 慢启动阈值(ssthresh):在慢启动过程中,当拥塞窗口达到一个阈值(ssthresh)时,窗口增长方式会发生变化:从指数增长转为线性增长。此时,拥塞窗口的增加速度减慢,避免网络过度拥堵。该阈值的设置非常关键,合理的阈值有助于平衡吞吐量和网络拥堵的关系。如果网络中发生了丢包或超时重传,通常会触发调整阈值的操作。每次超时后,慢启动阈值会减半,并且拥塞窗口会重新设置为 1。

  4. 拥塞控制的其他机制快重传和快恢复:当接收端收到重复的 ACK 时,TCP 会认为某些数据包丢失,立即触发快速重传,减少拥塞的延迟,并尽快恢复正常的发送速率。拥塞避免:当拥塞窗口大于慢启动阈值时,TCP 会逐渐增加拥塞窗口的大小,通常是每经过一个 RTT(往返时延),拥塞窗口会增加 1(线性增长)。这种方式避免了指数增长带来的过度拥堵。

  5. 网络拥堵的感知丢包:如果发送的数据包丢失,通常是因为网络的拥塞。TCP 通过检测丢包情况来判断网络是否发生拥塞,并相应调整发送速率。超时重传:如果数据包的确认 ACK 在预定时间内没有到达,TCP 会重传该数据包并触发拥塞控制机制,减缓数据发送速率。

2.3.9 延迟应答        

        当接收端接收到数据后,通常会立即返回 ACK 来确认接收情况。但如果每次都立刻发送 ACK,网络上的 ACK 包数量就会增加,造成带宽浪费。在延迟应答中,接收端会在一定时间内或在收到一定数量的数据后再发送 ACK。这可以让窗口的大小更大,从而提高吞吐量和传输效率。

        延迟应答有两种限制数量限制:即在接收到一定数量的数据包后才进行 ACK 应答。例如,接收端可能设定每接收到 2 个数据包才返回一次 ACK。时间限制:即如果接收端在某段时间内没有收到新的数据包,它会在设定的最大延迟时间(如 200ms)内发送 ACK。

        延迟应答可以使接收端能够在一个 ACK 中传递更大的窗口大小。由于接收端在延迟应答时已经处理掉一部分数据,它可以返回一个更大的窗口大小,允许发送方发送更多的数据。更大的接收窗口意味着可以发送更多数据,从而增加吞吐量。但这必须在不导致网络拥塞的情况下进行,否则会产生反效果。

        

        捎带应答(Piggybacking)是一种优化策略,通常在延迟应答的基础上进行。它通过将 ACK 消息和数据消息一起发送,从而减少网络中独立 ACK 的数量。具体来说,当客户端发送请求并等待服务器的回应时,服务器不仅仅会响应请求的数据,还可以在回应数据的同时,返回客户端的 ACK 消息。

        

2.3.10 面向字节流

        创建一个 TCP 套接字时,内核会同时为该套接字分配一个发送缓冲区和一个接收缓冲区。当调用 write 函数时,数据首先会被写入发送缓冲区。如果待发送的数据字节数较大,系统会将数据分割成多个 TCP 数据包发送;如果数据量较小,则会在缓冲区中等待,直到积累到一定的长度或其他合适的时机,再进行发送。

        接收数据时,数据通过网卡驱动程序传入内核的接收缓冲区,然后应用程序可以调用 read 函数从接收缓冲区读取数据。

        TCP 连接的特点是,每个连接都有独立的发送缓冲区和接收缓冲区,因此在同一连接中,既可以进行数据的读取,也可以进行数据的写入,这种机制被称为全双工通信。

        由于缓冲区的存在,TCP 的读写操作不需要严格匹配。例如,向缓冲区写入 100 字节数据时,可以通过一次 write 操作完成,也可以通过 100 次 write 操作,每次写入一个字节。同样,读取 100 字节数据时,应用程序不需要关心数据是如何写入的,可以一次性调用 read 读取 100 字节,也可以调用 100 次 read,每次读取一个字节。

2.3.11 粘包问题

        在 TCP 中,由于其流式传输的特性,数据以字节流的方式进行传送,并没有明确的“边界”来分隔不同的数据包。这就导致了应用层无法直接区分哪些字节属于同一个数据包,哪些字节属于另一个数据包。为了解决这个问题,应用层需要通过以下方式来明确边界:

  • 固定长度包:对于定长的数据包,应用层可以按照固定大小读取缓冲区的内容。例如,如果每个数据包都是 1024 字节,应用程序可以每次读取 1024 字节来获得一个完整的数据包。
  • 包头包含数据长度:对于变长数据包,应用层可以在包头中预留一个字段来存储该包的总长度。这样,在接收数据时,就可以通过读取包头来知道整个数据包的长度,从而确保正确读取完整数据包。
  • 特殊分隔符:应用层也可以设计特定的分隔符来分隔不同的数据包,只要这些分隔符在数据正文中不会出现。

       在 UDP 中,由于每个 UDP 数据包都是独立传输的,并且 UDP 本身是面向数据报的协议,存在一个很明确的数据边界。因此,UDP 不存在类似 TCP 中的粘包问题。

2.3.12 TCP异常情况

        进程终止:当一个进程终止时,操作系统会释放它所持有的文件描述符,但这并不意味着 TCP 连接立刻关闭。系统会发送一个 FIN 包,表示连接的另一端可以开始进行正常的连接关闭流程。与正常的 TCP 连接关闭类似,进程终止不会导致连接立即被重置。

        机器重启:机器重启和进程终止的情况类似,所有与该机器相关的网络连接都会被丢弃。当机器重启时,连接会被强制断开,接收方依然认为连接存在,直到它尝试进行数据写入时才会发现连接已经不存在,通常会收到 RST 包(重置连接)作为响应。

        机器掉电/网线断开:机器掉电或者网线断开时,接收端会认为连接依然存在,并不会立刻察觉到连接已中断。等到接收端尝试进行写入操作时,它会发现连接已经断开,这时 TCP 会发送 RST 包来重置连接。在这种情况下,接收端可能通过定期探测来判断连接的存活状态。比如 TCP 有内置的 保活定时器,用于检查连接是否还存活。如果检测到连接已经断开,它会自动释放该连接。

        应用层协议检测:很多应用层协议(例如 HTTP 长连接)也会实现定期的心跳检测机制,用于确保连接的有效性。例如,HTTP 长连接会定期发送 ping/pong 消息,或者通过设置超时时间来检测连接是否仍然有效。即使在即时通讯软件(如 QQ)中,断开连接后,客户端通常会尝试周期性地重新连接。

2.3.13 TCP小结

        既要保证可靠性,同时又尽可能的提高性能,这就注定了TCP非常复杂。

        可靠性机制:校验和、序列号(按序到达)、确认应答、超时重发、连接管理、流量控制、拥塞控制;

        提高性能:滑动窗口、快速重传、延迟应答、捎带应答;

        基于TCP应用层协议:HTTP、HTTPS、SSH、Telnet、FTP、SMTP。

extra 用UDP实现可靠传输

        使用 UDP 实现可靠传输协议的核心思路是参考 TCP 的可靠性机制,包括顺序控制、确认应答、超时重传等。因为 UDP 本身并不保证数据的可靠传输,所以我们可以在应用层设计和实现这些机制。

        

1. 引入序列号

        为了保证数据按顺序传输,需要给每个数据包分配一个序列号。接收方根据序列号来确定数据包的顺序。

  • 每个数据包都包含一个 序列号
  • 接收方接收到数据包后,检查序列号,如果序列号顺序正确,进行处理;否则,丢弃或请求重发。

2. 引入确认应答

        每个数据包传输后,接收方会发送一个确认应答(ACK)给发送方,表示该数据包已成功接收。如果发送方在超时前没有收到确认,应当进行重传。

  • 每个数据包发送后,发送方等待接收方的确认。
  • 如果接收方收到数据包且顺序正确,发送一个 ACK 确认消息给发送方。
  • 如果发送方未在规定时间内收到确认,则重新发送数据包。

3. 引入超时重传

        超时重传机制是可靠传输协议的核心。发送方会设置一个 定时器,在一定时间内等待确认应答。如果超时没有收到确认,重新发送数据包。

  • 每发送一个数据包后,启动定时器。
  • 如果在定时器超时前收到了确认应答,停止定时器。
  • 如果超时仍未收到确认,重发数据包。

4. 数据包的格式

        每个数据包的格式需要包括以下字段:

  • 序列号:标识数据包的顺序。
  • 数据:传输的实际数据。
  • 确认号(可选):如果是确认应答包,表示接收到的数据包的序列号。
  • 校验和:保证数据的完整性。

5. 重传机制

        我们还需要一个 滑动窗口 机制,用来控制哪些数据包已经成功接收,并允许发送方根据接收方的确认信息进行有序重传。

6. 简单示例代码(C++)

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <iomanip>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 12345
#define MAX_RETRIES 5
#define TIMEOUT 2  // 超时设置为2秒// 发送方
void send_data(const std::string &data, const std::string &receiver_ip) {int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {std::cerr << "Socket creation failed!" << std::endl;return;}struct sockaddr_in receiver_addr;receiver_addr.sin_family = AF_INET;receiver_addr.sin_port = htons(PORT);receiver_addr.sin_addr.s_addr = inet_addr(receiver_ip.c_str());int seq_num = 0;int retries = 0;while (retries < MAX_RETRIES) {// 构造数据包:序列号 + 数据std::string packet = std::to_string(seq_num) + ":" + data;// 发送数据包ssize_t sent = sendto(sockfd, packet.c_str(), packet.size(), 0, (struct sockaddr*)&receiver_addr, sizeof(receiver_addr));if (sent < 0) {std::cerr << "Failed to send data!" << std::endl;break;}std::cout << "Sent: " << packet << std::endl;// 设置接收超时struct timeval tv;tv.tv_sec = TIMEOUT;tv.tv_usec = 0;setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));// 等待确认char ack_buffer[1024];socklen_t len = sizeof(receiver_addr);ssize_t recv_len = recvfrom(sockfd, ack_buffer, sizeof(ack_buffer), 0, (struct sockaddr*)&receiver_addr, &len);if (recv_len >= 0) {ack_buffer[recv_len] = '\0';  // 确保字符串结束int ack_seq_num = std::stoi(ack_buffer);  // 获取确认号std::cout << "Received ACK: " << ack_seq_num << std::endl;// 如果确认号与发送的序列号一致,表示确认成功if (ack_seq_num == seq_num) {std::cout << "Data successfully acknowledged." << std::endl;break;  // 数据发送成功,退出重传} else {std::cout << "Incorrect ACK received." << std::endl;}} else {std::cout << "Timeout, resending..." << std::endl;retries++;seq_num++;  // 增加序列号,准备重发}}if (retries == MAX_RETRIES) {std::cout << "Max retries reached. Failed to send data." << std::endl;}close(sockfd);
}// 接收方
void receive_data() {int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {std::cerr << "Socket creation failed!" << std::endl;return;}struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "Bind failed!" << std::endl;close(sockfd);return;}int expected_seq_num = 0;while (true) {char buffer[1024];struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &len);if (recv_len < 0) {std::cerr << "Failed to receive data!" << std::endl;continue;}buffer[recv_len] = '\0';  // 确保字符串结束std::string received_data(buffer);size_t colon_pos = received_data.find(":");if (colon_pos != std::string::npos) {int seq_num = std::stoi(received_data.substr(0, colon_pos));std::string content = received_data.substr(colon_pos + 1);// 如果序列号匹配,处理数据并发送确认if (seq_num == expected_seq_num) {std::cout << "Data received: " << content << std::endl;expected_seq_num++;  // 更新期望的序列号// 发送确认消息std::string ack = std::to_string(seq_num);sendto(sockfd, ack.c_str(), ack.size(), 0, (struct sockaddr*)&client_addr, len);} else {std::cout << "Out-of-order packet. Expected " << expected_seq_num << ", but got " << seq_num << "." << std::endl;std::string ack = std::to_string(expected_seq_num - 1);  // 发送最后一次成功的确认sendto(sockfd, ack.c_str(), ack.size(), 0, (struct sockaddr*)&client_addr, len);}}}close(sockfd);
}int main() {std::thread receiver_thread(receive_data);std::this_thread::sleep_for(std::chrono::seconds(1));  // 等待接收方启动send_data("Hello, UDP!", "127.0.0.1");receiver_thread.join();return 0;
}

        


        

        

        

        

        

        

相关文章:

网络基础1(应用层、传输层)

目录 一、应用层 1.1 序列化和反序列化 1.2 HTTP协议 1.2.1 URL 1.2.2 HTTP协议格式 1.2.3 HTTP服务器示例 二、传输层 2.1 端口号 2.1.1 netstat 2.1.2 pidof 2.2 UDP协议 2.2.1 UDP的特点 2.2.2 基于UDP的应用层…...

使用Spring Boot集成Nacos

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它集成了服务发现、服务配置和服务管理等功能&#xff0c;是微服务架构中一个非常重要的组件。以下是使用Spring Boot集成Nacos的详细步骤。 1. 环境准备 确保你已经安装和配置了以下环境&#xff1…...

破局智算瓶颈:400G光模块如何重构AI时代的网络神经脉络

一、技术演进与市场需求双重驱动 在数字化转型浪潮下&#xff0c;全球互联网流量正以每年30%的复合增长率持续攀升。根据Dell’Oro Group最新报告&#xff0c;2023年400G光模块市场规模已突破15亿美元&#xff0c;预计2026年将占据数据中心光模块市场60%以上份额。这种爆发式增…...

Vue:插值表达

Vue 的插值表达式是数据绑定的基础形式&#xff0c;它通过 {{ }} 将 JavaScript 数据动态渲染到模板中。下面通过代码示例&#xff0c;直观感受它的用法和限制。 基础用法 <template>&#xff1c;div&#xff1e;<!-- 直接显示数据 --><p&#xff1e;{{ messag…...

26考研|数学分析:函数列与函数项级数

前言 函数列与函数项级数这一章虽然课本安排章节较少&#xff0c;只要两小节&#xff0c;但是在具体学习过程中&#xff0c;确实会有一定的难度&#xff0c;首先难点便是在对于函数列与函数项级数的理解&#xff0c;其次关于一致收敛性质的理解与判断&#xff0c;也是难点所在…...

设置环境变量启动jar报

1. 环境变量设置 set PATHC:\Program Files\java17\jdk-17.0.9\bin;%PATH%2. 启动jar java -jar jar包名3. 记录原因 PATH路径前添加java执行文件路径才会管用。添加后可以试试以下命令 直接输入PATH 回车 PATH进行java版本测试 java -version...

项目售后服务承诺书,软件售后服务方案,软件安装文档,操作文档,维护文档(Word原件)

一、系统安全性保障 &#xff08;一&#xff09;设计原则 &#xff08;二&#xff09;应用安全 &#xff08;三&#xff09;数据安全 &#xff08;四&#xff09;用户安全 &#xff08;五&#xff09;管理安全 二、售后服务 &#xff08;一&#xff09;服务总体要…...

Arduino快速入门

Arduino快速入门指南 一、硬件准备 选择开发板&#xff1a; 推荐使用 Arduino UNO&#xff08;兼容性强&#xff0c;适合初学者&#xff09;&#xff0c;其他常见型号包括NANO&#xff08;体积小&#xff09;、Mega&#xff08;接口更多&#xff09;。准备基础元件&#xff1a…...

每日一题——樱桃分级优化问题:最小化标准差的动态规划与DFS解决方案

文章目录 一、问题描述输入格式输出格式 二、问题本质分析三、解题思路1. 前缀和预处理2. DFS 枚举与剪枝3. 剪枝策略4. 标准差计算 四、代码实现五、样例解析样例 1样例 2 六、一行行代码带你敲dfs 七、总结 一、问题描述 某大型樱桃加工厂使用自动化机械扫描了一批樱桃的尺寸…...

C++类与对象(二):六个默认构造函数(一)

在学C语言时&#xff0c;实现栈和队列时容易忘记初始化和销毁&#xff0c;就会造成内存泄漏。而在C的类中我们忘记写初始化和销毁函数时&#xff0c;编译器会自动生成构造函数和析构函数&#xff0c;对应的初始化和在对象生命周期结束时清理资源。那是什么是默认构造函数呢&…...

荣耀手机,系统MagicOS 9.0 USB配置没有音频来源后无法被adb检测到,无法真机调试的解决办法

荣耀手机&#xff0c;系统MagicOS 9.0 USB配置没有音频来源后无法被adb检测到&#xff0c;无法真机调试的解决办法 前言环境说明操作方法 前言 一直在使用的uni-app真机运行荣耀手机方法&#xff0c;都是通过设置USB配置的音频来源才能成功。突然&#xff0c;因为我的手机的系…...

每日分享-Python哈希加盐加密实战分享

没事找事干&#xff0c;找到本地有个hashdemo.py&#xff0c;那就来分享一下代码吧&#xff0c;主要内容就是使用python实现哈希加盐加密方式。 1、导入所需库 不多BB&#xff0c;先打开我们的 pychram 然后导入所需要用到的库 import hashlib import random import strin…...

Webpack中Compiler详解以及自定义loader和plugin详解

Webpack Compiler 源码全面解析 Compiler 类图解析&#xff1a; 1. Tapable 基类 Webpack 插件系统的核心&#xff0c;提供钩子注册&#xff08;plugin&#xff09;和触发&#xff08;applyPlugins&#xff09;能力。Compiler 和 Compilation 均继承此类&#xff0c;支持插件…...

deepseek-coder-6.7b-instruct安装与体验-success

目录 步骤1:安装环境 步骤2:下载模型 步骤3:安装依赖 步骤4:运行模型 报错NameError: name torch is not defined 步骤5:运行结果 步骤1:安装环境 pip install modelscope 步骤2:下载模型 modelscope download --model deepseek-ai/deepseek-coder-6.7b-instruct --lo…...

对抗进行性核上性麻痹,健康护理筑牢生活防线

进行性核上性麻痹是一种复杂的神经退行性疾病&#xff0c;主要影响患者的运动、平衡及吞咽等功能&#xff0c;随着病情进展&#xff0c;患者生活质量会受到严重影响。除规范治疗外&#xff0c;科学的健康护理是提高患者生活质量、延缓病情发展的重要手段。 日常活动护理是基础。…...

科学养生,拥抱健康生活

在生活节奏日益加快的今天&#xff0c;养生不再是遥不可及的概念&#xff0c;而是可以融入日常的健康生活方式。即使抛开中医理念&#xff0c;通过科学的生活方式选择&#xff0c;也能为身体注入源源不断的活力。 从营养管理开始&#xff0c;构建科学的饮食体系。采用 “321 饮…...

基于若依框架的岗位名称查询模块实现

表名&#xff1a;sys_post&#xff08;若依自带的一个表&#xff09; 目标&#xff1a;获取post_name中所有的名字 模块结构说明 src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── ruoyi/ │ │ └── nametraversal/ │ │…...

(2)Python爬虫--requests

文章目录 前言一、 认识requests库1.1 前情回顾1.2 为什么要学习requests库1.3 requests库的基本使用1.4 响应的保存1.5 requests常用的方法1.6 用户代理1.7 requests库&#xff1a;构建ua池(可以先跳过去)1.8 requests库&#xff1a;带单个参数的get请求1.9 requests库&#x…...

springboot旅游小程序-计算机毕业设计源码76696

目 录 摘要 1 绪论 1.1研究背景与意义 1.2研究现状 1.3论文结构与章节安排 2 基于微信小程序旅游网站系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统…...

TCPIP详解 卷1协议 七 防火墙和网络地址转换

7.1——防火墙和网络地址转换 为防止终端系统不被攻击&#xff0c;需要一种方法来控制互联网中网络流量的流向。这项工作由防火墙来完成&#xff0c;它是一种能够限制所转发的流量类型的路由器。 随着部署防火墙来保护企业&#xff0c;另一个问题变得越来越重要&#xff1a;可…...

Golang 应用的 CI/CD 与 K8S 自动化部署全流程指南

一、CI/CD 流程设计与工具选择 1. 技术栈选择 版本控制&#xff1a;Git&#xff08;推荐 GitHub/GitLab&#xff09;CI 工具&#xff1a;Jenkins/GitLab CI/GitHub Actions&#xff08;本文以 GitHub Actions 为例&#xff09;容器化&#xff1a;Docker Docker Compose制品库…...

Jenkins:库博静态工具CI/CD 的卓越之选

在当今快节奏的软件开发领域&#xff0c;高效的持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;流程对于项目的成功至关重要。Jenkins 作为开源 CI/CD 软件的领导者&#xff0c;以其强大的功能、丰富的插件生态和高度的可扩展性&#xff0c;成为众多开…...

Maven私服搭建与登录全攻略

目录 1.背景2.简介3.安装4.启动总结参考文献 1.背景 回顾下maven的构建流程&#xff0c;如果没有私服&#xff0c;我们所需的所有jar包都需要通过maven的中央仓库或者第三方的maven仓库下载到本地&#xff0c;当一个公司或者一个团队所有人都重复的从maven仓库下载jar包&#…...

大模型数据分析破局之路20250512

大模型数据分析破局之路 本文面向 AI 初学者、数据分析从业者与企业技术负责人&#xff0c;围绕大模型如何为数据分析带来范式转变展开&#xff0c;从传统数据分析困境谈起&#xff0c;延伸到 LLM MCP 的协同突破&#xff0c;最终落脚在企业实践建议。 &#x1f30d; 开篇导语…...

数据结构-树(1)

一、树的基本概念 二&#xff0c;树的抽象数据结构 三&#xff0c;树的存储结构 1.双亲表示法 数组存储结点&#xff0c;含数据域和双亲下标&#xff08;根结点双亲为 - 1&#xff09; 代码示例 include <stdio.h> #include <stdlib.h>#define MAX_TREE_SIZE 10…...

什么是ERP?ERP有哪些功能?小微企业ERP系统源码,SpringBoot+Vue+ElementUI+UniAPP

什么是ERP&#xff1f; ERP翻译过来叫企业资源计划&#xff0c;通俗的讲&#xff0c;应该叫企业的全面预算控制&#xff0c;其通常包括三个部分&#xff1a;工程预算、投资预算和经营预算&#xff08;即产销存预算&#xff09;。之所以做预算控制&#xff0c;是因为企业运作的…...

视觉-语言-动作模型:概念、进展、应用与挑战(上)

25年5月来自 Cornell 大学、香港科大和希腊 U Peloponnese 的论文“Vision-Language-Action Models: Concepts, Progress, Applications and Challenges”。 视觉-语言-动作 (VLA) 模型标志着人工智能的变革性进步&#xff0c;旨在将感知、自然语言理解和具体动作统一在一个计…...

C++ 与 Go、Rust、C#:基于实践场景的语言特性对比

目录 ​编辑 一、语法特性对比 1.1 变量声明与数据类型 1.2 函数与控制流 1.3 面向对象特性 二、性能表现对比​编辑 2.1 基准测试数据 在计算密集型任务&#xff08;如 10⁷ 次加法运算&#xff09;中&#xff1a; 在内存分配测试&#xff08;10⁵ 次对象创建&#xf…...

RDB和AOF的区别

Redis提供两种主要的持久化机制&#xff1a;RDB&#xff08;Redis Database&#xff09;和AOF&#xff08;Append Only File&#xff09;&#xff0c;它们在数据持久化方式、性能影响及恢复策略上各有特点。以下是两者的对比分析及使用建议&#xff1a; RDB&#xff08;快照持久…...

因子分析基础指南:原理、步骤与地球化学数据分析应用解析

前言 在看深度学习成矿预测以及地球化学数据分析的文献的时候很多引言部分的内容会提到一些老的技术&#xff0c;正所谓&#xff1a;知其然知其所以然。所以我把关于一些老技术的基础铺垫的内容作为&#xff1a;研究生基础指南部分进行记录。 这部分讲述的是因子分析&#xf…...

采用AI神经网络降噪算法的通信语音降噪(ENC)模组性能测试和应用

采用AI降噪的语言通话环境抑制模组性能效果测试 随着AI时代来临.通话设备的环境噪音抑制也进入AI降噪算法时代. AI神经网络降噪技术是一款革命性的语音处理技术&#xff0c;他突破了传统单麦克风和双麦克风降噪的局限性,利用采集的各种日常环境中的噪音样本进行训练学习.让降噪…...

面试题 - Kafka、RabbitMQ、RocketMQ如何选型?

在当今的高并发、大数据时代&#xff0c;系统架构的复杂性呈指数级增长。你是否曾遇到过这样的问题&#xff1a;用户订单提交后&#xff0c;系统响应缓慢甚至卡顿&#xff1f;或者在业务高峰期&#xff0c;消息积压导致系统崩溃&#xff1f; 这些问题的背后&#xff0c;往往隐…...

【落羽的落羽 C++】stack和queue、deque、priority_queue、仿函数

文章目录 一、stack和queue1. 概述2. 使用3. 模拟实现 二、deque三、priority_queue1. 概述和使用2. 模拟实现 四、仿函数 一、stack和queue 1. 概述 我们之前学习的vector和list&#xff0c;以及下面要认识的deque&#xff0c;都属于STL的容器&#xff08;containers&#x…...

Golang 空结构体特性与用法

文章目录 1.简介2.核心特性2.1 零内存占用2.2 值比较语义2.3 类型隔离2.4 值地址 3.作用3.1 实现集合&#xff08;Set&#xff09;3.2 不发送数据的信道3.3 无状态方法接收者3.4 作为 context 的 value 的 key 4.小结参考文献 1.简介 在 Go 语言中&#xff0c;空结构体是一个不…...

企业对数据集成工具的需求及 ETL 工具工作原理详解

当下&#xff0c;数据已然成为企业运营发展过程中的关键生产要素&#xff0c;其重要性不言而喻。 海量的数据分散在企业的各类系统、平台以及不同的业务部门之中&#xff0c;企业要充分挖掘这些数据背后所蕴含的巨大价值&#xff0c;实现数据驱动的精准决策&#xff0c;数据集…...

基于HTTP头部字段的SQL注入:SQLi-labs第17-20关

前置知识&#xff1a;HTTP头部介绍 HTTP&#xff08;超文本传输协议&#xff09;头部&#xff08;Headers&#xff09;是客户端和服务器在通信时传递的元数据&#xff0c;用于控制请求和响应的行为、传递附加信息或定义内容类型等。它们分为请求头&#xff08;Request Headers&…...

Megatron系列——流水线并行

内容总结自&#xff1a;bilibili zomi 视频大模型流水线并行 注&#xff1a;这里PipeDream 1F1B对应时PP&#xff0c;Interleaved 1F1B对应的是VPP 1、朴素流水线并行 备注&#xff1a; &#xff08;1&#xff09;红色三个圈都为空泡时间&#xff0c;GPU没有做任何计算 &am…...

Android HttpAPI通信问题(待解决)

使用ClearTextTraffic是Android中一项重要的网络设置&#xff0c;它控制了应用程序是否允许在不使用HTTPS加密的情况下访问网络。在默认情况下&#xff0c;usescleartexttraffic的值为true&#xff0c;这意味着应用程序可以通过普通的HTTP协议进行网络通信。然而&#xff0c;这…...

WebFlux vs WebMVC vs Servlet 对比

WebFlux vs WebMVC vs Servlet 技术对比 WebFlux、WebMVC 和 Servlet 是 Java Web 开发中三种不同的技术架构&#xff0c;它们在编程模型、并发模型和适用场景上有显著区别。以下是它们的核心对比&#xff1a; 核心区别总览 特性ServletSpring WebMVCSpring WebFlux编程模型…...

Spring MVC参数传递

本内容采用最新SpringBoot3框架版本,视频观看地址:B站视频播放 1. Postman基础 Postman是一个接口测试工具,Postman相当于一个客户端,可以模拟用户发起的各类HTTP请求,将请求数据发送至服务端,获取对应的响应结果。 2. Spring MVC相关注解 3. Spring MVC参数传递 Spri…...

Spring MVC 和 Spring Boot 是如何访问静态资源的?

Spring MVC 和 Spring Boot 在配置静态资源访问方面有所不同&#xff0c;Spring Boot 提供了更便捷的自动配置。 一、Spring Boot 如何配置静态资源访问 (推荐方式) Spring Boot 遵循“约定优于配置”的原则&#xff0c;对静态资源的访问提供了非常方便的自动配置。 默认静态…...

如何应对网站被爬虫和采集?综合防护策略与实用方案

在互联网时代&#xff0c;网站内容被恶意爬虫或采集工具窃取已成为常见问题。这不仅侵犯原创权益&#xff0c;还可能影响网站性能和SEO排名。以下是结合技术、策略与法律的综合解决方案&#xff0c;帮助网站构建有效防护体系。 一、技术防护&#xff1a;阻断爬虫的“技术防线”…...

MySQL 分页查询优化

目录 前言1. LIMIT offset, count 的性能陷阱&#xff1a;为什么它慢&#xff1f;&#x1f629;2. 优化策略一&#xff1a;基于排序字段的“跳跃式”查询 (Seek Method) &#x1f680;3. 优化策略二&#xff1a;利用子查询优化 OFFSET 扫描 (ID Subquery)4. 基础优化&#xff1…...

我用Deepseek + 亮数据爬虫神器 1小时做出輿情分析器

我用Deepseek 亮数据爬虫神器 1小时做出輿情分析器 一、前言二、Web Scraper API 实战&#xff08;1&#xff09;选择对应的URL&#xff08;2&#xff09;点击进入对应url界面&#xff08;3&#xff09;API结果实例和爬取结果展示&#xff08;4&#xff09;用户直接使用post请…...

langchain4j中使用milvus向量数据库做RAG增加索引

安装milvus向量数据库 官方网址 https://milvus.io/zh 使用docker安装milvus mkdir -p /data/docker/milvus cd /data/docker/milvus wget https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh#在docker中启动milvus sh standalone_emb…...

【开源工具】深度解析:基于PyQt6的Windows时间校时同步工具开发全攻略

&#x1f552; 【开源工具】深度解析&#xff1a;基于PyQt6的Windows时间校时同步工具开发全攻略 &#x1f308; 个人主页&#xff1a;创客白泽 - CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f40d;《Python开源项目实战》 &#x1f4a1; 热爱不止于代码&#xff0c;热…...

开源 RPA 工具深度解析与官网指引

开源 RPA 工具深度解析与官网指引 摘要 &#xff1a;本文深入解析了多款开源 RPA 工具&#xff0c;涵盖 TagUI、Aibote、Taskt 等&#xff0c;分别介绍了它们的核心功能&#xff0c;并提供了各工具的官网链接&#xff0c;方便读者进一步了解与使用&#xff0c;同时给出了基于不…...

【免杀】C2免杀技术(一)VS设置

一、概述 编译器生成的二进制文件特征&#xff08;代码结构、元数据、指纹&#xff09;可能被杀软的静态或动态检测规则匹配。Visual Studio 的构建设置&#xff08;特别是运行库、编译器优化、链接方式等&#xff09;会直接影响最终生成的二进制文件的结构、行为特征和依赖关…...

OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库——nettle库

准备工作 请依照这篇文章搭建环境 OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库——环境配置_openharmony交叉编译-CSDN博客 编译依赖 相关依赖有 gmp-6.3.0 请依照这篇文章编译 OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库…...

Kotlin与Ktor构建Android后端API

以下是一个使用 Kotlin 和 Ktor 构建 Android 后端 API 的详细示例,包含常见功能实现: 1. 项目搭建 (build.gradle.kts) plugins {applicationkotlin("jvm") version "1.9.0"id("io.ktor.plugin") version "2.3.4"id("org.je…...