大型程序组织与网络协议应用示例
1. 程序结构概述
在开发大型程序时,将代码按模块划分是常见且有效的做法。不同的程序员小组负责不同的代码文件,这样能使程序的实现、测试、控制和管理更加高效。以下是程序结构组织的关键步骤:
1.
模块划分
:将应用需求分解为独立的模块,便于项目管理和理解程序结构。
2.
定义模块交互
:明确不同模块之间的交互方式、依赖关系以及通信机制,例如回调机制。
3.
模块服务映射
:将每个模块的服务映射到编程概念,如函数和类,这些都定义在相应的代码文件中。
4.
详细模块描述
:为每个模块提供详细的用途、服务和操作方式描述,方便维护和升级。
5.
代码文件与编译
:每个模块由多个代码文件组成,每个文件实现模块的部分服务。所有文件单独编译,由链接器将生成的代码链接起来。
6.
头文件选择
:选择可被其他模块共享的函数,将其声明放在头文件中,作为模块的接口文件。
1.1 程序元素放置规则
| 元素类型 | 放置位置 |
|---|---|
| 命名空间、内联函数定义、常量、枚举、类型同义词、宏、模板、类型和函数声明 | 头文件(使用 #ifndef 防止多重包含) |
| 变量和函数定义 | 代码文件 |
| extern 变量 | 可放在头文件,但建议放在使用它们的代码文件中,以明确使用位置 |
1.2 头文件组织方式
1.2.1 单头文件方式
将所有声明放在一个头文件中,定义放在使用它们的代码文件中。这种方式适用于小型程序,方便查找和编辑声明,但对于大型程序效率较低,因为头文件内容更改时,所有代码文件都需要重新编译,且多人修改易引入错误,文件过大也不利于阅读和维护。
1.2.2 多头文件方式
每个代码文件有自己的头文件,便于快速了解代码文件实现的程序部分和提供的服务。在大型程序中,这种方式有助于更好地控制、理解和维护程序。如果一个头文件发生变化,只需重新编译包含它的代码文件。公共信息可放在一个头文件中,方便修改。
2. 网络协议应用示例
2.1 网络协议简介
网络协议是使系统能够通信的一组规则,可通过硬件、软件或两者结合实现。其操作通常基于国际组织定义的标准,例如著名的互联网协议(IP)由 IETF 组织在 RFC 791 标准中规定。协议标准规定了系统之间交换的信息、消息格式、意义和处理程序,通常使用有限状态机来指导协议的实现。
2.2 待实现协议描述
本次实现的是 ITU - T Q.931 信令协议的简化版本,用于 ISDN 网络中呼叫的建立、维护和释放。以下是协议的主要流程:
1.
呼叫建立
:主叫用户拨号,创建 SETUP 消息,通过 ISDN 网络路由到被叫用户。如果被叫用户接受呼叫(如拿起电话),发送 CONNECT 消息回主叫用户,被叫用户回复 CONNECT_ACK,呼叫建立完成,预留电路进行数据传输。
2.
呼叫释放
:任何一方关闭连接(如挂断电话),发送 RELEASE 消息给另一方,释放电路。
2.3 应用程序设计
应用程序提供一个菜单选项,模拟双方之间的消息交换以建立和释放呼叫。每个消息由唯一代码标识,并包含特定信息。为了简化,假设每个消息只包含代码和呼叫参考值(CRV)。
2.4 模块划分与文件结构
程序的功能被划分为多个模块,每个源文件代表一个模块:
-
prtcl.cpp
:实现与上层和下层的接口,解析并转发接收到的消息到 fsm.cpp。
-
fsm.cpp
:实现协议的有限状态机,根据接收到的消息改变呼叫状态。
-
send.cpp
:创建消息并发送到下层,同时模拟被叫方的响应。
2.5 头文件 general.h
/* general.h */
#ifndef general_h
#define general_h
#include <iostream>
#include <vector>
#include <map>
using std::cout;
using std::cin;
using std::map;
using std::vector;
#define LOOPBACK_MODE
// Message codes according to the ITU-T standard.
const int SETUP = 0x5;
const int CONNECT = 0x7;
const int RELEASE = 0x4D;
const int RELEASE_COMPLETE = 0x5A;
struct Msg /* This structure is used as an argument in function calls. */
{
int code;
int CRV;
int call_index;
/* Normally, the structure contains many more members that correspond
to the information elements conveyed by each message. */
};
class Call // Each call is represented by a Call object.
{
private:
int CRV; // It holds the identifier of the call.
int state; // It holds the state of the call.
/* Normally, the class contains many more members related to the
call, such as the phone numbers of the two parties and the duration of the
call. */
public:
Call(int crv);
int fsm(Msg *p);
int send_msg(Msg *p);
};
class Prtcl
{
private:
vector<Call> calls; // We use a vector to store the calls.
map<int, int> map_CRV; /* Mapping of the key, which is the CRV value
of each call with the position of the object in the vector. Thus, we find the
position of the object that corresponds to a CRV value. */
public:
int handle_up_msg(Msg *p); /* It is used for communication with the
upper layer. */
int handle_low_msg(int data[]); /* It is used for communication with
the lower layer. */
int find_call(int CRV) const;
void show() const {cout << "\nCalls: " << calls.size() << '\n';} /* As
you see we can define the function in the header file. It displays the number
of the established calls. */
};
#endif
2.6 prtcl.cpp 文件
#include "general.h"
#include "fsm.h"
Prtcl prtcl; /* I declare it as global, to show you how to use it as extern
in other files. */
int main()
{
int CRV, index, num, sel;
Msg msg;
CRV = 100; // Just, an initial value.
while(1)
{
cout << "\nOperations\n";
cout << "------------\n";
cout << "1. Establish Call\n";
cout << "2. Release Call\n";
cout << "3. Show Calls\n";
cout << "4. Exit\n";
cout << "\nEnter choice: ";
cin >> sel;
switch(sel)
{
case 1:
msg.code = SETUP;
msg.CRV = CRV; // Call identifier.
CRV++; /* For simplicity, the CRV of each call is
incremented by one. */
prtcl.handle_up_msg(&msg); /* In a real
application, the user enters information related to the call, such as the
phone number of the called user. */
break;
case 2:
cout << "Enter CRV of the call to be released: ";
cin >> num;
index = prtcl.find_call(num); /* We find the
call, that is, the object, with the same CRV as the one entered by the
user. */
if(index != -1)
{
msg.code = RELEASE;
msg.CRV = num;
msg.call_index = index;
prtcl.handle_up_msg(&msg); /* Activate the
call release procedure. */
}
else
cout << "Non existing call\n";
break;
case 3:
prtcl.show();
break;
case 4:
return 0;
default:
cout << "\nWrong choice\n";
break;
}
}
return 0;
}
int Prtcl::handle_up_msg(Msg *p) /* Handle the message coming from the upper
layer. */
{
int index;
if(p->code == SETUP)
{
Call c(p->CRV); // Create an object to manage the call.
calls.push_back(c); // Store the object.
index = calls.size()-1;
map_CRV[p->CRV] = index; /* Map the CRV to the position of the
object in the vector. */
calls[index].fsm(p); /* Forward the message to the object that
manages the call. */
}
else if(p->code == RELEASE)
{
index = p->call_index;
calls[index].fsm(p);
}
else
{
cout << "Upper message_" << p->code << " not supported\n";
return -1;
}
return 0;
}
int Prtcl::handle_low_msg(int data[]) /* Handle the message coming from the
lower layer. */
{
int ret, index;
Msg msg; /* The information coming from the lower layer is stored in
the Msg members. */
index = find_call(data[0]); /* We find the call, that is, the object
which has the same CRV with the CRV contained in the message. */
if(index == -1)
{
cout << "Non existing call\n";
return -1;
}
msg.code = data[1];
switch(msg.code)
{
case CONNECT:
calls[index].fsm(&msg); /* Forward the message to the
object that manages the call. */
break;
case RELEASE_COMPLETE:
ret = calls[index].fsm(&msg); /* Here is an example to
check the return value. The respective object is deleted. */
if(ret == CLEAR_CALL)
{
calls.erase(calls.begin()+index);
map_CRV.erase(data[0]);
}
break;
default:
cout << "Received message_" << msg.code << " not
supported\n";
return -1;
}
return 0;
}
int Prtcl::find_call(int CRV) const /* To find the object we use map_CRV
which maps the CRV with its position in the vector. */
{
auto it = map_CRV.find(CRV);
if(it != map_CRV.end())
return it->second;
return -1;
}
2.7 协议状态机流程
graph TD;
A[IDLE] -->|SETUP| B[CALL_INIT];
B -->|CONNECT| C[ACTIVE];
C -->|RELEASE| D[RELEASE_INIT];
D -->|RELEASE_COMPLETE| E[CALL_RELEASED];
在后续内容中,我们将继续介绍 fsm.cpp、fsm.h 和 send.cpp 文件的内容,以及网络协议实现的测试建议和多文件程序的创建方法。
3. fsm.cpp 和 fsm.h 文件
3.1 fsm.cpp 文件
fsm.cpp 文件实现了协议的状态机。根据接收到的消息,呼叫状态会发生相应的变化。以下是 fsm.cpp 的代码:
#include "general.h"
#include "fsm.h"
Call::Call(int crv)
{
CRV = crv;
state = IDLE;
}
int Call::fsm(Msg *p)
{
switch(state)
{
case IDLE:
if(p->code == SETUP)
{
state = CALL_INIT;
send_msg(p);
}
else
{
cout << "Unexpected message_" << p->code << " in
state_" << state << '\n';
return UNKNOWN_MSG;
}
break;
case CALL_INIT:
if(p->code == CONNECT)
{
cout << "\nCall with CRV " << CRV << " is
established\n";
state = ACTIVE;
}
break;
case ACTIVE:
if(p->code == RELEASE)
{
state = RELEASE_INIT;
send_msg(p);
}
break;
case RELEASE_INIT:
if(p->code == RELEASE_COMPLETE)
{
cout << "\nCall with CRV " << CRV << " is
released\n";
return CLEAR_CALL;
}
break;
default:
cout << "Not supported state\n";
return UNKNOWN_STATE;
}
return 0;
}
3.2 fsm.h 文件
fsm.h 文件定义了呼叫状态的枚举和返回值常量。以下是 fsm.h 的代码:
/* fsm.h */
#ifndef fsm_h
#define fsm_h
// Call states.
enum fsm_states {IDLE = 1, CALL_INIT, ACTIVE, RELEASE_INIT};
// Return values.
const int CLEAR_CALL = 1;
const int UNKNOWN_MSG = 2;
const int UNKNOWN_STATE = 3;
#endif
3.3 状态机状态说明
| 状态名称 | 状态说明 |
|---|---|
| IDLE | 初始状态,等待 SETUP 消息 |
| CALL_INIT | 呼叫初始化状态,等待 CONNECT 消息 |
| ACTIVE | 呼叫已建立状态,可进行通信 |
| RELEASE_INIT | 呼叫释放初始化状态,等待 RELEASE_COMPLETE 消息 |
4. send.cpp 文件
send.cpp 文件负责创建消息并将其发送到下层。为了简化,假设每个消息只包含代码和 CRV。以下是 send.cpp 的代码:
#include "general.h"
extern Prtcl prtcl;
int Call::send_msg(Msg *p)
{
int data[2]; /* Since the message contains only its code and the CRV,
its size is set to 2. The exercise C.11.22 shows an example of how to create
a message. */
data[0] = CRV;
data[1] = p->code;
switch(data[1])
{
case SETUP:
break;
case RELEASE:
break;
default:
cout << "Unexpected message_" << data[1] << " for
transmission\n";
return -1;
}
#ifndef LOOPBACK_MODE
/* In a real application, here is the place where we typically call a
function of the lower layer (e.g., a function of the driver) with arguments
the data array and its length for transmission in the network. */
cout << "Only loopback tests are supported\n";
#else
switch(data[1])
{
case SETUP:
data[0] = CRV;
data[1] = CONNECT; /* We simulate the scenario that the
other party accepted the call and the message CONNECT is received to complete
the call establishment. */
break;
case RELEASE:
data[0] = CRV;
data[1] = RELEASE_COMPLETE; /* We simulate the scenario
that the other party closed the connection and the message RELEASE_COMPLETE
is received to complete the release of the call. */
break;
}
prtcl.handle_low_msg(data);
#endif
return 0;
}
4.1 消息发送流程
- 根据传入的消息,将 CRV 和消息代码存储在 data 数组中。
- 检查消息代码,如果是 SETUP 或 RELEASE 消息,继续下一步;否则,输出错误信息并返回 -1。
- 如果未定义 LOOPBACK_MODE,输出仅支持回环测试的信息;否则,模拟对方的响应消息,并调用 prtcl.handle_low_msg(data) 处理响应消息。
5. 网络协议实现的测试建议
在实现网络协议时,测试是确保代码正确性的重要步骤。以下是一些测试建议:
1.
寻找模拟应用
:如果可以找到市场上模拟对方的应用程序,使用它来测试代码。
2.
回环测试
:如果找不到模拟应用,可以定义 LOOPBACK_MODE 宏,并在同一应用程序中添加代码来模拟对方。这样可以在本地测试大部分代码,修复错误后再部署到客户环境中。
3.
逐步添加功能
:编写模拟代码时,应逐步添加功能,先支持基本测试,再逐渐增加复杂度。
6. 多文件程序的创建方法
创建由多个文件组成的程序,取决于所使用的编译器。以 Microsoft Visual Studio 的集成开发环境(IDE)为例:
1.
创建项目
:选择类似 File->New->Project 的菜单选项来创建程序。
2.
添加文件
:使用 Project->Add_New_Item 选项添加源文件。
3.
编译文件
:可以单独编译源文件,也可以使用 Build 命令一起编译所有文件。
6.1 程序运行示例
以下是程序的运行示例:
Enter choice: 1
Call with CRV 100 is established
Enter choice: 1
Call with CRV 101 is established
Enter choice: 2
Enter CRV of the call to be released: 100
Call with CRV 100 is released
7. 总结
通过以上内容,我们了解了大型程序的组织方式,包括模块划分、头文件的使用等。同时,通过一个简化的网络协议实现示例,展示了如何使用面向对象的设计方法来开发程序。在实际开发中,合理的程序结构和有效的测试方法对于确保程序的正确性和可维护性至关重要。
7.1 关键知识点总结
| 知识点 | 描述 |
|---|---|
| 程序结构 | 将程序按模块划分,明确模块交互和服务映射,合理放置程序元素 |
| 网络协议 | 一组使系统能够通信的规则,可基于有限状态机实现 |
| 状态机 | 根据接收到的消息改变呼叫状态,实现协议的逻辑 |
| 消息发送 | 创建消息并发送到下层,可模拟对方响应进行测试 |
| 多文件程序 | 使用编译器的 IDE 创建项目和添加文件,进行编译 |
超级会员免费看
16万+

被折叠的 条评论
为什么被折叠?



