本教程可以带你深入了解ARQ协议的原理以及实现,并且掌握ARQ协议栈的编写原理。掌握图形化界面的展示方式,能够将信息以图形界面展示。
目录
编写接收端(服务器端)代码:
ARQ协议接收端主要包含三个功能,一是根据用户所提供的端口号将本程序绑定到本主机;二是接受来自发送端(客户端)的请求,根据ARQ协议进行一些事先的协商,并最终建立连接,接收来自客户机发送的数据;三是将接受到的数据经过一定的处理然后显示出来。图形界面使用MFC工具,选择了基于对话框的界面。
1、建立一个对话框,生成一个cpp源代码文件与rc图形预览界面,命名为CARQServerDlg。在初始化函数CARQServerDlg::OnInitDialog()中加入初始设定:
BOOL CARQServerDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != nullptr)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
WSADATA wsd;//定义WSADATA对象
WSAStartup(MAKEWORD(2, 2), &wsd);
this->SetWindowText(_T("服务器端ARQ协议模拟"));
connectinfo.SetWindowTextW(_T("未绑定\r\n请先绑定本机"));
CFont c,p;
c.CreatePointFont(360, _T("Arial"));
p.CreatePointFont(150, _T("宋体"));
GetDlgItem(IDC_STATIC)->SetFont(&c);
portinfo.SetWindowText(_T("请输入端口号"));
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
添加一个静态文本显示框,显示程序标题,将此编辑框的ID设置为IDC_STATIC,使用该ID设置其初始格式,文本与字体大小。同时设置对话框窗口标题栏。
添加一个编辑控件,调成只读模式,防止显示的信息被修改。同时给这个编辑控件赋变量,变量名为connectinfo,变量类型为CEdit。使用这个变量显示初始提示信息。
2、添加输入框与按钮,能够让用户输入所选择的端口号。输入编辑控件添加后,如上给其添加变量portinfo,读入类型为CString的端口号。按下按钮产生动作,执行一段代码,将数据检查正确性后赋值给全局变量ports,留待日后使用。并弹出对话框报错。
void CARQServerDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
portinfo.GetWindowText(str);
int h = _tstoi(str);
if (h >= 0 && h <= 65535)
{
ports = h;
MessageBox(_T("目的端口号:") + str, NULL, MB_ICONINFORMATION);
}
else
MessageBox(_T("请输入正确的端口号!") + str, NULL, MB_ICONEXCLAMATION);
}
3、添加按钮,按钮名称为“绑定本机”。此按钮按下后,执行一段代码,代码功能为获取本机IP,获取用户输入的端口号,初始化socket接口,尝试进行绑定。同时在绑定过程中显示提示信息。绑定结果显示在connectinfo变量中,告知用户。
void CARQServerDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
if (soc != INVALID_SOCKET)
closesocket(soc);
if (ports == -1)
{
MessageBox(_T("请输入端口号!"), NULL, MB_ICONERROR);
return;
}
connectinfo.SetWindowText(_T("Binding host..."));
//SOCKET soc;
sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;//设置服务器地址
serveraddr.sin_port = htons(ports);//设置端口号
serveraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
soc = socket(AF_INET, SOCK_STREAM, 0);
int i = bind(soc, (sockaddr*)&serveraddr, sizeof(serveraddr));
if (i == -1)
{
connectinfo.SetWindowText(_T("Binding host...\r\nBinding failed!"));
MessageBox(_T("绑定失败,请检查!"), NULL, MB_ICONEXCLAMATION);
return;
}
//成功
//char hostname[256]
CString p; p.Format(L"%d", ports);
gethostname(hostname, sizeof(hostname));
hostent *host = gethostbyname(hostname);
strcpy_s(hostname,sizeof(hostname),inet_ntoa(*(in_addr*)host->h_addr_list[2]));//*(struct in_addr*)*host->h_addr_list[0])
connectinfo.SetWindowTextW(_T("Binding successfully.\r\nhostIP:") + CString(hostname) + _T("\r\nhostPort:") + p+_T("\r\nListening..."));
HANDLE myh;
DWORD nthred = 0;
myh = (HANDLE)::CreateThread(NULL, 0, threadpro, this, 0, &nthred);
//SetTimer(1, 1000, NULL);
}
此时soc端口已经初始化完毕。使用bind函数进行主机绑定。
绑定失败和成功都在对话窗口(connectinfo)显示提示信息,但是绑定失败不会继续运行下面的代码。
4、编写主线程。主线程用于处理发送端的请求连接,判断连接数量是否合法。若合法,接收连接请求,并产生一个从属线程来具体处理每一个连接,负责具体发送事宜。主线程使用listen函数监听步骤3中已经绑定的端口。
while (1)
{
listen(soc, 0);
m_Server[presentCon] = accept(soc, (sockaddr*)&serveraddrfrom, &len);
if (m_Server[presentCon] != INVALID_SOCKET)
{
char s[256];
strcpy(s, "This is ARQServer:");
strcat(s, hostname);
send(m_Server[presentCon], s, sizeof(s), 0);
p->connectinfo.SetWindowText(_T("连接到ARQ客户端:") + CString(inet_ntoa(serveraddrfrom.sin_addr)));
if (presentCon == MAX_CONNECTIONS)
{
send(m_Server[presentCon], "服务器忙,暂时拒绝连接。", sizeof("服务器忙,暂时拒绝连接。"), 0);
continue;
}
presentCon++;
HANDLE myh;
DWORD nthred = 0;
myh = (HANDLE)::CreateThread(NULL, 0, threadpro2, p, 0, &nthred);
}
}
5、编写从属线程。每一个从属线程负责一个ARQ连接的处理,使用主线程分配的新的socket端口与发送端进行交互通信。从属线程主体为while循环结构,使用recv()函数接收来自发送端发送的信息,设定为阻塞接收。收到一段信息后,首先判断是否是退出报文,之后根据ARQ选择重传协议的特点模拟出4种状态:正确接收(回送ACK,ack=下一个报文的seq)、帧丢失(会送ACK,ack=丢失报文的seq)、帧错误(会送NAK)、应答帧丢失(不回送任何信息)。
if (!cs)
{
if (situation >= 0 && situation <= 15)//正常
{
strcat(str, "\r\n--->0(ok) send:ACK sseq:");
char s[8]; itoa(seq, s, 10);
strcat(str, s); strcat(str, "\r\n");
p->connectinfo.SetWindowText(CString(str));
for (int i = 1;; i++)
{
if (buff[i] == '\0')
break;
rf.put(buff[i]);
}
buff[1] = seq; seq++;
buff[2] = '\0';//clear
strcat(buff, "ACK\r\n");
send(soc, buff, sizeof(buff), 0); prevsit = 0;
}
else if (situation > 15 && situation <= 17)//1出错
{
strcat(str, "\r\n--->1(error) send:NAK sseq:");
char s[8]; itoa(seq, s, 10);
strcat(str, s); strcat(str, "\r\n");
p->connectinfo.SetWindowText(CString(str));
buff[1] = seq; seq++;
buff[2] = '\0';//clear
strcat(buff, "NAK\r\n");
send(soc, buff, sizeof(buff), 0); prevsit = 1;
}
else if (situation == 18)//2丢失
{
strcat(str, "\r\n--->2(loss) send:NAK sseq:");
char s[8]; itoa(seq, s, 10);
strcat(str, s); strcat(str, "\r\n");
p->connectinfo.SetWindowText(CString(str));
buff[1] = seq; seq++;
buff[2] = '\0';//clear
strcat(buff, "NAK\r\n");
send(soc, buff, sizeof(buff), 0); prevsit = 2;
}
else//3回复出错
{
strcat(str, "\r\n--->3(ACKloss) send:-- keep_sseq:");
char s[8]; itoa(seq, s, 10);
strcat(str, s); strcat(str, "\r\n");
p->connectinfo.SetWindowText(CString(str));
seq++; prevsit = 3;
//continue;
}
p->connectinfo.LineScroll(p->connectinfo.GetLineCount());
prevCSeq = cseq;
}
其中回送使用send()函数。应答帧除了帧丢失状态之外其余都序号都正常消耗。这里situation表示随机状态。每次接收到一个消息,不论上一次状态如何,此次都重新模拟,保证接收端状态形式多样性。
6、实验中所模拟的协议格式较为简单:
数据报格式:
序号seq(1B) | 保留,校验和(1B) | 数据部分 |
应答报文格式:
ack序号seq(1B) | 本报文序号(1B) | 保留,校验和(1B) | 数据部分 |
接收端从属线程在接收到信息进入某一个模拟状态的时候同时解析出报文内容,放入connectinfo同时告知用户。返回状态也会显示告知。
7、发送端可以选择是否使用校验和。这将会在建立链接后的第一次协商种确定。如果使用,需要编写判定、显示校验和的代码。
//增加的校验和部分
if (buff[1] == 'U'&&buff[2] == 'S'&&buff[3] == 'E'&&buff[4] == 'A')
{
strcat(str, "\r\n客户机要求使用校验和");
p->connectinfo.SetWindowText(CString(str));
p->connectinfo.LineScroll(p->connectinfo.GetLineCount());
cs = true;
buff[1] = seq; seq++;
buff[3] = '\0';//clear
strcat(buff, "ACK\r\n");
int rrsum = 0;
for (int i = 3;; i++)
{
if (buff[i] == '\0')
break;
rrsum += buff[i];
}
rrsum += buff[0]; rrsum += buff[1];
buff[2] = rrsum;
send(soc, buff, sizeof(buff), 0); prevsit = 0;
continue;
}
//
使用校验和,按照校验和计算方式 s=(b0+b1+…+bn) mod 256 ,对收到的数据进行重新计算对比收到报文中校验和部分,判断是否正确,并且显示出来。发送时计算应答帧报文本身的校验和填入报文相应部分。
接收到的数据部分会按照原来的格式写进文件当中,并且只有正确收到才会写入。
编写发送端(客户端)代码:
ARQ发送端主要有三个功能,一是根据用户所提供的接收端的IP地址与目的端口号,发送连接请求;二是在连接建立成功以后按照ARQ协议发送数据;三是将发送与接收到的应答解析并且显示出来。
1、添加一个对话框窗口,对话框命名为ARQprojectDlg,对话框源代码放在ARQprojectDlg.cpp文件中,对话框图形界面的预览放在.rc文件当中。
在函数CARQprojectDlg::OnInitDialog()中添加初始化代码。为对话框添加一个静态控件,添加变量命名为title,初始化title为对话框标题,并设置字体与大小。
CFont t,e;
t.CreatePointFont(300, _T("宋体"));
title.SetWindowText(_T("请填入主机信息"));
title.SetFont(&t);
e.CreatePointFont(150,_T("宋体"));
ipinfo.SetWindowText(_T("请输入ipv4地址"));
ipinfo.SetFont(&e);
CEdit *port = (CEdit*)GetDlgItem(IDC_portedit);
port->SetWindowText(_T("请输入端口号"));
port->SetFont(&e);
this->SetWindowText(_T("发送端ARQ协议模拟"));
connectinfo.SetWindowTextW(_T("未建立连接"));
CRect rect;
GetDlgItem(IDC_STATIC_PICTURE)->GetWindowRect(&rect); //IDC_WAVE_DRAW为Picture Control的ID
ScreenToClient(&rect);
GetDlgItem(IDC_STATIC_PICTURE)->MoveWindow(rect.left, rect.top, 60, 220, true);
设置对话框窗口标题为“发送端ARQ协议模拟”。
2、分别添加两个编辑控件以及两个按钮,一组对应用户输入目的主机的ip,一组对应目的端口号,构成socket接口的参数。输入ip信息的编辑框控件设置初始提示信息为“请输入ipv4地址”,目的端口号编辑框内设置初始提示信息为“请输入目的端口号”。当用户将焦点放置在此控件上面的时候,提示信息消失。
按钮按下对应一段代码。对于用于输入会使用正则表达式判断是否正确。不正确给出提示。只有正确才会放入全局变量留待日后使用。
3、添加编辑控件,为只读状态。添加变量为connectinfo,初始设置为“未建立连接”。建立连接后给出提示。
4、添加“建立连接”按钮,按钮对应使用socket向目标及请求连接的代码。只有用户输入信息全部正确才会执行下面的代码。
//建立连接
connectinfo.SetWindowText(_T("Connecting to server start..."));
sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(ports);
serveraddr.sin_addr.S_un.S_addr = inet_addr(add);
//inet_pton(AF_INET,add,(void*)&serveraddr.sin_addr.S_un.S_addr) ;
client_soc = socket(AF_INET, SOCK_STREAM, 0);
connectinfo.SetWindowText(_T("Connecting to server start...\r\nConstly trying now..."));
int i = connect(client_soc,(sockaddr*)&serveraddr,sizeof(serveraddr));
if (i == -1)
{
connectinfo.SetWindowText(_T("Connecting to server start...\r\nConstly trying now...\r\nFail to connect."));
MessageBox(_T("连接失败,请检查"), NULL, MB_ICONEXCLAMATION);
return;
}
connectinfo.SetWindowText(_T("Connect successfully!"));
char buff[256];
recv(client_soc, buff, 256, 0);
connectinfo.SetWindowTextW(_T("Connect successfully!\r\n")+CString(buff));
jumpEN = true;
使用ip地址与目的端口号初始化serveraddr变量,然后初始化socket:client_soc。使用
connect()函数向服务器(ARQ接收端)请求建立连接。连接失败进行提示,只有连接成功的时候才允许下一步跳转。
5、建立一个新的对话框窗口,命名为CMyARQ。在其源程序CMyARQ.cpp中添加代码。在原来的对话框(ARQprojectDlg)中加添按钮“确认”,当连接建立后允许用户点击跳转至新的首发界面。同时要将socket接口数据也一并传送。
void CARQprojectDlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
if (!jumpEN)
{
MessageBox(_T("未建立连接!"), NULL, MB_ICONEXCLAMATION);
return;
}
CMyARQ myarq;
myarq.soc = client_soc;
myarq.DoModal();
//CDialogEx::OnOK();
}
6、在新的对话框(CMyARQ)中添加初始化信息。设置对话框的标题、大小。同时添加编辑框控件,设置只读状态。为其添加变量connectinfo。同时打开发送文件sinfo.txt。
void CMyARQ::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT1, recvinfo);
DDX_Control(pDX, IDC_EDIT2, resend);
this->SetWindowText(_T("ARQ接收端信息"));
CRect temprect(0, 0, 800, 600);//设置对话框大小
CWnd::SetWindowPos(NULL, 0, 0, temprect.Width(), temprect.Height(), SWP_NOZORDER | SWP_NOMOVE);
CFont e,e1;
e.CreatePointFont(200, _T("宋体"));
GetDlgItem(IDC_STATIC)->SetFont(&e);
e1.CreatePointFont(120, _T("宋体"));
recvinfo.SetFont(&e1);
sf.open("sinfo.txt", std::ios::in);
if (!sf.is_open())
{
MessageBox(_T("文件打开失败!"), NULL, MB_ICONERROR);
return;
}
soc1 = CMyARQ::soc;
}
7、在对话框中添加一个编辑框控件,用于显示收发信息。给其添加变量recvinfo。另外添加一个编辑框控件,给其添加变量resend,用于显示超时重传信息。
8、添加“开始”与“停止”按钮。开始按钮对应开始使用ARQ协议发送数据的代码。停止按钮对应数据暂停发送的代码。同时添加一个选项控件,命名为“使用校验和”。如果用户勾选,在点击开始按钮的时候会产生使用校验和发送数据的子线程。如果用户不勾选,则会产生使用普通使用ARQ发送协议的子线程。
送协议的子线程。
9、编写使用校验和进行ARQ协议传输的子线程。如果socket连接合法,进行下一步操作。初始化数据发送与接收缓冲区,发送协商数据,告知发送端使用校验和,同时接收发送端打招呼信息。加入recvinfo变量中显示给用户。每次发送完一个消息不是立即转移到下一个消息,而是等待接收应答帧,使用while循环。如果应答帧正确,跳出内循环,准备吓一跳数据发送。如果应答帧错误,包括校验和错误、发送报文出错、应答帧出错,重新发送本次数据,seq不改变。
每一次发送数据设置超时定时器,超时时间为200ms,规定时间内没有收到回送应答帧就重传。收到则取消超时计时器。
在发送数据之前算出报文校验和填入相应位置,接收到数据之后按照规则解析出数据内容,自动判断是否正确,计算出校验和与应答帧所带的校验和对比,如果正确跳转到下一数据发送,否则重传本次数据。所有信息包括报文内容,本次传输状态、校验和,应答帧seq,都会显示在recvinfo当中。一旦出现超时,会将超时报文信息显示到另一个编辑控件当中,即resend变量。
void CMyARQ::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
switch (nIDEvent)
{
case 1:
//MessageBox(_T("lll"), NULL, MB_ICONERROR);
send(soc1, buff1, sizeof(buff1), 0);
char r[64];
char s[8]; itoa(buff1[0], s, 10);
strcpy(r, "本次超时\r\n进行重传\r\nseq:");
strcat(r, s);
resend.SetWindowText(CString(r));
break;
default:break;
}
CDialogEx::OnTimer(nIDEvent);
}
模拟出错部分和server线程类似,可以自行编写。
9、编写不使用校验和的子线程。使用校验和相比于不使用校验和有两个区别:是否计算校验和,报文的格式不同。不使用校验和要去掉每一次发送前验证校验和、计算校验和的步骤;并且数据部分紧跟在发送seq后面即可。其余与上面的代码相同。编写的时候使用另一类子线程。
if (num > 0)
{
KillTimer(p->m_hWnd, 1);
//计算校验和是否正确
char rsum = 0;
for (int i = 3;i<6; i++)
{
if (rbuff[i] == '\0')
break;
rsum += rbuff[i];
}
rsum += rbuff[0];
rsum += rbuff[1];
//计算完毕
//进行判断
if (rsum != rbuff[2])
{
strcat(str, "\r\nCSUM ERROR, recv:");
itoa(rbuff[2], s, 10); strcat(str, s); strcat(str, " cal:");
itoa(rsum, s, 10); strcat(str, s);
send(soc, buff, sizeof(buff), 0);
p->recvinfo.SetWindowText(CString(str));
p->recvinfo.LineScroll(p->recvinfo.GetLineCount());
Sleep(2000);
continue;
}
//
if (rbuff[3] == 'N')
{
strcat(str, "receive from server:\r\nNAK sseq: ");
char s[8]; itoa(rbuff[1], s, 10);
strcat(str, s); strcat(str, "本次重传\r\n");
send(soc, buff, sizeof(buff), 0);
p->recvinfo.SetWindowText(CString(str)); //seq++;
}
else
{
strcat(str, "receive from server:\r\nACK sseq: ");
char s[8]; itoa(rbuff[1], s, 10);
strcat(str, s); strcat(str, "本次成功\r\n");
p->recvinfo.SetWindowText(CString(str));
break;
}
}
p->recvinfo.LineScroll(p->recvinfo.GetLineCount());
Sleep(2000);
10、添加“取消”按钮。按下按钮,发送端停止发送数据,并且发送退出信息给接收端,断开连接。接收端收到退出信息后,将此socket接口重置,连接数减一,以便空出新的连接位置,通知接收端和发送端都告知用户连接结束。
注意事项
实验环境为Windows10操作系统,代码编写使用的集成开发环境为Visual Studio2017。代码使用C++语言编写。图形化界面使用Microsoft vs所提供的MFC工具箱构造。在低版本windows系统上或者没有安装相应编译器以及MFC工具的目标机上无法编译源代码。
完整下载地址
ARQ协议模拟完整代码包链接https://github.com/prestyan/Computer_Network
Linux下载命令:
sudo wget https://github.com/prestyan/Computer_Network.git
#或者
sudo wget 专门的下载包地址