这是对之前写的一版更新了一下,修改了一些问题。因为之前写得代码逻辑稍微有些问题,但又不好在原基础上修改,就只好重新修改一下,写一篇文章了,原版:C++实现以太网帧的模拟封装与发送-优快云博客
(注:本文末尾留了完整代码文件链接,可直接免费获取)
一、实验目的与要求
计算机网络实验,模拟以太网帧的创建、读取和校验。
目的要求:
(1)设计以太网V2的MAC帧结构的数据结构。
(2)能够从文件中读取来自网络层的数据,并显示到屏幕上。
(3)用模2运算方法由CRC-32函数得到FCS。
(4)加上帧首部和尾部组成发送帧。
(5)将组成的发送帧显示到屏幕上并保存到一个输出文件中。
二、环境
代码采用C++语言编写,IDE是Visual Studio 2022。
三、实验原理
以太网(Ethernet)是目前广泛应用于局域网中的一种数据链路层协议。它定义了数据在网络中如何传输,并使用数据帧作为传输单元。以太网帧的结构由多个部分组成,包括帧头、数据负载和帧尾。理解以太网帧的封装结构,对于学习计算机网络协议和数据链路层的工作原理至关重要。
本实验的主要目标是通过C++实现以太网帧的模拟封装与发送。具体过程涉及将数据按照以太网协议要求进行封装,并模拟通过网络进行发送。以太网帧的封装过程大致分为以下几个步骤:
以太网帧结构:
以太网帧由以下几个主要部分组成:
前导码(Preamble):7字节,作用是同步接收端和发送端的时钟。
起始帧定界符(SFD,Start Frame Delimiter):1字节,标识帧的起始。
目标MAC地址(Destination MAC Address):6字节,表示数据帧的接收方。
源MAC地址(Source MAC Address):6字节,表示数据帧的发送方。
类型字段(Type):2字节,指示数据字段的协议类型,如IPv4、ARP等。
数据字段(Data):46-1500字节,包含实际传输的数据。
填充字段(Padding):如果数据字段少于46字节,会进行填充。
CRC校验和(CRC,Cyclic Redundancy Check):4字节,用于数据的完整性校验。
以太网帧封装:
在发送数据之前,发送端需要根据以上结构将数据封装成以太网帧。通过将目标MAC地址、源MAC地址、类型字段、数据部分和CRC校验和等内容组织起来,形成一个完整的以太网帧。
封装过程主要包括:
在数据前加上目标和源的MAC地址;
添加类型字段以标识数据内容的协议类型;
如果数据长度小于46字节,则进行填充;
计算并添加CRC校验和以确保数据的完整性。
模拟发送:
虽然在实验中我们不会直接通过物理网络进行数据传输,但可以模拟发送过程。在C++程序中,我们通过模拟以太网帧的发送,实际上是将以太网帧的数据打印输出,进行模拟。
C++实现:
使用C++可以模拟以太网帧的封装过程。为了简单起见,实验中可以使用结构体来表示以太网帧的各个部分,并使用数组或缓冲区来存储完整的帧数据。通过C++中的文件操作,来实现以太网帧的发送。
四、功能与代码
1.结构体定义帧
EthernetFrame结构体定义了一个以太网帧的各个组成部分:
destAddress 和 sourceAddress 是两个 vector<unsigned char> 类型的数组,分别存储目的地址和源地址。
etherType 用来表示以太网帧的类型(例如IPv4或ARP)。
payload 是实际的数据载荷。
FCS 是帧校验序列(Frame Check Sequence),用于在数据传输时验证数据的完整性。
struct EthernetFrame {
vector<unsigned char> destAddress; // 目的地址
vector<unsigned char> sourceAddress; // 源地址
unsigned short etherType; // 以太网帧类型
vector<unsigned char> payload; // 数据载荷
unsigned int FCS; // 帧校验序列
};
2.读取文件数据
该函数用于从指定文件中读取二进制数据,并将数据以十六进制格式输出到屏幕。
istreambuf_iterator<char> 用于从文件流中按字节读取数据。
void readDataFromFile(const string& filename) {
ifstream file(filename, ios::binary);
if (file) {
vector<unsigned char> data((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
for (unsigned char byte : data) {
cout << hex << setw(2) << setfill('0') << static_cast<int>(byte) << " ";
}
cout << endl;
} else {
cerr << "错误:未能打开文件" << endl;
}
}
3.CRC-32 校验表
该表是一个预先计算的CRC-32校验表,用于快速计算CRC-32值。CRC-32是常见的错误检测算法,广泛应用于网络协议中。(注意这里下面代码我省略了,因为内容太长,可以在本文末尾最后的链接中,看到完整代码)
const unsigned int crc32Table[256] = { ... };
4. CRC-32 计算
calculateCRC32 函数通过对输入数据执行CRC-32算法来计算校验和。这个过程是基于事先计算的CRC-32查找表进行的。
unsigned int calculateCRC32(const vector<unsigned char>& data) {
unsigned int crc = 0xFFFFFFFF;
for (unsigned char byte : data) {
crc = (crc >> 8) ^ crc32Table[(crc ^ byte) & 0xFF];
}
return crc ^ 0xFFFFFFFF;
}
5.组装以太网帧
assembleEthernetFrame 函数组装以太网帧:
先加入目的地址、源地址和以太网类型(etherType)。
然后加入数据载荷(payload)。
最后计算并添加帧校验序列(FCS)到帧中。FCS 是通过计算整个帧(包括目的地址、源地址、类型和数据载荷)后的CRC-32值得到的。
vector<unsigned char> assembleEthernetFrame(EthernetFrame& frame) {
vector<unsigned char> assembledFrame;
assembledFrame.insert(assembledFrame.end(), frame.destAddress.begin(), frame.destAddress.end());
assembledFrame.insert(assembledFrame.end(), frame.sourceAddress.begin(), frame.sourceAddress.end());
assembledFrame.push_back((frame.etherType >> 8) & 0xFF);
assembledFrame.push_back(frame.etherType & 0xFF);
assembledFrame.insert(assembledFrame.end(), frame.payload.begin(), frame.payload.end());
frame.FCS = calculateCRC32(assembledFrame);
assembledFrame.push_back((frame.FCS >> 24) & 0xFF);
assembledFrame.push_back((frame.FCS >> 16) & 0xFF);
assembledFrame.push_back((frame.FCS >> 8) & 0xFF);
assembledFrame.push_back(frame.FCS & 0xFF);
return assembledFrame;
}
6.main函数
在 main 函数中:
先从文件中读取数据,并输出该数据。
设置一个以太网帧的目的地址、源地址和类型(IPv4)。
将文件的内容作为载荷数据(payload)加入帧中。
使用 assembleEthernetFrame 函数组装整个以太网帧,并计算 CRC。
将组装好的以太网帧输出到屏幕,最后将帧保存到一个新的文件中。
(注意一定要修改文件路径,只需要修改需要发送内容的文件和保存之后的文件这两处路径,这个只是我电脑上的路径,需要修改成你电脑文件保存路径,这是全部代码唯一需要修改的两处地方)
int main() {
EthernetFrame frame;
// 从文件读取数据并显示
string filename = "D:\\计算机网络\\network_data.dat";
readDataFromFile(filename);
// 设置帧内容
frame.destAddress = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 };
frame.sourceAddress = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
frame.etherType = 0x0800; // IPv4 数据包类型
frame.payload.assign(filename.begin(), filename.end()); // 文件内容作为载荷
// 组装以太网帧
vector<unsigned char> assembledFrame = assembleEthernetFrame(frame);
// 显示组装后的帧内容
cout << "组装后的帧内容是:" << endl;
for (unsigned char byte : assembledFrame) {
cout << hex << setw(2) << setfill('0') << static_cast<int>(byte) << " ";
}
cout << endl;
// 将组装好的帧保存到文件
ofstream outputFile("D:\\计算机网络\\output_frame.dat", ios::binary);
if (outputFile) {
outputFile.write(reinterpret_cast<char*>(assembledFrame.data()), assembledFrame.size());
} else {
cerr << "错误:未能打开生成的文件" << endl;
}
return 0;
}
五、一些问题说明和解释
1.CRC-32表一定要这样写吗?
答:不一定需要。在C++中,确实没有内置的 CRC-32 计算库,但是有很多第三方库和工具可以用于计算 CRC-32。
一些流行的 C++ CRC-32 计算库包括 Boost.CRC、zlib、CRC++ 等。这些库提供了方便的接口来计算各种 CRC 校验值。
2.原本文件和生成得文件都需要提前创建吗?为什么后缀是.dat?txt的文本文件不行吗?
答:我们要模仿读取帧和封装帧,所有要读取的文件得提前在确定得路径下创建好,而生成的文件不需要,程序会自动写在确定得路径下。
文本文件和二进制文件在存储和读取数据的方式上有一些不同。如果你将数据保存为文本文件(如.txt),编辑器可能会以文本格式保存数据,这会导致额外的字符(如换行符、回车符)被添加到文件中,这些字符在读取时可能会导致意外的结果。为了确保程序能够正确读取文件中的二进制数据,我们通常建议直接使用二进制文件格式。在这种格式下,数据将按字节存储,不会有任何多余的字符或格式信息。
六、完整代码文件
夸克网盘链接:https://pan.quark.cn/s/15c9566fab3c