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


    实例功能 使用Visual C++开发一个类似于Windows自带的TCP程序

    源码路径 光盘yuanma2TCP

    本实例的目的是,使用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 ", 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 ", WSAGetLastError(), x);  
    40. }  
    41.  
    42. /*用户帮助函数*/  
    43. void userHelp()  
    44. {  
    45. printf("userHelp:  -h:str -p:int -n:int ");  
    46. printf("           -h:str  The host name  ");  
    47. printf("                   The default host is 127.0.0.1 ");  
    48. printf("           -p:int  The Port number to use ");  
    49. printf("                   The default port is 9999 ");  
    50. printf("           -n:int  The number of service,below MAX_SER  ");  
    51. printf("                   The default number is 3 ");  
    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 ");  
    23. return 1;  
    24. }  
    25. /*接收成功*/  
    26. printf("Accepted connection from client at %s ",  
    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 ", 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 ", 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 ");  
      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 ");  
      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 ");  
      65. return 1;  
      66. }  
      67.  
      68. LoopControl(listenfd, 1);  
      69. printf("Server is down ");  
      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 ", 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 ", WSAGetLastError(), x);  
      36. }  
      37.  
      38. /*用户帮助函数*/  
      39. void userHelp()  
      40. {  
      41. printf("userHelp:  -h:str -p:int ");  
      42. printf("           -h:str  The host name  ");  
      43. printf("           -p:int  The Port number to use ");  
      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 ");  
    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 ", response);  
    67. /*关闭套接字*/  
    68. closesocket(clientfd);  
    69. /*释放Winscoket初始化时占用的资源*/  
    70. WSACleanup();  
    71. return 0;  
    72. }  

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

     
    图2-6  执行效果
  • 相关阅读:
    9.2模拟题解
    NOI1995 石子合并
    NOIP2012 借教室
    织梦内页读取栏目banner图
    mysql数据库版本引发的问题
    简单修改hosts文件加快打开网页速度
    详细剖析电脑hosts文件的作用和修改
    内部标签样式
    织梦让当前的子栏目拥有特殊的样式
    织梦获取单个顶级栏目名
  • 原文地址:https://www.cnblogs.com/For-her/p/3939393.html
Copyright © 2020-2023  润新知