首页 > 代码库 > 2.1.2 小试牛刀--模拟实现Windows的TCP程序

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) 服务器端的全局变量如下:

  1. /*定义全局变量*/  
  2. char *hostName;  
  3. unsigned short maxService;  
  4. unsigned short  port; 

 

(2) 客户端的全局变量如下:

  1. /*定义全局变量*/  
  2. unsigned short port;  
  3. char *hostName; 

 

4. 规划函数

(1) 服务器端。服务器端的构成函数如下。

intial():用于初始化服务器端的全局变量。

InitSockets():用于初始化Winsock。

GetArgments():用于获取用户提供的选项。

ErrorPrint():用于输出错误信息。

LoopControl():实现循环控制,当服务器次数在指定范围内时,将接收客户端请求,并创建一个线程为客户端服务。

Service():用于服务客户端。

(2) 客户端。客户端的构成函数如下。

InitSockets():用于初始化Winsock。

GetArgment():用于获取用户提供的选项。

ErrorPrint():用于输出错误信息。

5. 具体编码

(1) 服务器端编码

① 预处理

预处理包括文件导入、头文件加载、定义常量、定义变量等操作。具体代码如下:

 

  1. /*导入库文件*/  
  2. #pragma comment(lib, "wsock32.lib")  
  3. /*加载头文件*/  
  4. #include <stdio.h
  5. #include <winsock2.h
  6. /*自定义函数原型*/  
  7. void initial();  
  8. int InitSockets(void);  
  9.  
  10. void GetArgments(int argc, char **argv);  
  11. void ErrorPrint(x);  
  12. void userHelp();  
  13.  
  14. int LoopControl(SOCKET listenfd, int isMultiTasking);  
  15.  
  16. void Service(LPVOID lpv);  
  17.  
  18. /*定义常量*/  
  19. #define MAX_SER 10  
  20. /*定义全局变量*/  
  21. char *hostName;  
  22. unsigned short maxService;  
  23. unsigned short  port;  

② 初始化模块

此处的初始化分为全局变量初始化和Winsock初始化两部分,分别通过如下两个函数来实现:

initial():用于初始化全局变量,通过设置hostName="127.0.0.1",说明程序运行时仅限定客户端和服务器在同一台机器上。

InitSockets(void):用于初始化Winsock。

对应的代码如下:

  1. /*初始化全局变量函数*/  
  2. void initial()  
  3. {  
  4. hostName = "127.0.0.1";  
  5. maxService = 3;  
  6. port = 9999;  
  7. }  
  8.  
  9. /*初始化Winsocket函数*/  
  10. int InitSockets(void)  
  11. {  
  12. WSADATA wsaData;  
  13. WORD sockVersion;  
  14. int err;  
  15.  
  16. /*设置Winsock版本号*/  
  17. sockVersion = MAKEWORD(2, 2);  
  18. /*初始化Winsock*/  
  19. err = WSAStartup(sockVersion, &wsaData);  
  20. /*如果初始化失败*/  
  21. if (err != 0)  
  22. {  
  23. printf("Error %d: Winsock not available\n", err);  
  24. return 1;  
  25. }  
  26. return 0;  
  27. }  

③ 功能控制模块

此模块提供了参数获取、错误输出和用户帮助等功能,上述功能分别通过如下3个函数实现:

GetArgments:用于获取用户提供的选项值。

ErrorPrint:用于输出错误。

userHelp:用于输出帮助信息。

对应的实现代码如下:

  1. /*获取选项函数*/  
  2. void GetArgments(int argc, char **argv)  
  3. {  
  4. int i;  
  5. for(i=1; i<argc; i++)  
  6. {  
  7. /*参数的第一个字符若是“-”*/  
  8. if (argv[i][0] == ‘-‘)  
  9. {  
  10. /*转换成小写*/  
  11. switch (tolower(argv[i][1]))  
  12. {  
  13. /*若是端口号*/  
  14. case ‘p‘:   
  15. if (strlen(argv[i]) > 3)  
  16. port = atoi(&argv[i][3]);  
  17. break;  
  18. /*若是主机名*/  
  19. case ‘h‘:   
  20. hostName = &argv[i][3];  
  21. break;  
  22. /*最多服务次数*/  
  23. case ‘n‘:   
  24. maxService = atoi(&argv[i][3]);  
  25. break;  
  26. /*其他情况*/  
  27. default:  
  28. userHelp();  
  29. break;  
  30. }  
  31. }  
  32. }  
  33. return;  
  34. }  
  35.  
  36. /*错误输出函数*/  
  37. void ErrorPrint(x)  
  38. {   
  39. printf("Error %d: %s\n", WSAGetLastError(), x);  
  40. }  
  41.  
  42. /*用户帮助函数*/  
  43. void userHelp()  
  44. {  
  45. printf("userHelp:  -h:str -p:int -n:int\n");  
  46. printf("           -h:str  The host name \n");  
  47. printf("                   The default host is 127.0.0.1\n");  
  48. printf("           -p:int  The Port number to use\n");  
  49. printf("                   The default port is 9999\n");  
  50. printf("           -n:int  The number of service,below MAX_SER \n");  
  51. printf("                   The default number is 3\n");  
  52. ExitProcess(-1);  
  53. }  

④ 循环控制模块

此模块的功能是通过函数LoopControl实现的,具体代码如下:

  1. /*循环控制函数*/  
  2. int LoopControl(SOCKET listenfd, int isMultiTasking)  
  3. {  
  4. SOCKET acceptfd;  
  5. struct sockaddr_in clientAddr;  
  6. int err;  
  7. int nSize;  
  8. int serverNum = 0;  
  9. HANDLE handles[MAX_SER];  
  10. int myID;  
  11.  
  12. /*服务次数小于最大服务次数*/  
  13. while (serverNum maxService)  
  14. {  
  15. nSize = sizeof(clientAddr);  
  16. /*接收客户端请求*/  
  17. acceptacceptfd = accept(listenfd, (struct sockaddr *)  
  18.                          &clientAddr, &nSize);  
  19. /*如果接收失败*/  
  20. if (acceptfd == INVALID_SOCKET)  
  21. {  
  22. ErrorPrint("Error: accept failed\n");  
  23. return 1;  
  24. }  
  25. /*接收成功*/  
  26. printf("Accepted connection from client at %s\n",  
  27.              inet_ntoa(clientAddr.sin_addr));  
  28. /*如果允许多任务执行*/  
  29. if (isMultiTasking)   
  30. {  
  31. /*创建一个新线程来执行任务,新线程的初始堆栈大小为1000,线程执行函数  
  32. 是Service(),传递给Service()的参数为acceptfd*/  
  33. handles[serverNum] = CreateThread(NULL, 1000,  
  34.                       (LPTHREAD_START_ROUTINE)Service,  
  35.                       (LPVOID) acceptfd, 0, &myID);  
  36.  
  37. }  
  38. else  
  39. /*直接调用服务客户端的函数*/  
  40. Service((LPVOID)acceptfd);  
  41. serverNum++;  
  42. }  
  43.  
  44. if (isMultiTasking)  
  45. {  
  46. /*在一个线程中等待多个事件,当所有对象都被通知时函数才会返回,且等待没有时间限制*/  
  47. err = WaitForMultipleObjects(maxService, handles, TRUE, INFINITE);  
  48. printf("Last thread to finish was thread #%d\n", err);  
  49. }  
  50. return 0;  
  51. }  

⑤ 服务模块

此模块的功能是通过函数Service()实现的,功能是实现接收、判断来自客户端的数据,并发送数据到客户端。具体代码如下:

  1. /*服务函数*/  
  2. void Service(LPVOID lpv)  
  3. {  
  4. SOCKET acceptfd = (SOCKET)lpv;  
  5. const char *msg = "HELLO CLIENT";  
  6. char response[4096];  
  7.  
  8. /*用0初始化response[4096]数组*/  
  9. memset(response, 0, sizeof(response));  
  10. /*接收数据,存入response中*/  
  11. recv(acceptfd, response, sizeof(response), 0);  
  12.  
  13. /*如果接收到的数据和预定义的数据不同*/  
  14. if (strcmp(response, "HELLO SERVER"))   
  15. {  
  16. printf("Application:  client not using expected "  
  17. "protocol %s\n", response);  
  18. }  
  19. else  
  20. /*发送服务器端信息到客户端*/  
  21. send(acceptfd, msg, strlen(msg)+1, 0);  
  22. /*关闭套接字*/  
  23. closesocket(acceptfd);  
  24. }  

⑥ 主函数模块

主函数是整个程序的入口,里面实现了套接字的创建、绑定、侦听和释放等操作,并且实现了对各个功能函数的调用。具体代码如下:

    1. /*主函数*/  
    2. int main(int argc, char **argv)  
    3. {  
    4. SOCKET listenfd;  
    5. int err;  
    6. struct sockaddr_in serverAddr;  
    7. struct hostent *ptrHost;  
    8. initial();  
    9. GetArgments(argc, argv);  
    10. InitSockets();  
    11. /*创建TCP流套接字,在domain参数为PF_INET的SOCK_STREAM的
      套接口中,protocol参数为0意味着告诉内核选择IPPRPTP_TCP,
      这也意味着套接口将使用TCP/IP协议*/  
    12. listenfd = socket(PF_INET, SOCK_STREAM, 0);  
    13. /*如果创建套接字失败*/  
    14. if (listenfd == INVALID_SOCKET)  
    15. {  
    16. printf("Error: out of socket resources\n");  
    17. return 1;  
    18. }  
    19.  
    20. /*如果是IP地址*/  
    21. if (atoi(hostName))   
    22. {  
    23. /*将IP地址转换成32二进制表示法,返回32位二进制的网络字节序*/  
    24. u_long ip_addr = inet_addr(hostName);  
    25. /*根据IP地址找到与之匹配的主机名*/  
    26. ptrHost = gethostbyaddr((char*)&ip_addr,  
    27.                       sizeof(u_long), AF_INET);  
    28. }  
    29. /*如果是主机名*/  
    30. else  
    31. /*根据主机名获取一个指向hosten的指针,该结构中包含了该主机所有的IP地址*/  
    32. ptrHost = gethostbyname(hostName);  
    33.  
    34. /*如果解析失败*/  
    35. if (!ptrHost)  
    36. {  
    37. ErrorPrint("cannot resolve hostname");  
    38. return 1;  
    39. }  
    40.  
    41. /*设置服务器地址*/  
    42. /*设置地址族为PF_INET*/  
    43. serverAddr.sin_family = PF_INET;  
    44. /*将一个通配的Internet地址转换成无符号长整型的网络字节序数*/  
    45. serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    46. /*将端口号转换成无符号短整型的网络字节序数*/  
    47. serverAddr.sin_port = htons(port);  
    48.  
    49. /*将套接字与服务器地址绑定*/  
    50. err = bind(listenfd, (const struct sockaddr *) &serverAddr,  
    51.                sizeof(serverAddr));  
    52. /*如果绑定失败*/  
    53. if (err == INVALID_SOCKET)  
    54. {  
    55. ErrorPrint("Error: unable to bind socket\n");  
    56. return 1;  
    57. }  
    58.  
    59. /*开始侦听,设置等待连接的最大队列长度为SOMAXCONN,默认值为5个*/  
    60. err = listen(listenfd, SOMAXCONN);  
    61. /*如果侦听失败*/  
    62. if (err == INVALID_SOCKET)  
    63. {  
    64. ErrorPrint("Error: listen failed\n");  
    65. return 1;  
    66. }  
    67.  
    68. LoopControl(listenfd, 1);  
    69. printf("Server is down\n");  
    70. /*释放Winscoket初始化时占用的资源*/  
    71. WSACleanup();  
    72. return 0;  
    73. }  

(2) 客户端

① 预处理

预处理包括文件导入、头文件加载、定义常量、定义变量等操作。具体代码如下:

  1. /*导入库文件*/  
  2. #pragma comment(lib, "wsock32.lib")  
  3. /*加载头文件*/  
  4. #include <stdio.h
  5. #include <winsock2.h
  6.  
  7. /*自定义函数*/  
  8. int InitSockets(void);  
  9.  
  10. void GetArgument(int argc, char **argv);  
  11. void ErrorPrint(x);  
  12. void userHelp();  
  13.  
  14. /*定义全局变量*/  
  15. unsigned short port;  
  16. char *hostName;  


② 初始化模块

初始化模块无需对全局变量赋值,只须实现对Winsock的初始化,包括初始化套接字版本号和加载Winsock库。具体代码如下:

  1. /*初始化Winsock函数*/  
  2. int InitSockets(void)  
  3. {  
  4. WSADATA wsaData;  
  5. WORD sockVersion;  
  6. int err;  
  7.  
  8. /*设置Winsock版本号*/  
  9. sockVersion = MAKEWORD(2, 2);  
  10. /*初始化Winsock*/  
  11. err = WSAStartup(sockVersion, &wsaData);  
  12. /*如果初始化失败*/  
  13. if (err != 0)  
  14. {  
  15. printf("Error %d: Winsock not available\n", err);  
  16. return 1;  
  17. }  
  18. return 0;  
  19. }  

③ 功能控制模块

此模块提供了参数获取、错误输出和用户帮助等功能,上述功能分别通过如下函数来实现。

GetArgments:用于获取用户提供的选项值。

ErrorPrint:用于输出错误。

userHelp:用于输出帮助信息。

对应的实现代码如下:

    1. /*获取选项函数*/  
    2. void GetArgments(int argc, char **argv)  
    3. {  
    4. int i;  
    5. for(i=1; i<argc; i++)  
    6. {  
    7. /*参数的第一个字符若是“-”*/  
    8. if (argv[i][0] == ‘-‘)  
    9. {  
    10. /*转换成小写*/  
    11. switch (tolower(argv[i][1]))  
    12. {  
    13. /*若是端口号*/  
    14. case ‘p‘:   
    15. if (strlen(argv[i]) > 3)  
    16. port = atoi(&argv[i][3]);  
    17. break;  
    18. /*若是主机名*/  
    19. case ‘h‘:   
    20. hostName = &argv[i][3];  
    21. break;  
    22. /*其他情况*/  
    23. default:  
    24. userHelp();  
    25. break;  
    26. }  
    27. }  
    28. }  
    29. return;  
    30. }  
    31.  
    32. /*错误输出函数*/  
    33. void ErrorPrint(x)  
    34. {   
    35. printf("Error %d: %s\n", WSAGetLastError(), x);  
    36. }  
    37.  
    38. /*用户帮助函数*/  
    39. void userHelp()  
    40. {  
    41. printf("userHelp:  -h:str -p:int\n");  
    42. printf("           -h:str  The host name \n");  
    43. printf("           -p:int  The Port number to use\n");  
    44. ExitProcess(-1);  
    45. }  

④ 数据传输控制模块

客户端程序会把数据的传入传出部分放在主函数中执行,也就是说此处的数据传输功能是通过主函数实现的。主函数中包括套接字创建、绑定和释放,并实现对服务器连接、数据发送、数据接收等各个模块的调用。具体实现代码如下:

  1. /*主函数*/  
  2. int main(int argc, char **argv)  
  3. {  
  4. SOCKET clientfd;  
  5. int err;  
  6. struct sockaddr_in serverAddr;  
  7. struct hostent *ptrHost;  
  8. char response[4096];  
  9. char *msg = "HELLO SERVER";  
  10. GetArgments(argc, argv);  
  11. if (argc != 3)   
  12. {  
  13. userHelp();  
  14. return 1;  
  15. }  
  16. GetArgments(argc,argv);  
  17. InitSockets();  
  18. /*创建套接字*/  
  19. clientfd = socket(PF_INET, SOCK_STREAM, 0);  
  20. /*如果创建失败*/  
  21. if (clientfd == INVALID_SOCKET)  
  22. {  
  23. ErrorPrint("no more socket resources");  
  24. return 1;  
  25. }  
  26. /*根据IP地址解析主机名*/  
  27. if (atoi(hostName))  
  28. {  
  29. u_long ip_addr = inet_addr(hostName);  
  30. ptrHost = gethostbyaddr((char*)&ip_addr,  
  31.             sizeof(u_long), AF_INET);  
  32. }  
  33. /*根据主机名解析IP地址*/  
  34. else  
  35. ptrHost = gethostbyname(hostName);  
  36.  
  37. /*如果解析失败*/  
  38. if (!ptrHost)  
  39. {  
  40. ErrorPrint("cannot resolve hostname");  
  41. return 1;  
  42. }  
  43.  
  44. /*设置服务器端地址选项*/  
  45. serverAddr.sin_family = PF_INET;  
  46. memcpy((char*)&(serverAddr.sin_addr),  
  47.         ptrHost->h_addr, ptrHost->h_length);  
  48. serverAddr.sin_port = htons(port);  
  49.  
  50. /*连接服务器*/  
  51. err = connect(clientfd, (struct sockaddr *) &serverAddr,  
  52.              sizeof(serverAddr));  
  53. /*连接失败*/  
  54. if (err == INVALID_SOCKET)  
  55. {  
  56. ErrorPrint("cannot connect to server");  
  57. return 1;  
  58. }  
  59. /*连接成功后,输出信息*/  
  60. printf("You are connected to the server\n");  
  61. /*发送消息到服务器端*/  
  62. send(clientfd, msg, strlen(msg)+1, 0);  
  63. memset(response, 0, sizeof(response));  
  64. /*接收来自服务器端的消息*/  
  65. recv(clientfd, response, sizeof(response), 0);  
  66. printf("server says %s\n", response);  
  67. /*关闭套接字*/  
  68. closesocket(clientfd);  
  69. /*释放Winscoket初始化时占用的资源*/  
  70. WSACleanup();  
  71. return 0;  
  72. }  

 

到此为止,整个实例设计完毕,编译执行后的效果如图2-6所示。

 
图2-6  执行效果

2.1.2 小试牛刀--模拟实现Windows的TCP程序