一、从样例程序上手
在实验文件夹的Example文件夹下有三个样例程序:gobackn.exe,selective.exe,stopwait.exe;
可通过命令行窗口打开:在文件夹上方的地址栏里直接输入cmd即可打开命令行窗口(如图);
打开两个命令行窗口;
打开后任选一个程序名,如stopwait,在两个窗口内分别输入 stopwait A 和 stopwait B,连接建立,程序开始执行;
命令行中输入的指令可带选项,具体可参考实验文档,常用的有如图:
注:datalink -d3 A 可打印出协议运行信息,便于debug;
程序运行正常时会打印出这样的报告:480.484 .... 1784 packets received, 7611 bps, 95.14%, Err 38 (9.9e-006)
其表示含义为:时间坐标为 480.484 秒,收到了 1784 个分组,网络层有效数据传输率 7611bps,实际线路利用率
95.14%,接收方向共检出 38 个帧校验和错误,统计计算出实际误码率 9.9x10 ^(-6) 。实验要求修改文件夹中的datalink.c文件,完成自己的协议设计。
二、各模块函数的定义
1.日志函数
extern void lprintf(char *fmt,...); //在输出信息之前首先输出当前的时间坐标;
2.初始化函数
void protocol_init(int argc, char **argv); //对运行环境初始化;
3.与网络层模块的接口函数
void enable_network_layer(void); //允许网络层发送数据分组;
void disable_network_layer(void); //不允许网络层发送数据分组;
int get_packet(unsigned char *packet); //将分组拷贝到指针 packet 指定的缓冲区中,函数返回值为分组长度;
void put_packet(unsigned char *packet, int len); //两个参数:存放收到分组的缓冲区首地址和分组长度;
4.事件驱动函数
int wait_for_event(int *arg); //参数 arg 用于获得已发生事件的相关信息;
#define NETWORK_LAYER_READY 0 //网络层有待发送的分组;
#define PHYSICAL_LAYER_READY 1 //物理层发送队列的长度低于 50 字节;
#define FRAME_RECEIVED 2 //物理层收到了一整帧;
#define DATA_TIMEOUT 3 //定时器超时,参数 arg 中返回发生超时的定时器的编号;
#define ACK_TIMEOUT 4 //所设置的搭载 ACK 定时器超时;
5.与物理层模块的接口函数
void send_frame(unsigned char *frame, int len); //将内存 frame 处长度为 len 的缓冲区块向物理层发送为一帧;
int recv_frame(unsigned char *buf, int size); //从物理层接收一帧,size 为用于存放接收帧的缓冲区 buf 的空间大小,返 回值为收到帧的实际长度;
int phl_sq_len(void); //返回当前物理层队列的长度;
注:物理层发送队列最多可以保留 64K 字节。
6.CRC 校验和的产生与验证
unsigned int crc32(unsigned char *buf, int len); //校验和正确为 0,否则不为 0;
例:*(unsigned int *)(p + 243) = crc32(p, 243);
功能:指针 p 的定义为 char *p 并且 p 指向一个缓冲区,缓冲区内有 243 字节数据,为这 243 字节数据生成 CRC-32 校验和,并且把这 32 比特校验和附在 243 字节之后;
7.定时器管理
unsigned int get_ms(void); //获取当前的时间坐标,单位为毫秒;
void start_timer(unsigned int nr, unsigned int ms); //用于启动一个定时器;两个参数分别为计时器的编号和超时时间值;
void stop_timer(unsigned int nr); //中止一个定时器。在定时器未超时之前直接对同一个编号的定时器执行
start_timer()调用,将按照新的时间设置产生超时事件。
void start_ack_timer(unsigned int ms);
void stop_ack_timer(void);
start_ack_timer()与 start_timer()有两点不同:首先,定时器启动时刻为当前时刻;其次,在先前启动的定时器未超时之前重新执行 start_ack_timer()调用,定时器将依然按照先前的时间设置产生超时事件 ACK_TIMEOUT。
8.协议工作过程的跟踪和调试
extern void dbg_event(char *fmt, ...); //只有在发生某些不正常事件时才产生输出;
extern void dbg_frame(char *fmt, ...); //每发送和接收一帧,都打印出相关调试信息便于协议分析;
extern void dbg_warning(char *fmt, ...);
char *station_name(void); //获取当前进程所对应的站点名,为字符串”A”或者”B”;
dbg_frame,dbg_event,dbg_warning 三个函数最终调用 lprintf,可以通过命令行参数的--debug 选项或者-d 选项调整输出。
三、程序主体流程
enable_network_layer();
for (;;) {
event = wait_for_event(&arg);
switch (event) {
case EVENT_NETWORK_LAYER_READY:
len = get_packet(my_buf);
… …
break;
case EVENT_PHYSICAL_LAYER_READY:
… …
break;
case EVENT_FRAME_RECEIVED:
rbuf_len = recv_frame(rbuf, sizeof rbuf);
... ...
break;
case EVENT_ACK_TIMEOUT:
... ...
break;
case EVENT_DATA_TIMEOUT:
... ...
break;
}
if (...)
enable_network_layer();
else
disable_network_layer();
}
四、一种实验方案(选择重传协议)
1.物理层
为数据链路层提供的服务为8000bps,270ms传播延时,10^(-5)误码率的字节流传输通道。为了仿真实现上述服务质量的信道,利用在同一台计算机上TCP Socket完成两个站点之间的通信。由于同一台计算机上TCP通信传播时延短、传播速度快、没有误码,物理层仿真程序在发送端利用“令牌桶”算法限制发送速率以仿真8000bps线路;在接收端误码插入模块利用一个伪随机数“随机地”篡改从TCP收到的数据,使得所接收到的每个比特出现差错的概率为10^(-5);接收到的数据缓冲后延时270ms才提交给数据链路层程序,以仿真信道的传播时延特性。为了简化程序,省略了成帧功能,数据链路层利用接口函数send_frame()和recv_frame()发送和接收一帧。
2.数据链路层
发送方和接收方都维持一个窗口,窗口内部分别包含可发送或已发送但未被确认的和可接受的序号。接收到的数据包被缓存起来,当按正确的顺序接收完毕后再提交给网络层。ACK信息通过数据帧捎带确认的方式传递,若遇到长时间无数据帧发送,则产生ACK超时事件(ACK_TIMEOUT),主动发送空的ACK帧。若长时间未收到ACK信息,则产生数据帧超时事件(DATA_TIMEOUT),发送方自动重传未确认帧;当出现帧丢失或校验错误时,接收方会主动发送NAK帧提示发送方立即重传。
数据链路层通过物理层提供的函数来利用物理层提供的服务。通过get_packet()函数从网络层得到一个分组;当数据链路层成功接收到一个分组后,通过put_packet()函数提交给网络层。
3.网络层
利用数据链路层提供的“可靠的分组传输”服务,在站点A与站点B之间交换长度固定为256字节的数据分组。网络层把产生的分组交付数据链路层,并接受数据链路层提交来的数据分组。
“分组序列发生器”生成的每个分组的前两个字节放置了一个两字节整数作为“分组 ID”,例如:在调试数据帧重传功能时,可以打印出这个分组 ID 以分辨是哪个网络层分组数据在重传。设指针 p 的定义为 unsigned char *p,并且 p 已指向分组的首字节,那么,打印整数*(short *)p 就可以得到分组 ID。站点 A 产生的分组的分组 ID 取值为 10000~19999,站点 B 产生的分组的分组 ID 取值为 20000~29999,分组 ID 值递增,递增到最大值后回卷到最小值。