这段时间开始复习计算机网络,看到帧封装这一节,结合以前的课程设计,就用C写了个帧封装的程序,说实话C学的确实不怎么样,实现的时候对于文件操作那部分查了好多资料,下面说说帧封装是啥情况。
学过计算机网络的都知道,数据的传输都是以固定的格式进行传输,在计算机当中是以二进制的数据进行传输,在网络通信中, “帧” 指通信中的一个数据块。但是帧在数据链路层传输的时候是有讲究的,不是随便的封装和打包就可以传输,大小有限制,最小46字节,最大1500字节所以我们必须按照这个规则来封装,具体的原因有兴趣的可以参考谢希仁的计算机网络帧碰撞检测那部分,说的很详细,这里不再多说,如果帧长度下于46字节就用数据填充,直到46B为止,并且填充的字段不加入长度字段中,如果大于1500字节那必须分为两个帧进行传输。
要进行帧封装还必须知道帧的结构,不知道结构无从谈起。结合书上说的802.3标准的帧结构给大家画出来了:(需要说明这是802.3标准的帧结构,还有其他版本的,也还有的吧前导码和定界符放在一起8B,既是64位,都一样)
前导码 | 前定界符 | 目的地址 | 源目的地址 | 长度字段 | 数据字段 | 校验字段 |
7B | 1B | 6B | 6B | 2B | 46-1500 | 4B |
CRC校验:
在校验字段中,使用的是CRC校验。校验的范围包括目的地址字段、源地址字段、长度字段、数据字段。CRC是一种重要的线性分组码、编码和解码方法,有检错和纠错能力强等特点,不仅能检查出离散错误,还能检查出突发错误。
按照书上的描述:利用CRC进行检错的过程可简单描述如下:在发送端根据要传送的m位二进制码序列,以一定的规则产生一个校验用的r位监督码(CRC码),附在原始信息的后边,构成一个新的二进制码序列(共m+r位),然后发送出去。在接收端,根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。这个规则在差错控制理论中称为“生成多项式”具体代码:
int statusNum = dataTotalNum;
while(statusNum--) {
char temp;
//读1B的数据
temp = fgetc(fileOut);
//模拟数据除的二进制除法过程
for(unsigned char i = (unsigned char)0x80; i > ;i >>= ) {
if(crc & 0x80) { //当前余数最高位为1,需要进行除法运算。
crc <<= ;
//将输入数据相应位的值递补到余数末位
if(temp & i){
crc ^= 0x01;
}
//进行除法运算,即与除数的低位相异或。
crc ^= 0x07;
}else{
crc <<= ; //crc左移位,最低位补。
//输入数据相应位的值递补到余数末位
if(temp & i) crc ^= 0x01;//将输入数据相应位的值递补到余数末位。
}
}
}
运算符:
这里还要说一下运算符,待会CRC校验还要用到:& >>=
1:按位与运算 按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结
果位才为1 ,否则为0。参与运算的数以补码方式出现。
例如:9&5可写算式如下: 00001001 (9的二进制补码)&00000101 (5的二进制补码) 00000001 (1的二进制补码)可见9&5=1。
按位与运算通常用来对某些位清0或保留某些位。例如把a 的高八位清 0 , 保留低八位, 可作 a&255 运算 ( 255 的二进制数为0000000011111111)。
2:>>是右移运算符,就是将n的二进制表示向右移位。你这里的n>>=1表示将n的二进制表示向右移动一位再赋值给n.这里的
>>=与+=,-=,*=的用法是一样的,先做运算再赋值
3:^ 二进制位异或,双目操作符如果a与b中有且仅有一个为1时,a^b的值为1,其它情况下值为0
设计方法:
首先向输出文件写入前导码、帧前定界符、目的地址、源地址和长度字段。然后读取到封装的数据文件(以2进制方式读取),填充到数据位,然后用crc计算校验字段就可以了。
问题:
这里需要有两个要解决的问题一个是文件大小的读取(不是直接得到文件的大小而是得到读取量的大小),一个是循环读取,当帧长度大于1500字节后要循环读取在封装。
第一个问题解决方法:记录读取数据块的位置,然后利用读取文件内部指针的偏移计算读取的大小。
//得到最后一个数据块的位置
lastDataPacket = ftell(fileIn);
第二个问题解决办法:先计算总文件的大小,利用fou循环以此讲数据块封装,具体的实现如下:
/**
* 先把fpIn指针退回到文件结尾处。再得到文件位置指针
* 当前位置相对于文件首的偏移字节数,即可得到内容的长度
*/
fseek(fileIn,,SEEK_END);
offsetNum = ftell(fileIn);
//计算整1500数据包个数
int dataPacketNum = offsetNum/;
//把文件指针重新回到文件开头。
rewind(fileIn);
/**
* for循环处理大于1500B的情况,当大于1500时
* 自动转换到下一个数据帧中
*/
for(int j = ;j <= dataPacketNum; j++){
char data[MAXSIZE]; //数据临时存储数组
. . .for(i = ; i < ; i++){
fputc(sourceMac[i],fileOut); //写入源MAC
printf("%X ",sourceMac[i]);
}
//不是最后一个数据,则前面的数据都应该是1500,所以按最大数据数算
if(j != dataPacketNum){
//添加长度字段
. . .逻辑处理
}else{
. . . 逻辑处理
if(surplusLongs > ){
. . .逻辑处理
}else{
//多于或者等于46B,则正常读取
. . .逻辑处理
}
}
fseek(fileOut,(short)startCalibration,SEEK_SET); //将读指针指向开始校验的位置
}
再补充一下C的文件操作部分,都是常用到的函数:
fopen(打开文件) * fopen(const char * path,const char * mode);参数path字符串包含欲打开的文件路径及文件名,参数mode字符串则代表着流形态。
r 打开只读文件。r+ 打开可读写的文件,该文件必须存在。w 打开只写文件,若文件不存在则建立该文件。w+ 打开可读写文件,若文件不存在则建立文件。
a 以附加的方式打开只写文件。若文件不存在,则建立,如果文件存在,数据加到文件尾。a+ 以附加方式打开可读写的文件。若文件不存在,则建立,如果文件存在,数据加到文件尾后。
fputs(将一指定的字符串写入文件内) int fputs(const char * s,FILE * stream); 说明 fputs()用来将参数s所指的字符串写入到参数stream所指的文件内。
返回值 若成功则返回写出的字符个数,返回EOF则表示有错误发生。
fseek(移动文件流的读写位置)
int fseek(FILE * stream,long offset,int whence); 说明
fseek()用来移动文件流的读写位置。参数stream为已打开的文件指针,参数offset为根据参数whence来移动读写位置的位移数。
SEEK_SET从距文件开头offset位移量为新的读写位置。SEEK_CUR
以目前的读写位置往后增加offset个位移量。SEEK_END将读写位置指向文件尾后再增加offset个位移量。
putc(将一指定字符写入文件中)
int putc(int c,FILE * stream); putc()会将参数c转为unsigned
char后写入参数stream指定的文件中。虽然putc()与fputc()作用相同,但putc()为宏定义,非真正的函数调用。返回值
putc()会返回写入成功的字符,即参数c。若返回EOF则代表写入失败。
ftell(取得文件流的读取位置) long ftell(FILE * stream); 函数说明 ftell()用来取得文件流目前的读写位置。参数stream为已打开的文件指针。
返回值 当调用成功时则返回目前的读写位置,若有错误则返回-1,errno会存放错误代码。
总体来说就是这些,代码测试的时候需要注意,输出的是二进制文件,,查看的时候需要用编辑器打开比如UE编辑器,可以看到封装后的帧的结构。下面给出完整的代码,代码可运行,输出位置可以自己定:
/**
* 将fileinput文件中的数据封装成帧,封装好的帧写入到文件中.
* 如果数据长度小于46字节,则补全到46字节,如果数据长度大于1500,则封装成多个帧
* @Author: zhaoyafei
* @Time : 2015年7月7日
*/
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 1500
/*
* 帧封装函数,完成把给
* 定的文件封装成帧的功能
*/
int frameEncapsulation(char *fileinput){
int i,lastDataPacket,offsetNum; //记录最后一个帧,记录偏移量
FILE *fileIn,*fileOut;
long startCalibration,endCalibration; //开始检验位置,结束检验位置
short dataTotalNum; //记录校验字节个数
if((fileIn = fopen(fileinput,"r+")) == NULL){
printf("%s","打开文件失败");
return ;
} if((fileOut = fopen("E:\\out.txt","wb+")) == NULL){
printf("%s","写入文件失败");
return ;
}
printf("\n封装后的文件保存地址:E:\\out.txt");
/**
* 先把fpIn指针退回到文件结尾处。再得到文件位置指针
* 当前位置相对于文件首的偏移字节数,即可得到内容的长度
*/
fseek(fileIn,,SEEK_END);
offsetNum = ftell(fileIn);
//计算整1500数据包个数
int dataPacketNum = offsetNum/;
//把文件指针重新回到文件开头。
rewind(fileIn); /**
* for循环处理大于1500B的情况,当大于1500时
* 自动转换到下一个数据帧中
*/
for(int j = ;j <= dataPacketNum; j++){
char data[MAXSIZE]; //数据临时存储数组
//写入帧前导码,十六进制0xaa
printf("\n帧的前导码:");
for(i = ;i < ; i++){
fputc(0xaa,fileOut);
printf("%X ",0xaa);
}
//写入帧定界符
fputc(0xab,fileOut);
printf("%X ",0xab);
char objectiveMac[] = {0xff,0xff,0xff,0xff,0xff,0xff};//模拟目的MAC地址
char sourceMac[] = {0x10,0x16,0x76,0xb4,0xe4,0x77};//模拟源MAC地址 //记录开始进行校验的位置,因为校验是从前导码以后开始的
startCalibration = ftell(fileOut);
printf("\n帧的目的MAC地址:");
for(i = ; i < ; i++){
fputc(objectiveMac[i],fileOut);//写入目的MAC
printf("%X ",objectiveMac[i]);
}
printf("\n帧的源MAC地址:");
for(i = ; i < ; i++){
fputc(sourceMac[i],fileOut); //写入源MAC
printf("%X ",sourceMac[i]);
}
//不是最后一个数据,则前面的数据都应该是1500,所以按最大数据数算
if(j != dataPacketNum){
//添加长度字段
fputc(char(/),fileOut);
fputc(char(%),fileOut); fread(data,sizeof(char),,fileIn);
fwrite(data,sizeof(char),,fileOut);
//记录开插入校验码的位置
endCalibration = ftell(fileOut);
fputc(0x00,fileOut); //添加辅助检验字段
dataTotalNum = short(ftell(fileOut)) - (short)startCalibration; //计算检验的字段长度
}else{
//得到最后一个数据块的位置
lastDataPacket = ftell(fileIn); //剩下有多少字节
int hasLongs = offsetNum - dataPacketNum * ;
//还有多少不够46B
int surplusLongs = - hasLongs; //记录长度字段
fputc(char(hasLongs/),fileOut);
fputc(char(hasLongs%),fileOut); //先读取剩余的所有数据
fread(data,sizeof(char),offsetNum - lastDataPacket,fileIn);
//如果不足,则填充
if(surplusLongs > ){
for(i = ;i < surplusLongs; i++){
//把不够的部分模拟补上
data[hasLongs++] = 0x00;
}
fwrite(data,sizeof(char),,fileOut); //写入数据
endCalibration = ftell(fileOut); //记录开插入校验码的位置
//添加辅助检验字段
fputc(0x00,fileOut);
dataTotalNum = short(ftell(fileOut)) - (short)startCalibration; //计算检验的字段长度
}else{
//多于或者等于46B,则正常读取
fwrite(data,sizeof(char),offsetNum - lastDataPacket,fileOut);
//记录开插入校验码的位置
endCalibration = ftell(fileOut);
fputc(0x00,fileOut);
dataTotalNum = short(ftell(fileOut)) - (short)startCalibration;
}
}
fseek(fileOut,(short)startCalibration,SEEK_SET); //将读指针指向开始校验的位置
unsigned char crc = ;
int statusNum = dataTotalNum;
while(statusNum--) {
char temp;
//读1B的数据
temp = fgetc(fileOut);
//模拟数据除的二进制除法过程
for(unsigned char i = (unsigned char)0x80; i > ;i >>= ) {
if(crc & 0x80) { //当前余数最高位为1,需要进行除法运算。
crc <<= ;
//将输入数据相应位的值递补到余数末位
if(temp & i){
crc ^= 0x01;
}
//进行除法运算,即与除数的低位相异或。
crc ^= 0x07;
}else{
crc <<= ; //crc左移位,最低位补。
//输入数据相应位的值递补到余数末位
if(temp & i) crc ^= 0x01;//将输入数据相应位的值递补到余数末位。
}
}
}
//将读指针指尾部,开始插入检验字段
fseek(fileOut,(short)endCalibration,SEEK_SET);
//若不足4B,补位至4B
printf("\n帧的校验和:");
if(sizeof(crc) == ){
fputc(0x00,fileOut);
fputc(0x00,fileOut);
fputc(0x00,fileOut);
printf("%X %X %X ",0x00,0x00,0x00);
printf("%X ",crc);
fputc(crc,fileOut);
}else if(sizeof(crc) == ){
fputc(0x00,fileOut);
fputc(0x00,fileOut);
printf("%X %X ",0x00,0x00);
printf("%X ",crc);
fputc(crc,fileOut);
}else {
fputc(0x00,fileOut);
printf("%X ",0x00);
printf("%X ",crc);
fputc(crc,fileOut);
}
printf("\n帧的数据部分:");
for(int m = ; m < dataTotalNum - ; m++){
printf("%X ",data[m]);
}
printf("\n");
}
//关闭文件资源
fclose(fileIn);
fclose(fileOut);
return ;
} //程序主函数
int main(){
int status;
char fileinput[]; //记录地址
while(){
printf("*******************************************************************************\n");
printf("%s","* 1.帧封装,2.退出 *\n");
printf("*******************************************************************************\n");
printf("%s","Please input selection number:");
scanf("%d",&status);
switch(status){
case :
//帧封装调用
printf("Please enter path to the file:");
scanf("%s",fileinput);
frameEncapsulation(fileinput);
break;
case :
exit();
default:
putchar('\a');
printf("%s","You can choose 1 or 2 !\n");
}
printf("%s","\n");
}
return ;
}
运行界面,封装的文件内容是 hello world:
封装后的帧结构(我用UE打开的):