2.1.2 小试牛刀--模拟实现Windows的TCP程序
实例功能 使用Visual C++开发一个类似于Windows自带的TCP程序
源码路径 光盘\yuanma\2\TCP
本实例的目的是,使用Visual C++ 6.0开发一个类似于Windows自带的TCP程序。
1. 划分模块
项目中TCP模块的功能描述如下。
(1) 服务器端能够以默认选项启动提供服务功能,默认选项包括服务器端的IP或主机名和端口号。
(2) 服务器端能够根据用户指定的选项,提供服务功能,这些选项包括服务器端的IP或主机名和端口号。
(3) 如果服务器以错误选项启动,则提示错误信息,并终止程序。
(4) 客户端连接到服务器端后,可以发送信息到服务器,也可以接收来自服务器端的响应。
(5) 如果客户端不能连接到服务器端,则输出错误信息。
(6) 当客户端以错误选项启动时,会提示错误信息,并终止程序。
根据上述功能分析,得出TCP模块的构成功能如下所示。
服务器端
初始化模块:初始化全局变量,并为全局变量赋值,初始化Winsock,并加载Winsock库。
功能控制模块:是其他模块的调用函数,实现参数获取、用户帮助和错误处理等。
循环控制模块:用于控制服务器端的服务次数,如果超过指定次数则停止服务。
服务模块:为客户提供服务,接收客户端的数据,并发送数据到客户端。
客户端
初始化模块:用于初始化客户端的Winsock,并加载Winsock库。
功能控制模块:是其他模块的调用函数,实现参数获取、用户帮助和错误处理等。
传输控制模块:用于控制整个客户端的数据传输,包括发送和接收。
总体结构如图2-5所示。
图2-5 TCP模块的总体结构 |
2. 运行流程分析
(1) 服务器端运行流程。
在服务器端,首先调用GetArgments()函数获取用户提供的选项,如果没有提供选项,则直接使用默认值,如果有选项提供并成功获取,则初始化变量和Winsock,并创建TCP流套接字,然后解析主机名或IP地址,解析成功后设置服务器地址的各个参数,包括地址族和IP地址等。接下来将创建的TCP流套接字和设定的服务器地址绑定。绑定成功后开始侦听客户端的连接,并调用循环函数LoopControl()函数和Service()函数进行接收客户端的连接、接收数据和发送数据等操作。当服务次数达到最多服务次数时,则关闭服务器,并释放所占用的资源。
(2) 客户端运行流程。
客户端执行时必须带选项,首先判断用户提供参数的个数,如果参数不是3个,则说明没有提供正确的选项,退出当前程序。如果等于3个,则调用GetArgments()函数获取用户提供的选项,如果获取的选项错误则终止程序,正确则创建TCP流套接字,接着进行和服务器端类似的操作,即解析主机和IP地址,然后进行连接服务器的操作,连接成功则输出连接信息,并发送信息到客户端,然后接收来自服务器端的响应,并将接收到的信息输出。最后关闭套接字并释放所占用的资源。
3. 设计数据结构
(1) 服务器端的全局变量如下:
- /*定义全局变量*/
- char *hostName;
- unsigned short maxService;
- unsigned short port;
(2) 客户端的全局变量如下:
- /*定义全局变量*/
- unsigned short port;
- char *hostName;
4. 规划函数
(1) 服务器端。服务器端的构成函数如下。
intial():用于初始化服务器端的全局变量。
InitSockets():用于初始化Winsock。
GetArgments():用于获取用户提供的选项。
ErrorPrint():用于输出错误信息。
LoopControl():实现循环控制,当服务器次数在指定范围内时,将接收客户端请求,并创建一个线程为客户端服务。
Service():用于服务客户端。
(2) 客户端。客户端的构成函数如下。
InitSockets():用于初始化Winsock。
GetArgment():用于获取用户提供的选项。
ErrorPrint():用于输出错误信息。
5. 具体编码
(1) 服务器端编码
① 预处理
预处理包括文件导入、头文件加载、定义常量、定义变量等操作。具体代码如下:
- /*导入库文件*/
- #pragma comment(lib, "wsock32.lib")
- /*加载头文件*/
- #include <stdio.h>
- #include <winsock2.h>
- /*自定义函数原型*/
- void initial();
- int InitSockets(void);
- void GetArgments(int argc, char **argv);
- void ErrorPrint(x);
- void userHelp();
- int LoopControl(SOCKET listenfd, int isMultiTasking);
- void Service(LPVOID lpv);
- /*定义常量*/
- #define MAX_SER 10
- /*定义全局变量*/
- char *hostName;
- unsigned short maxService;
- unsigned short port;
② 初始化模块
此处的初始化分为全局变量初始化和Winsock初始化两部分,分别通过如下两个函数来实现:
initial():用于初始化全局变量,通过设置hostName="127.0.0.1",说明程序运行时仅限定客户端和服务器在同一台机器上。
InitSockets(void):用于初始化Winsock。
对应的代码如下:
- /*初始化全局变量函数*/
- void initial()
- {
- hostName = "127.0.0.1";
- maxService = 3;
- port = 9999;
- }
- /*初始化Winsocket函数*/
- int InitSockets(void)
- {
- WSADATA wsaData;
- WORD sockVersion;
- int err;
- /*设置Winsock版本号*/
- sockVersion = MAKEWORD(2, 2);
- /*初始化Winsock*/
- err = WSAStartup(sockVersion, &wsaData);
- /*如果初始化失败*/
- if (err != 0)
- {
- printf("Error %d: Winsock not available\n", err);
- return 1;
- }
- return 0;
- }
③ 功能控制模块
此模块提供了参数获取、错误输出和用户帮助等功能,上述功能分别通过如下3个函数实现:
GetArgments:用于获取用户提供的选项值。
ErrorPrint:用于输出错误。
userHelp:用于输出帮助信息。
对应的实现代码如下:
- /*获取选项函数*/
- void GetArgments(int argc, char **argv)
- {
- int i;
- for(i=1; i<argc; i++)
- {
- /*参数的第一个字符若是“-”*/
- if (argv[i][0] == '-')
- {
- /*转换成小写*/
- switch (tolower(argv[i][1]))
- {
- /*若是端口号*/
- case 'p':
- if (strlen(argv[i]) > 3)
- port = atoi(&argv[i][3]);
- break;
- /*若是主机名*/
- case 'h':
- hostName = &argv[i][3];
- break;
- /*最多服务次数*/
- case 'n':
- maxService = atoi(&argv[i][3]);
- break;
- /*其他情况*/
- default:
- userHelp();
- break;
- }
- }
- }
- return;
- }
- /*错误输出函数*/
- void ErrorPrint(x)
- {
- printf("Error %d: %s\n", WSAGetLastError(), x);
- }
- /*用户帮助函数*/
- void userHelp()
- {
- printf("userHelp: -h:str -p:int -n:int\n");
- printf(" -h:str The host name \n");
- printf(" The default host is 127.0.0.1\n");
- printf(" -p:int The Port number to use\n");
- printf(" The default port is 9999\n");
- printf(" -n:int The number of service,below MAX_SER \n");
- printf(" The default number is 3\n");
- ExitProcess(-1);
- }
④ 循环控制模块
此模块的功能是通过函数LoopControl实现的,具体代码如下:
- /*循环控制函数*/
- int LoopControl(SOCKET listenfd, int isMultiTasking)
- {
- SOCKET acceptfd;
- struct sockaddr_in clientAddr;
- int err;
- int nSize;
- int serverNum = 0;
- HANDLE handles[MAX_SER];
- int myID;
- /*服务次数小于最大服务次数*/
- while (serverNum < maxService)
- {
- nSize = sizeof(clientAddr);
- /*接收客户端请求*/
- acceptacceptfd = accept(listenfd, (struct sockaddr *)
- &clientAddr, &nSize);
- /*如果接收失败*/
- if (acceptfd == INVALID_SOCKET)
- {
- ErrorPrint("Error: accept failed\n");
- return 1;
- }
- /*接收成功*/
- printf("Accepted connection from client at %s\n",
- inet_ntoa(clientAddr.sin_addr));
- /*如果允许多任务执行*/
- if (isMultiTasking)
- {
- /*创建一个新线程来执行任务,新线程的初始堆栈大小为1000,线程执行函数
- 是Service(),传递给Service()的参数为acceptfd*/
- handles[serverNum] = CreateThread(NULL, 1000,
- (LPTHREAD_START_ROUTINE)Service,
- (LPVOID) acceptfd, 0, &myID);
- }
- else
- /*直接调用服务客户端的函数*/
- Service((LPVOID)acceptfd);
- serverNum++;
- }
- if (isMultiTasking)
- {
- /*在一个线程中等待多个事件,当所有对象都被通知时函数才会返回,且等待没有时间限制*/
- err = WaitForMultipleObjects(maxService, handles, TRUE, INFINITE);
- printf("Last thread to finish was thread #%d\n", err);
- }
- return 0;
- }
⑤ 服务模块
此模块的功能是通过函数Service()实现的,功能是实现接收、判断来自客户端的数据,并发送数据到客户端。具体代码如下:
- /*服务函数*/
- void Service(LPVOID lpv)
- {
- SOCKET acceptfd = (SOCKET)lpv;
- const char *msg = "HELLO CLIENT";
- char response[4096];
- /*用0初始化response[4096]数组*/
- memset(response, 0, sizeof(response));
- /*接收数据,存入response中*/
- recv(acceptfd, response, sizeof(response), 0);
- /*如果接收到的数据和预定义的数据不同*/
- if (strcmp(response, "HELLO SERVER"))
- {
- printf("Application: client not using expected "
- "protocol %s\n", response);
- }
- else
- /*发送服务器端信息到客户端*/
- send(acceptfd, msg, strlen(msg)+1, 0);
- /*关闭套接字*/
- closesocket(acceptfd);
- }
⑥ 主函数模块
主函数是整个程序的入口,里面实现了套接字的创建、绑定、侦听和释放等操作,并且实现了对各个功能函数的调用。具体代码如下:
- /*主函数*/
- int main(int argc, char **argv)
- {
- SOCKET listenfd;
- int err;
- struct sockaddr_in serverAddr;
- struct hostent *ptrHost;
- initial();
- GetArgments(argc, argv);
- InitSockets();
- /*创建TCP流套接字,在domain参数为PF_INET的SOCK_STREAM的
套接口中,protocol参数为0意味着告诉内核选择IPPRPTP_TCP,
这也意味着套接口将使用TCP/IP协议*/ - listenfd = socket(PF_INET, SOCK_STREAM, 0);
- /*如果创建套接字失败*/
- if (listenfd == INVALID_SOCKET)
- {
- printf("Error: out of socket resources\n");
- return 1;
- }
- /*如果是IP地址*/
- if (atoi(hostName))
- {
- /*将IP地址转换成32二进制表示法,返回32位二进制的网络字节序*/
- u_long ip_addr = inet_addr(hostName);
- /*根据IP地址找到与之匹配的主机名*/
- ptrHost = gethostbyaddr((char*)&ip_addr,
- sizeof(u_long), AF_INET);
- }
- /*如果是主机名*/
- else
- /*根据主机名获取一个指向hosten的指针,该结构中包含了该主机所有的IP地址*/
- ptrHost = gethostbyname(hostName);
- /*如果解析失败*/
- if (!ptrHost)
- {
- ErrorPrint("cannot resolve hostname");
- return 1;
- }
- /*设置服务器地址*/
- /*设置地址族为PF_INET*/
- serverAddr.sin_family = PF_INET;
- /*将一个通配的Internet地址转换成无符号长整型的网络字节序数*/
- serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
- /*将端口号转换成无符号短整型的网络字节序数*/
- serverAddr.sin_port = htons(port);
- /*将套接字与服务器地址绑定*/
- err = bind(listenfd, (const struct sockaddr *) &serverAddr,
- sizeof(serverAddr));
- /*如果绑定失败*/
- if (err == INVALID_SOCKET)
- {
- ErrorPrint("Error: unable to bind socket\n");
- return 1;
- }
- /*开始侦听,设置等待连接的最大队列长度为SOMAXCONN,默认值为5个*/
- err = listen(listenfd, SOMAXCONN);
- /*如果侦听失败*/
- if (err == INVALID_SOCKET)
- {
- ErrorPrint("Error: listen failed\n");
- return 1;
- }
- LoopControl(listenfd, 1);
- printf("Server is down\n");
- /*释放Winscoket初始化时占用的资源*/
- WSACleanup();
- return 0;
- }
(2) 客户端
① 预处理
预处理包括文件导入、头文件加载、定义常量、定义变量等操作。具体代码如下:
- /*导入库文件*/
- #pragma comment(lib, "wsock32.lib")
- /*加载头文件*/
- #include <stdio.h>
- #include <winsock2.h>
- /*自定义函数*/
- int InitSockets(void);
- void GetArgument(int argc, char **argv);
- void ErrorPrint(x);
- void userHelp();
- /*定义全局变量*/
- unsigned short port;
- char *hostName;
② 初始化模块
初始化模块无需对全局变量赋值,只须实现对Winsock的初始化,包括初始化套接字版本号和加载Winsock库。具体代码如下:
- /*初始化Winsock函数*/
- int InitSockets(void)
- {
- WSADATA wsaData;
- WORD sockVersion;
- int err;
- /*设置Winsock版本号*/
- sockVersion = MAKEWORD(2, 2);
- /*初始化Winsock*/
- err = WSAStartup(sockVersion, &wsaData);
- /*如果初始化失败*/
- if (err != 0)
- {
- printf("Error %d: Winsock not available\n", err);
- return 1;
- }
- return 0;
- }
③ 功能控制模块
此模块提供了参数获取、错误输出和用户帮助等功能,上述功能分别通过如下函数来实现。
GetArgments:用于获取用户提供的选项值。
ErrorPrint:用于输出错误。
userHelp:用于输出帮助信息。
对应的实现代码如下:
- /*获取选项函数*/
- void GetArgments(int argc, char **argv)
- {
- int i;
- for(i=1; i<argc; i++)
- {
- /*参数的第一个字符若是“-”*/
- if (argv[i][0] == '-')
- {
- /*转换成小写*/
- switch (tolower(argv[i][1]))
- {
- /*若是端口号*/
- case 'p':
- if (strlen(argv[i]) > 3)
- port = atoi(&argv[i][3]);
- break;
- /*若是主机名*/
- case 'h':
- hostName = &argv[i][3];
- break;
- /*其他情况*/
- default:
- userHelp();
- break;
- }
- }
- }
- return;
- }
- /*错误输出函数*/
- void ErrorPrint(x)
- {
- printf("Error %d: %s\n", WSAGetLastError(), x);
- }
- /*用户帮助函数*/
- void userHelp()
- {
- printf("userHelp: -h:str -p:int\n");
- printf(" -h:str The host name \n");
- printf(" -p:int The Port number to use\n");
- ExitProcess(-1);
- }
④ 数据传输控制模块
客户端程序会把数据的传入传出部分放在主函数中执行,也就是说此处的数据传输功能是通过主函数实现的。主函数中包括套接字创建、绑定和释放,并实现对服务器连接、数据发送、数据接收等各个模块的调用。具体实现代码如下:
- /*主函数*/
- int main(int argc, char **argv)
- {
- SOCKET clientfd;
- int err;
- struct sockaddr_in serverAddr;
- struct hostent *ptrHost;
- char response[4096];
- char *msg = "HELLO SERVER";
- GetArgments(argc, argv);
- if (argc != 3)
- {
- userHelp();
- return 1;
- }
- GetArgments(argc,argv);
- InitSockets();
- /*创建套接字*/
- clientfd = socket(PF_INET, SOCK_STREAM, 0);
- /*如果创建失败*/
- if (clientfd == INVALID_SOCKET)
- {
- ErrorPrint("no more socket resources");
- return 1;
- }
- /*根据IP地址解析主机名*/
- if (atoi(hostName))
- {
- u_long ip_addr = inet_addr(hostName);
- ptrHost = gethostbyaddr((char*)&ip_addr,
- sizeof(u_long), AF_INET);
- }
- /*根据主机名解析IP地址*/
- else
- ptrHost = gethostbyname(hostName);
- /*如果解析失败*/
- if (!ptrHost)
- {
- ErrorPrint("cannot resolve hostname");
- return 1;
- }
- /*设置服务器端地址选项*/
- serverAddr.sin_family = PF_INET;
- memcpy((char*)&(serverAddr.sin_addr),
- ptrHost->h_addr, ptrHost->h_length);
- serverAddr.sin_port = htons(port);
- /*连接服务器*/
- err = connect(clientfd, (struct sockaddr *) &serverAddr,
- sizeof(serverAddr));
- /*连接失败*/
- if (err == INVALID_SOCKET)
- {
- ErrorPrint("cannot connect to server");
- return 1;
- }
- /*连接成功后,输出信息*/
- printf("You are connected to the server\n");
- /*发送消息到服务器端*/
- send(clientfd, msg, strlen(msg)+1, 0);
- memset(response, 0, sizeof(response));
- /*接收来自服务器端的消息*/
- recv(clientfd, response, sizeof(response), 0);
- printf("server says %s\n", response);
- /*关闭套接字*/
- closesocket(clientfd);
- /*释放Winscoket初始化时占用的资源*/
- WSACleanup();
- return 0;
- }
到此为止,整个实例设计完毕,编译执行后的效果如图2-6所示。
图2-6 执行效果 |