【2024寒假实习面经】——CPP方向{篇Ⅲ}
文章目录
- TCP建立连接的时候能传数据吗?【能但不推荐】
- 客户端在发送恶意的syn报文和重复的报文时怎么处理?
- 1. **恶意SYN报文**【SYN Cookies➕半连接队列➕超时丢弃】
- 处理方式:
- 2. **重复的SYN报文**【seq相同➕重复syn限制】
- 处理方式:
- 3. **TCP三次握手中的保护机制**
- 4. **SYN Flood攻击的额外防护**
- TCP丢包和粘包问题
- 半连接队列和全连接队列的具体实现。
- listen()函数第二个参数的作用?
- 迭代器类型
- 1. **`iterator`**
- 2. **`const_iterator`**
- 3. **`reverse_iterator`**
- 4. **`const_reverse_iterator`**
- 5. **`input_iterator`**
- 6. **`output_iterator`**
- 7. **`bidirectional_iterator`**
- 8. **`random_access_iterator`**
- sort函数只支持随机访问迭代器
- 总结:
- 分辨类模板和函数模板
- 1. **类模板 (Class Template)**
- 2. **函数模板 (Function Template)**
- 3. **主要区别**
- 4. **模板实例化的方式**
- 5. **常见的语法区别**
- 6. **类模板与函数模板的混合使用**
- 总结
- 为什么需要三次握手?三次握手怎么保证可靠的?
- TCP cookie
- **TCP Cookie(SYN Cookie)**
- **SYN洪泛攻击简述**
- **SYN Cookie的工作原理**
- **SYN Cookie的优势和局限性**
- **SYN Cookie实现示例**
- 总结:
- TCP具体怎么处理丢包问题?【确认应答/超市重传/快速重传/选择重传】
- 1. **重传机制**
- 2. **滑动窗口与流量控制**
- 3. **快速重传(Fast Retransmit)**
- 4. **选择性重传(Selective Acknowledgment,SACK)**
- 5. **TCP拥塞控制**
- 6. **计算超时重传的时间(RTO)**
- 总结
- TCP怎么确认收到的报文是完整的?
- 1. **序列号(Sequence Number)**
- 2. **确认号(Acknowledgment Number)**
- 3. **校验和(Checksum)**
- 4. **滑动窗口与流量控制**
- 5. **重传机制**
- 6. **选择性确认(SACK)**
- 7. **数据边界与分段重组**
- 8. **连接关闭时的完整性检查**
- 综上所述,TCP通过以下几个关键机制来确保数据的完整性:
- 拥塞机制
- 接收缓冲区满了,怎么通知可以收包了。【发送方的探测报文+接收方的窗口通告】
- 迭代器失效【扩容/删除/中间插入/范围更改】
- 完美转发。
- **目的**:
- **实现方式**:
- **核心概念**:
- **基本语法**:
- **示例**:
- **总结**:
- 模板,特化。实际应用。
- 函数模板特化
- 类模板特化
- 分离编译
- 优缺点
- STL容器实现。
- 智能指针。
- 多态底层
- 虚继承,菱形继承,地址空间分布
TCP建立连接的时候能传数据吗?【能但不推荐】
在TCP建立连接时,不能立即传输应用层数据。TCP连接的建立过程通过三次握手完成,在这三次握手完成之前,双方并没有一个稳定的连接,无法可靠地进行数据传输。因此,数据传输是要等到三次握手完成之后才能开始的。
虽然TCP连接的建立过程中严格来说不能传输应用层数据,但从技术上讲,你可以在TCP三次握手的过程中“携带”一些数据。这种做法通常不推荐,因为它违反了TCP协议的设计规范,可能会导致连接不稳定或不兼容,甚至可能影响连接的建立过程。
具体来说,TCP三次握手中的每个包都允许携带TCP选项,这些选项可以用于一些控制信息。理论上,你可以在SYN包或者SYN-ACK包中通过TCP选项携带一些数据。但是,有几个关键点需要注意:
- 数据量受限:TCP选项的空间非常有限,通常是0~40字节。无法传输大量数据。
- 不是应用层数据:即使在SYN包中携带一些数据,也通常只能作为连接建立的一部分控制信息来使用,而不是传统的应用层数据。
- 不符合TCP协议的标准:TCP三次握手的目的仅仅是建立可靠连接,应用数据的传输应该等到连接完全建立后。随意携带数据可能会导致连接失败或数据丢失。
- 中间设备的处理:一些中间设备(如防火墙、路由器)可能会拦截包含非标准TCP选项的数据包,导致连接失败。
综上所述,尽管可以在SYN包中携带少量数据,但它并不是传输实际应用数据的合适方式。实际应用中,建议在三次握手完成后再开始传输数据,这样能确保连接的稳定性和可靠性。
客户端在发送恶意的syn报文和重复的报文时怎么处理?
1. 恶意SYN报文【SYN Cookies➕半连接队列➕超时丢弃】
恶意SYN报文通常是指伪造的、具有欺骗性质的SYN包。最常见的攻击形式是SYN洪水攻击(SYN flood),攻击者向目标服务器发送大量的SYN包,但不发送后续的ACK包,从而消耗服务器资源并导致拒绝服务。
处理方式:
- SYN Cookies:一种防止SYN洪水攻击的技术。在服务器收到SYN包时,不立即分配资源,而是生成一个基于请求数据(如IP、端口等)和加密哈希的“cookie”值,并将其发送回客户端。当客户端发送正确的ACK包时,服务器验证该cookie,并继续建立连接。这可以防止攻击者消耗服务器资源。
- 连接队列限制:服务器通常会设置一个SYN队列,用于存储正在等待完成三次握手的连接。如果队列满了,服务器会丢弃新的SYN包,从而限制恶意连接的创建。
- 超时机制:如果客户端在收到SYN-ACK后没有及时发送ACK,连接会在一段时间后超时。这样可以防止伪造的SYN包占用过多的服务器资源。
2. 重复的SYN报文【seq相同➕重复syn限制】
重复的SYN报文可能会由于网络问题或攻击者的行为而发送。这种情况一般不会影响连接建立,因为TCP协议对这种重复请求做了一些处理。
处理方式:
- TCP协议重传机制:TCP协议对重复的SYN报文有很好的容错能力。当服务器收到重复的SYN包时,会认为这是一个网络重传,且通常不会重复分配资源。服务器会根据相同的序列号和连接标识符来处理连接请求。
- SYN重试计数:对于合法的连接请求,TCP实现通常会限制同一个客户端的SYN重试次数。如果客户端发送多个重复的SYN包,且没有有效的回应,服务器会逐渐增加超时时间或者直接拒绝连接请求。
3. TCP三次握手中的保护机制
- 防止伪造源IP的SYN包:为了防止伪造源IP的攻击(如IP欺骗),许多服务器和防火墙会检查SYN包的源地址是否合法,并采用一些防火墙策略来过滤非法来源的连接。
- 防火墙和入侵检测系统(IDS):很多网络部署了防火墙和IDS,能够检测和过滤异常的SYN流量。它们能够基于流量的异常模式或已知攻击的特征来识别并阻止恶意请求。
4. SYN Flood攻击的额外防护
为了进一步保护服务器免受SYN Flood攻击的影响,以下是一些常见的防护措施:
- 黑名单和速率限制:如果检测到某个IP地址频繁发起SYN请求,可以将该IP加入黑名单,或者通过速率限制策略,限制每秒内从同一IP接收的SYN包数量。
- 防火墙策略:设置防火墙规则来限制来自特定源IP的连接请求频率。
- TCP/IP栈优化:在服务器端,调整内核的TCP/IP栈配置(例如增加SYN队列的大小、调整SYN超时时间)可以帮助提升服务器对SYN Flood攻击的防御能力。
TCP丢包和粘包问题
- 丢包问题【确认应答/重传/滑动窗口/拥塞控制/流量控制】
- 发生场景:在网络传输中,由于网络拥堵、设备故障或丢包,数据包可能丢失,导致接收端无法收到完整的消息。
- 解决方案:
- TCP协议提供了可靠传输机制,通过重传机制来解决丢包问题。当发送端没有收到确认响应时,会自动重传丢失的数据包。
- 使用滑动窗口和确认机制来控制数据的流量和保证数据传输的可靠性。
- 粘包问题【应用层协议格式➕TCP层的流量控制】
- 发生场景:发送端发送多个小数据包时,接收端可能会将这些数据包合并成一个包一起接收,导致数据边界混乱。
- 解决方案:
- 应用层协议:采用固定长度的报文或在每个消息前添加一个固定的消息头(如消息长度字段)来标识每个数据包的边界。
- TCP流量控制:通过合适的缓冲区大小和分包控制来避免数据粘连。
- 定长包或分隔符:应用层可以设计定长的消息,或者在消息之间添加特殊的分隔符。
半连接队列和全连接队列的具体实现。
- 半连接队列(SYN Queue)
- 定义:半连接队列存储的是已经收到SYN请求,但尚未完成三次握手的连接请求。客户端发送SYN请求后,服务器通过SYN+ACK响应客户端,并等待客户端的ACK确认。此时,连接还没有完全建立。
- 实现:
- 当服务器调用
listen()
时,操作系统会为该监听套接字分配一块内存,作为半连接队列**【早期】**。 - 客户端发送SYN请求后,服务器的内核将该连接请求加入半连接队列,等待客户端的ACK确认。
- 如果客户端确认连接,半连接队列中的该项将移到全连接队列。
- 半连接队列的长度(一般由操作系统参数配置)限制了同时等待握手确认的连接数。
- 当服务器调用
- 全连接队列(Accept Queue)
- 定义:全连接队列存储的是已经完成三次握手、连接已建立的客户端连接。该队列中的连接已经准备好进行数据传输,可以由
accept()
调用取出。 - 实现:
- 当客户端完成三次握手并确认连接时,服务器通过
accept()
调用从全连接队列中取出连接。 - 一旦连接被取出,全连接队列中的该项会被移除。
accept()
返回一个新的套接字用于与客户端进行数据通信。
- 当客户端完成三次握手并确认连接时,服务器通过
- 定义:全连接队列存储的是已经完成三次握手、连接已建立的客户端连接。该队列中的连接已经准备好进行数据传输,可以由
- 流程与关系:
- 服务器调用
listen()
设置监听,并指定半【早期】 / 全【现在】连接队列的长度。 - 客户端向服务器发送SYN包,服务器收到后将其加入半连接队列,并发送SYN+ACK响应。
- 客户端收到SYN+ACK包后,发送ACK包确认,服务器将该连接从半连接队列移到全连接队列。
- 服务器使用
accept()
从全连接队列中取出已完成握手的连接。
- 服务器调用
- 半连接队列:保存着尚未完全握手的连接请求,最多可存储5个。
- 全连接队列:存储已经建立的连接,可以通过
accept()
取出进行通信。
注意:
- 半连接队列的长度过小可能导致客户端的连接请求被拒绝或延迟。
- 全连接队列的长度过小会导致服务端无法快速接受已经完成握手的连接。
listen()函数第二个参数的作用?
https://developer.aliyun.com/article/1541655
迭代器类型
1. iterator
- 定义:用于遍历容器中的元素,允许读取和修改元素。
- 适用容器:几乎所有标准容器,如
std::vector
,std::map
,std::list
等。 - 用法:可用于修改容器中的元素。
std::vector<int> vec = {1, 2, 3, 4};
std::vector<int>::iterator it = vec.begin();
*it = 10; // 修改元素
2. const_iterator
- 定义:用于遍历容器中的元素,但不能修改元素,只能读取元素的值。
- 适用容器:所有标准容器。
- 用法:用于只读访问容器中的元素。
std::vector<int> vec = {1, 2, 3, 4};
std::vector<int>::const_iterator it = vec.begin();
std::cout << *it; // 只能读取,不能修改
3. reverse_iterator
- 定义:用于反向遍历容器,从容器的末尾到开头。
- 适用容器:所有标准容器。
- 用法:反向遍历容器的元素。
std::vector<int> vec = {1, 2, 3, 4};
std::vector<int>::reverse_iterator rit = vec.rbegin();
std::cout << *rit; // 输出 4
4. const_reverse_iterator
- 定义:与
reverse_iterator
类似,但不能修改元素,只能读取元素的值。 - 适用容器:所有标准容器。
- 用法:反向只读遍历容器的元素。
std::vector<int> vec = {1, 2, 3, 4};
std::vector<int>::const_reverse_iterator rit = vec.rbegin();
std::cout << *rit; // 输出 4
5. input_iterator
- 定义:只支持一次性读操作的迭代器,只能读取容器元素,不能修改,且每次访问一次之后会自动前进。
- 适用容器:通常用于流输入和一些非序列化的容器。
- 特点:可以向前移动,但不能向后移动。
- 用法:适用于读取流数据,如文件输入。
std::istream_iterator<int> it(std::cin);
6. output_iterator
- 定义:只支持一次性写操作的迭代器,用于将值写入容器。
- 适用容器:通常用于输出流或容器。
- 用法:用于向容器中写入数据。
std::ostream_iterator<int> out_it(std::cout, " ");
*out_it = 10; // 输出 10
7. bidirectional_iterator
- 定义:支持双向移动的迭代器,可以向前也可以向后移动。
- 适用容器:如
std::list
、std::map
、std::set
等。 - 用法:可以向前或向后遍历容器元素。
std::list<int> lst = {1, 2, 3};
std::list<int>::iterator it = lst.begin();
++it; // 向前移动
--it; // 向后移动
8. random_access_iterator
- 定义:支持任意位置的随机访问迭代器,可以向前、向后移动,支持通过偏移量进行访问。
- 适用容器:如
std::vector
,std::deque
等。 - 用法:可以使用下标和加法运算来访问元素。
std::vector<int> vec = {10, 20, 30};
std::vector<int>::iterator it = vec.begin();
it += 2; // 向前移动两个位置
std::cout << *it; // 输出 30
sort函数只支持随机访问迭代器
template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);
// 函数模板
template <class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);
sort对map排序【自定义比较函数】
#include <iostream>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
// 自定义比较函数,按 `second` 升序排序
bool cmp(const pair<const string, int>& a, const pair<const string, int>& b) {
return a.second < b.second;
}
int main() {
map<string, int> ans = {{"apple", 3}, {"banana", 1}, {"orange", 2}};
// 将 map 的元素转换为 vector 以便排序
vector<pair<const string, int>> vec(ans.begin(), ans.end());
// 使用自定义比较函数进行排序
sort(vec.begin(), vec.end(), cmp);
// 输出排序后的结果
for (const auto& p : vec) {
cout << p.first << ": " << p.second << endl;
}
return 0;
}
lamnda表达式
#include <iostream>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
map<string, int> ans = {{"apple", 3}, {"banana", 1}, {"orange", 2}};
// 将 map 的元素转换为 vector 以便排序
vector<pair<const string, int>> vec(ans.begin(), ans.end());
// 使用 lambda 表达式进行排序,按 `second` 升序排序
sort(vec.begin(), vec.end(), [](const pair<const string, int>& a, const pair<const string, int>& b) {
return a.second < b.second; // 按 `second` 升序排序
});
// 输出排序后的结果
for (const auto& p : vec) {
cout << p.first << ": " << p.second << endl;
}
return 0;
}
总结:
iterator
和const_iterator
:用于普通遍历和只读遍历。reverse_iterator
和const_reverse_iterator
:用于反向遍历和只读反向遍历。input_iterator
和output_iterator
:主要用于流操作。bidirectional_iterator
:支持双向遍历。random_access_iterator
:支持随机访问,可以进行指针偏移操作。
普通/只读;正向/反向;单向/双向/随机;
分辨类模板和函数模板
在 C++ 中,类模板和函数模板是两种非常常见的模板类型。它们的主要区别在于它们是应用于类的定义还是函数的定义。以下是分辨 类模板 和 函数模板 的方法及其示例。
1. 类模板 (Class Template)
类模板是用来定义一个类的模板,可以根据不同的类型(或者其他模板参数)来实例化一个类。
-
语法:
template <typename T> // 或者 template <class T> class ClassName { public: T value; ClassName(T val) : value(val) {} T getValue() { return value; } };
-
特点:
- 模板定义的是一个类,它包含数据成员和成员函数。
- 类模板在实例化时,类型(或其他参数)会被替换成特定的类型。
-
示例:
template <typename T> // 定义类模板 class Box { private: T value; public: Box(T val) : value(val) {} T getValue() const { return value; } }; int main() { Box<int> intBox(10); // 实例化 Box 为 int 类型 Box<double> doubleBox(3.14); // 实例化 Box 为 double 类型 std::cout << intBox.getValue() << std::endl; std::cout << doubleBox.getValue() << std::endl; return 0; }
输出:
10 3.14
2. 函数模板 (Function Template)
函数模板是用来定义一个可以接受任意类型参数的函数。根据传递给函数的不同类型,编译器会生成相应类型的函数。
-
语法:
template <typename T> // 或者 template <class T> T functionName(T arg) { return arg; }
-
特点:
- 模板定义的是一个函数,它接受类型参数。
- 函数模板可以根据参数类型不同生成不同的函数实例。
-
示例:
template <typename T> // 定义函数模板 T add(T a, T b) { return a + b; } int main() { std::cout << add(10, 20) << std::endl; // 使用 int 类型 std::cout << add(3.5, 2.5) << std::endl; // 使用 double 类型 return 0; }
输出:
30 6
3. 主要区别
- 类模板 定义的是一个类,通常包含数据成员和成员函数,实例化时替换类型或其他参数。
- 函数模板 定义的是一个函数,可以根据传递的类型参数生成不同类型的函数。
4. 模板实例化的方式
- 对于 类模板,你需要显式地指定类型进行实例化(如
Box<int>
或Box<double>
)。 - 对于 函数模板,编译器会根据传递的参数类型自动推导出类型,或者你也可以显式指定类型(如
add<int>(10, 20)
)。
5. 常见的语法区别
特性 | 类模板 | 函数模板 |
---|---|---|
定义 | template <typename T> class ClassName { ... }; | template <typename T> T functionName(T arg); |
实例化 | ClassName<int> obj; | functionName<int>(10); |
用途 | 定义泛型类,成员变量和成员函数。 | 定义泛型函数,执行操作(如加法、比较等)。 |
6. 类模板与函数模板的混合使用
你还可以将类模板和函数模板结合使用,例如一个类模板中有一个函数模板的成员函数。
示例:
template <typename T>
class Calculator {
public:
T add(T a, T b) { return a + b; }
T multiply(T a, T b) { return a * b; }
};
int main() {
Calculator<int> calcInt;
std::cout << calcInt.add(10, 20) << std::endl; // 使用 add 函数模板
std::cout << calcInt.multiply(10, 20) << std::endl; // 使用 multiply 函数模板
Calculator<double> calcDouble;
std::cout << calcDouble.add(3.14, 2.71) << std::endl; // 使用 add 函数模板
std::cout << calcDouble.multiply(3.14, 2.71) << std::endl; // 使用 multiply 函数模板
return 0;
}
输出:
30
200
5.85
8.5094
总结
- 类模板 用于定义 类,其中可以包含数据成员和成员函数,可以通过指定类型来实例化。
- 函数模板 用于定义 函数,可以接受任意类型的参数,通过参数类型来生成不同类型的函数实例。
为什么需要三次握手?三次握手怎么保证可靠的?
- 确认双方可通信:确保客户端和服务器双方都能够相互通信,并且双方都已经准备好进行数据传输。
- 同步序列号:在TCP中,每个数据包都有一个序列号,用于确保数据的顺序传输。三次握手过程中,双方会交换并确认初始序列号,以确保可靠的数据传输。
- 防止重复连接请求:三次握手可以防止旧的连接请求影响新的连接请求,确保连接的唯一性。
过程
- 第一次握手 (SYN):
- 客户端向服务器发送一个SYN包,表示客户端请求与服务器建立连接,并附带一个初始序列号(Seq=X)。
- 这一包的作用:客户端告诉服务器自己准备好并想建立连接,并向服务器传递了自己可以使用的序列号。
- 第二次握手 (SYN-ACK):
- 服务器收到SYN包后,确认客户端的请求,并向客户端发送一个SYN-ACK包。这个包包含服务器的初始序列号(Seq=Y)以及对客户端序列号的确认(Ack=X+1)。
- 这一包的作用:服务器确认自己也准备好了,并且它回复了客户端的序列号,表示自己已收到客户端的连接请求。
- 第三次握手 (ACK):
- 客户端收到服务器的SYN-ACK包后,向服务器发送一个ACK包,确认已收到服务器的SYN请求,并将确认号设置为
Ack=Y+1
,表示确认收到了服务器的序列号。 - 这一包的作用:客户端向服务器发送确认信息,确认与服务器的通信链路已经建立。
- 客户端收到服务器的SYN-ACK包后,向服务器发送一个ACK包,确认已收到服务器的SYN请求,并将确认号设置为
如何保证可靠性?
- 确认机制:每次交换的包都包含确认号(Ack),表示对方的包已经被接收。这就避免了数据丢失或者顺序错误的情况。
- 同步序列号:每次连接的建立都涉及到序列号的交换。通过序列号,双方可以确保发送的数据顺序和完整性。客户端和服务器都能追踪自己发送的和接收的字节数据,保证没有丢失。
- 防止重复连接:通过三次握手,可以确保请求的唯一性。比如,如果客户端的请求包在网络中延迟了很长时间并被重复发送,服务器会根据序列号的不同识别出旧的连接请求,不会错误地建立新的连接。
- 确认连接双方的能力:三次握手不仅确认双方可以通信,还通过序列号的交换确认双方的接收能力。例如,如果客户端没有收到服务器的响应(ACK),它会重新发送SYN请求,直到建立连接或者超时。
TCP cookie
在TCP协议中,“cookie” 这个术语通常并非指传统意义上的 web 端的会话标识符,而是指一种 TCP连接的验证机制,用于防止某些类型的攻击,特别是在 SYN洪泛攻击 中。它通过使用一个加密的、唯一的验证信息来确保客户端是合法的。
TCP Cookie(SYN Cookie)
TCP Cookie,也称为 SYN Cookie,是一个用于抵御 SYN洪泛攻击(SYN Flood Attack)的技术,它可以在不占用大量内存的情况下,处理大量的SYN请求。
SYN洪泛攻击简述
SYN洪泛攻击是一种拒绝服务(DoS)攻击,攻击者发送大量的伪造源地址的SYN请求来耗尽服务器的资源。由于每个连接请求都需要占用服务器内存,如果恶意请求非常多,服务器的资源会被消耗殆尽,导致无法正常服务合法用户。
SYN Cookie的工作原理
SYN Cookie的核心思想是在服务器还没有分配内存给一个新连接之前,通过 计算一个"cookie"(通常是一个加密的值)来标识这个连接,并将这个"cookie"发送给客户端。当客户端确认连接时,服务器根据该"cookie"来验证客户端请求的有效性。
过程:
-
客户端发送SYN请求:客户端向服务器发起连接请求(SYN包),并等待响应。
-
服务器响应SYN+ACK
:
- 如果服务器没有收到完整的连接信息(即没有足够的内存空间处理该连接请求),它不直接分配资源。相反,服务器通过 计算一个SYN Cookie 并将其作为确认号发送回客户端。
- SYN Cookie 是通过对客户端的 IP 地址、端口、服务器的当前时间戳以及一个秘钥等进行加密计算得出的。这个值没有任何实际的连接状态信息,因此即使攻击者伪造了大量的连接请求,服务器也不会为这些伪造的请求分配资源。
-
客户端回复ACK:客户端收到SYN+ACK包后,正常发送ACK包以确认连接建立。
-
服务器验证ACK:当服务器收到ACK包时,服务器根据客户端的ACK值计算SYN Cookie,验证客户端是否是合法的。只有合法的客户端(即响应中包含正确的SYN Cookie)才会继续进行后续的数据传输,而不合法的请求会被丢弃。
通过这种方式,服务器在没有分配内存的情况下就可以确认连接是否有效,从而避免了因SYN洪泛攻击而导致的资源耗尽问题。
SYN Cookie的优势和局限性
优势:
- 减少资源消耗:服务器无需为每个SYN请求分配资源(如缓冲区、内存等),因此可以防止由于恶意攻击导致的资源耗尽。
- 有效抵抗SYN洪泛攻击:攻击者即使发送大量伪造的SYN请求,服务器也不会受到影响。
局限性:
- 不存储连接状态:在SYN Cookie的机制下,服务器不会存储传统的连接状态信息(如接收的序列号等)。因此,只有在完成握手后才能开始正常的连接数据传输。
- 限制了某些功能:由于不保存连接状态,SYN Cookie可能不适用于某些需要更多状态信息的场景(例如TCP选项的处理)。
SYN Cookie实现示例
// 示例:在Linux内核中实现SYN Cookie
// 通过合适的参数和加密方法来生成和验证SYN Cookie
// 创建SYN Cookie的计算方法
uint32_t generate_syn_cookie(uint32_t ip, uint16_t port, uint32_t timestamp, uint32_t secret_key) {
uint32_t cookie = hash_function(ip, port, timestamp, secret_key);
return cookie;
}
// 验证SYN Cookie的有效性
int validate_syn_cookie(uint32_t cookie, uint32_t ip, uint16_t port, uint32_t timestamp, uint32_t secret_key) {
uint32_t expected_cookie = generate_syn_cookie(ip, port, timestamp, secret_key);
if (cookie == expected_cookie) {
return 1; // 合法连接
} else {
return 0; // 非法连接
}
}
总结:
TCP Cookie(或SYN Cookie)是一种有效的技术,用于防止SYN洪泛攻击,尤其适用于资源有限的环境。它通过在三次握手的过程中不立即分配资源,而是通过计算加密的“cookie”来验证连接的合法性,从而保证服务器不受大量恶意请求的影响。这种方式有效减少了资源消耗,并且可以抵御拒绝服务攻击。
TCP具体怎么处理丢包问题?【确认应答/超市重传/快速重传/选择重传】
TCP 协议通过其内建的 可靠性机制 来有效地处理丢包问题。由于 TCP 是一种面向连接、可靠的协议,它设计了多种机制确保数据的完整性和顺序性,即使在丢包、网络延迟或错误的情况下,也能保证数据能够正确地传输。
1. 重传机制
TCP 通过 确认机制 和 超时重传 来处理丢包问题。
- 确认机制(Acknowledgment):接收方通过发送一个带有确认号的 ACK 包来确认收到的数据。如果发送方没有收到对某个数据包的确认,它会假设该数据包丢失并重新发送。
- 超时重传(Timeout Retransmission):如果发送方在设定的超时时间内没有收到对应的 ACK 包,它会触发重传机制,重新发送丢失的数据包。超时时间通常是动态调整的,以适应网络的延迟和波动。
- 发送方发送数据并开始计时。
- 如果接收方收到数据,它会发送 ACK 确认包。
- 如果发送方在超时时间内没有收到 ACK,便认为数据包丢失并进行重传。
2. 滑动窗口与流量控制
TCP 使用 滑动窗口机制 来管理数据的流动,防止数据包丢失。
- 滑动窗口:TCP 通过滑动窗口控制发送方的发送速率,避免接收方缓冲区溢出。当接收方的缓冲区满时,它会通知发送方停止发送数据或发送少量数据。滑动窗口机制通过调整窗口大小来实现流量控制。
- 流量控制:接收方会告诉发送方自己可用的缓冲区大小(即窗口大小),发送方根据接收方的缓冲区状态来调整发送速率。如果缓冲区满,接收方会停止发送 ACK,促使发送方减慢发送速度或停止发送数据。
3. 快速重传(Fast Retransmit)
TCP 还使用 快速重传 技术来更迅速地恢复丢失的数据包。
- 当发送方连续收到多个相同的 ACK(即 重复 ACK),这表示有某些数据包没有被接收方成功接收,通常是因为某个数据包丢失。
- 一旦发送方收到三个重复的 ACK,它会立即进行重传,跳过超时等待的过程。这样可以在数据包丢失后,快速进行重传,减少延迟。
4. 选择性重传(Selective Acknowledgment,SACK)
- 选择性重传 是 TCP 的一个扩展,它允许接收方通知发送方哪些数据包已经成功接收,哪些没有成功接收。这样,发送方只需要重传丢失的数据包,而不是重传整个窗口的数据。
- 这避免了重传已经成功接收的数据,减少了带宽的浪费和延迟,提高了传输效率。
5. TCP拥塞控制
TCP 的 拥塞控制机制 也有助于减少丢包的发生,尽量避免因网络拥塞导致的数据包丢失。
TCP 使用了 慢启动、拥塞避免、快重传 和 快恢复 等算法来调整发送窗口的大小,从而减少网络中的拥塞,并避免过多数据包的丢失。
- 慢启动:连接开始时,TCP 会逐渐增加发送窗口的大小,避免一下子发送过多数据。
- 拥塞避免:当网络拥塞时,TCP 会减小发送窗口的大小,减少发送速率,避免丢包。
- 快恢复:当发生丢包时,TCP 会快速减少窗口大小,进入恢复模式,确保传输稳定。
6. 计算超时重传的时间(RTO)
TCP 在超时重传机制中会动态计算一个 重传超时(RTO) 时间,这个时间基于往返时间(RTT)和其变化量。RTO 计算公式通常如下:
RTO = SRTT + max(G, 4 * RTTVAR)
- SRTT:平滑的往返时间,表示网络的平均延迟。
- RTTVAR:往返时间的变化量,表示网络延迟的波动。
- G:用于防止过短的超时设定的最小值。
通过动态调整超时时间,TCP 可以更灵活地应对不同网络状况,从而减少不必要的重传。
总结
TCP 通过一系列可靠性机制来处理丢包问题,确保数据的完整传输:
- 重传机制:使用确认包和超时重传来应对丢包。
- 滑动窗口:通过流量控制避免接收方缓冲区溢出,从而减少丢包。
- 快速重传:通过重复 ACK 来快速识别丢包并进行重传。
- 选择性重传(SACK):只重传丢失的数据包,提高传输效率。
- 拥塞控制:通过调整发送速率减少网络拥塞,减少丢包。
- 动态调整 RTO:根据网络延迟动态计算重传超时,避免过早或过晚的重传。
这些机制结合在一起,使得 TCP 可以可靠地处理丢包问题,即使在不稳定的网络环境中也能够保证数据的正确传输。
TCP怎么确认收到的报文是完整的?
TCP 确认收到的报文是完整的主要通过以下几种机制来保证数据的可靠性、完整性以及顺序性:
1. 序列号(Sequence Number)
每个TCP数据包都携带一个 序列号,用于标识数据流中的每个字节。序列号的作用是确保数据包按正确的顺序到达并且没有丢失。
- 发送方根据数据流中的字节顺序为每个字节分配一个序列号。
- 接收方根据收到的包的序列号进行排序,确保数据流是按顺序接收的。
2. 确认号(Acknowledgment Number)
接收方通过发送 确认号(ACK) 来确认接收到的字节。每个确认包中都包含一个确认号,表示接收方期望收到的下一个字节的序列号。
- 例如,如果接收方收到序列号为
1
到100
的数据,那么它会发送一个确认号101
,表示它已经收到并确认了序列号为1
到100
的数据,接下来期望序列号为101
的数据。 - 如果发送方没有收到确认号,它会认为数据丢失,并在超时后重传该数据包。
3. 校验和(Checksum)
TCP 通过 校验和 来验证数据是否在传输过程中发生错误。每个TCP数据包都包含一个校验和字段,发送方在发送数据时计算该数据的校验和,并将其放入TCP头部。
- 接收方在接收到数据包后,会重新计算数据的校验和。如果校验和匹配,说明数据在传输过程中没有发生错误。如果校验和不匹配,说明数据发生了错误,接收方会丢弃该数据包并请求重传。
4. 滑动窗口与流量控制
TCP使用 滑动窗口 机制来管理数据流动,从而确保接收方有足够的缓冲区来接收数据,防止丢包。
- 通过滑动窗口,接收方能够告知发送方它的接收窗口大小,也就是接收方可以缓存的数据量。这样可以避免发送方发送过多数据,导致接收方的缓冲区溢出,从而避免丢包。
5. 重传机制
TCP通过 重传机制 来确保丢失的数据包会被重新发送。
- 如果发送方发送的数据包没有收到接收方的确认(ACK),则会在超时时间到达之前重新发送该数据包。
- 快速重传:当接收方收到三个重复的ACK时,表示某个数据包可能丢失。发送方会立即重传丢失的数据包,而不需要等到超时。
6. 选择性确认(SACK)
TCP的 选择性确认(SACK) 扩展允许接收方告知发送方哪些数据已经成功接收,哪些数据丢失。
- 传统的确认机制只告诉发送方“到哪一个字节已经收到”,但如果接收方没有按顺序接收到所有字节,就不能告知具体哪些字节丢失。
- 使用SACK时,接收方可以在ACK包中添加选项,告诉发送方哪些数据段已成功接收,哪些数据段丢失,这样发送方就可以只重传丢失的部分,而不是重传整个窗口的数据。
7. 数据边界与分段重组
TCP是一个 面向字节流的协议,它并不关心数据的边界。因此,接收方需要根据 序列号 来正确地重组数据。
- 发送方会把大块数据分段传输,每个段都有一个序列号。接收方根据收到的序列号将这些段重新拼接成完整的字节流。
- 如果某个数据段丢失,接收方会等待该数据段的重传,并根据ACK来确保顺序正确。
8. 连接关闭时的完整性检查
在TCP连接关闭时,双方会通过四次挥手(Four-Way Handshake)来确保所有数据都被成功传输和接收。在最后一个FIN包确认之前,双方会确认所有的数据都已经被处理完毕。
综上所述,TCP通过以下几个关键机制来确保数据的完整性:
- 序列号:确保数据包按正确顺序传输。
- 确认号(ACK):接收方通过ACK确认数据包,发送方知道数据是否成功到达。
- 校验和:确保数据在传输过程中没有被损坏。
- 滑动窗口:防止接收方缓冲区溢出,减少丢包的风险。
- 重传机制:丢失的数据包会被重新发送。
- 选择性确认(SACK):帮助发送方只重传丢失的数据块,而不是整个数据流。
- 分段重组:确保传输的每个数据段正确拼接成完整的字节流。
这些机制共同保证了TCP协议能够在不可靠的网络上实现可靠的数据传输,确保接收方能完整地接收到数据。
拥塞机制
接收缓冲区满了,怎么通知可以收包了。【发送方的探测报文+接收方的窗口通告】
当接收缓冲区满时,接收方通过在 ACK 包中设置 窗口大小为0 通告发送方停止发送数据。发送方接收到 窗口大小为0 后,会暂停发送数据,并定期发送 探测报文(一个空数据包)来查询接收方的窗口是否已经更新。一旦接收方处理了数据并腾出空间,它会更新 窗口大小 并通知发送方,允许其继续发送数据。
迭代器失效【扩容/删除/中间插入/范围更改】
中间插入/扩容:vector deque
删除:vector deque map set
范围更改:map set 插入新元素可能导致元素顺序改变
完美转发。
完美转发(Perfect Forwarding) 是 C++ 中一种技术,用于将函数参数原封不动地传递给另一个函数,保留参数的值类别(如左值或右值)和类型。
目的:
确保将函数的参数转发给另一个函数时,原始参数的 值类别(左值或右值)和 类型信息 不发生变化。
实现方式:
使用 std::forward
配合 右值引用(T&&
) 和 模板,实现完美转发。
核心概念:
- 左值引用(Lvalue Reference):指向一个可以被修改的对象。
- 右值引用(Rvalue Reference):允许接受临时对象,可以避免不必要的拷贝。
基本语法:
template <typename T>
void wrapper(T&& arg) {
// 完美转发:根据arg的类型转发给另一个函数
some_function(std::forward<T>(arg));
}
-
T&&
是一个 完美转发 参数类型,它可以接受左值或右值。 -
std::forward<T>(arg)
将根据
arg
的类型(左值或右值)转发参数:
- 如果
arg
是左值,std::forward<T>(arg)
会返回一个左值引用。 - 如果
arg
是右值,std::forward<T>(arg)
会返回一个右值引用。
- 如果
示例:
#include <iostream>
#include <utility>
void print(int& x) {
std::cout << "Lvalue: " << x << std::endl;
}
void print(int&& x) {
std::cout << "Rvalue: " << x << std::endl;
}
template <typename T>
void wrapper(T&& arg) {
print(std::forward<T>(arg)); // 完美转发
}
int main() {
int x = 10;
wrapper(x); // Lvalue
wrapper(20); // Rvalue
}
总结:
- 完美转发 保证参数的类型和左值/右值特性不丢失。
- 通过
std::forward<T>(arg)
和 右值引用 实现。
模板,特化。实际应用。
函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字 template 后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
// 函数模板特化
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 调用特化之后的版本,不走模板生成
return 0;
}
类模板特化
// 类模板
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Date<int, char>
{
public:
Date() { cout << "Date<int, char>" << endl; }
};
Date<int, int> d1;
Date<int, char> d2;
// 偏特化1.部分特化
template <class T1>
class Date<T1, int>
{
public:
Date() { cout << "Date<T1, int>" << endl; }
};
// 偏特化2.两个参数偏特化为指针类型
template <class T1, class T2>
class Date<T1*, T2*>
{
public:
Date() { cout << "Date<T1*, T2*>" << endl; }
};
Date<char, int> d4;
Date<int*, int*> d3;// 原函数模板就可以 只不过实现了偏特化版本 且这个版本更符合d3 故d3使用偏特化
分离编译
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
模板的实例化由编译器在编译时完成。
编译器在编译 main.cpp
时无法看到模板函数的实现,它只能生成一个对模板函数实现的引用(或者说是一个占位符),而不知道具体的实现代码在哪里。
在链接时,链接器会尝试找到这些引用的具体实现,但在 a.cpp
编译生成的目标文件中,并没有为 main.cpp
中使用的特定模板类型(如 int
和 double
)生成实例化代码,因为 a.cpp
本身并没有使用这些类型的 Add
函数。==》链接器找不到这些实例化的实现,从而引发链接错误
解决办法
// a.h
#ifndef A_H
#define A_H
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
#endif
优缺点
【优点】
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺陷】
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误