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. void GetArgments(int argc, char **argv);
  10. void ErrorPrint(x);
  11. void userHelp();
  12. int LoopControl(SOCKET listenfd, int isMultiTasking);
  13. void Service(LPVOID lpv);
  14. /*定义常量*/
  15. #define MAX_SER 10
  16. /*定义全局变量*/
  17. char *hostName;
  18. unsigned short maxService;
  19. 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. /*初始化Winsocket函数*/
  9. int InitSockets(void)
  10. {
  11. WSADATA wsaData;
  12. WORD sockVersion;
  13. int err;
  14. /*设置Winsock版本号*/
  15. sockVersion = MAKEWORD(2, 2);
  16. /*初始化Winsock*/
  17. err = WSAStartup(sockVersion, &wsaData);
  18. /*如果初始化失败*/
  19. if (err != 0)
  20. {
  21. printf("Error %d: Winsock not available\n", err);
  22. return 1;
  23. }
  24. return 0;
  25. }

③ 功能控制模块

此模块提供了参数获取、错误输出和用户帮助等功能,上述功能分别通过如下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. void ErrorPrint(x)
  37. {
  38. printf("Error %d: %s\n", WSAGetLastError(), x);
  39. }
  40. /*用户帮助函数*/
  41. void userHelp()
  42. {
  43. printf("userHelp:  -h:str -p:int -n:int\n");
  44. printf("           -h:str  The host name \n");
  45. printf("                   The default host is 127.0.0.1\n");
  46. printf("           -p:int  The Port number to use\n");
  47. printf("                   The default port is 9999\n");
  48. printf("           -n:int  The number of service,below MAX_SER \n");
  49. printf("                   The default number is 3\n");
  50. ExitProcess(-1);
  51. }

④ 循环控制模块

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

⑤ 服务模块

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

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

⑥ 主函数模块

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

    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. /*如果是IP地址*/
    20. if (atoi(hostName))
    21. {
    22. /*将IP地址转换成32二进制表示法,返回32位二进制的网络字节序*/
    23. u_long ip_addr = inet_addr(hostName);
    24. /*根据IP地址找到与之匹配的主机名*/
    25. ptrHost = gethostbyaddr((char*)&ip_addr,
    26. sizeof(u_long), AF_INET);
    27. }
    28. /*如果是主机名*/
    29. else
    30. /*根据主机名获取一个指向hosten的指针,该结构中包含了该主机所有的IP地址*/
    31. ptrHost = gethostbyname(hostName);
    32. /*如果解析失败*/
    33. if (!ptrHost)
    34. {
    35. ErrorPrint("cannot resolve hostname");
    36. return 1;
    37. }
    38. /*设置服务器地址*/
    39. /*设置地址族为PF_INET*/
    40. serverAddr.sin_family = PF_INET;
    41. /*将一个通配的Internet地址转换成无符号长整型的网络字节序数*/
    42. serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    43. /*将端口号转换成无符号短整型的网络字节序数*/
    44. serverAddr.sin_port = htons(port);
    45. /*将套接字与服务器地址绑定*/
    46. err = bind(listenfd, (const struct sockaddr *) &serverAddr,
    47. sizeof(serverAddr));
    48. /*如果绑定失败*/
    49. if (err == INVALID_SOCKET)
    50. {
    51. ErrorPrint("Error: unable to bind socket\n");
    52. return 1;
    53. }
    54. /*开始侦听,设置等待连接的最大队列长度为SOMAXCONN,默认值为5个*/
    55. err = listen(listenfd, SOMAXCONN);
    56. /*如果侦听失败*/
    57. if (err == INVALID_SOCKET)
    58. {
    59. ErrorPrint("Error: listen failed\n");
    60. return 1;
    61. }
    62. LoopControl(listenfd, 1);
    63. printf("Server is down\n");
    64. /*释放Winscoket初始化时占用的资源*/
    65. WSACleanup();
    66. return 0;
    67. }

(2) 客户端

① 预处理

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

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

② 初始化模块

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

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

③ 功能控制模块

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

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. void ErrorPrint(x)
    33. {
    34. printf("Error %d: %s\n", WSAGetLastError(), x);
    35. }
    36. /*用户帮助函数*/
    37. void userHelp()
    38. {
    39. printf("userHelp:  -h:str -p:int\n");
    40. printf("           -h:str  The host name \n");
    41. printf("           -p:int  The Port number to use\n");
    42. ExitProcess(-1);
    43. }

④ 数据传输控制模块

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

  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. if (!ptrHost)
  38. {
  39. ErrorPrint("cannot resolve hostname");
  40. return 1;
  41. }
  42. /*设置服务器端地址选项*/
  43. serverAddr.sin_family = PF_INET;
  44. memcpy((char*)&(serverAddr.sin_addr),
  45. ptrHost->h_addr, ptrHost->h_length);
  46. serverAddr.sin_port = htons(port);
  47. /*连接服务器*/
  48. err = connect(clientfd, (struct sockaddr *) &serverAddr,
  49. sizeof(serverAddr));
  50. /*连接失败*/
  51. if (err == INVALID_SOCKET)
  52. {
  53. ErrorPrint("cannot connect to server");
  54. return 1;
  55. }
  56. /*连接成功后,输出信息*/
  57. printf("You are connected to the server\n");
  58. /*发送消息到服务器端*/
  59. send(clientfd, msg, strlen(msg)+1, 0);
  60. memset(response, 0, sizeof(response));
  61. /*接收来自服务器端的消息*/
  62. recv(clientfd, response, sizeof(response), 0);
  63. printf("server says %s\n", response);
  64. /*关闭套接字*/
  65. closesocket(clientfd);
  66. /*释放Winscoket初始化时占用的资源*/
  67. WSACleanup();
  68. return 0;
  69. }

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

 
图2-6  执行效果
时间: 2024-08-25 17:43:22

2.1.2 小试牛刀--模拟实现Windows的TCP程序的相关文章

2.2.2 小试牛刀--模拟实现Windows的UDP程序

1. 规划分析 在具体编码之前,先进行项目规划分析.本项目即有广播的功能,又有多播的功能,能实现基本的广播和多播机制,主要包括如下功能: 提供广播机制. 能设定身份,即是广播消息发送者,也是接收者,默认是消息接收者. 能在默认的广播地址和端口号上发送广播消息,接收广播消息. 能够指定广播地址.端口号.发送(或接收)数量选项进行广播消息的发送和接收. 提供多播机制. 能指定身份,即是多播消息发送者,也是接收者,默认是消息接收者. 主机能加入一个指定多播组. 能以默认选项发送多播消息和接收多播消息.

用Wireshark抓包来揭开ftp client GG和ftp server MM的勾搭内容并用C代码来简要模拟实现Windows自带的ftp client

前面, 我们玩过http, 颇有点意思, 在本文中, 我们继续来玩ftp(file transfer protocol).   http和ftp都是建立在tcp之上的应用层协议, 无论他们怎么包装, 怎么装bigger, 最终还是基于tcp端到端传输的.本文主要分为两个部分: 一. 用Wireshark抓包来揭开ftp client GG和ftp server MM的勾搭内容.二.用C代码来简要模拟实现Windows自带的ftp client. 说明, 本文中的实验, 我用了两台电脑, 分别是p

Windows 10 UWP程序标题栏设置

原文:Windows 10 UWP程序标题栏设置 在Windows 10程序中,以前只能用于全屏方式的Metro程序现在可以运行在窗口模式下了,并且改了个新名字,叫Windows 通用程序(Universal Windows app),简称UWP程序.新的UWP程序虽然大体上还是和以前的Metro程序差不多的,但还是引入了一点新东西的,本文这里就介绍一下它的标题栏设置的几个特性. 隐藏标题栏: 将应用界面扩展至 Titlebar 区域 CoreApplication.GetCurrentView

Windows 8 应用程序前后台切换事件监听

在一些情况下,我们需要监听应用程序切换到后台或者从后台切换至前台的事件,从而进行相关处理操作.支付宝应用锁屏(IOS,Android平台)的处理中就需要监听此事件,在用户将应用切换至后台一段时间后再切换至前台的情况下就需要弹出锁屏页面. 下图给出Windows 应用商店应用的生命周期图,应用前后台切换就是在运行和挂起直接进行切换,关于生命周期的详细介绍可以参阅官方文档:http://msdn.microsoft.com/zh-cn/library/windows/apps/hh464925.as

API、Win32 SDK、Win32项目、MFC、Windows窗体应用程序的区别

[原]API.Win32 SDK.Win32项目.MFC.Windows窗体应用程序的区别 首先来看一下每一个术语的定义: API:Application Programming Interface.Windows操作系统提供给应用程序编程的接口, 简称 为API函数. Win32 SDK:SDK(Software Development Kit)中文是软件开发包.则Win32 SDK是Windows 32位平台下的软件开发包,包括了API函数.帮助文档.微软 提供的一些辅助开发工具. Win3

利用netsh工具配置Windows系统TCP/IP

在特定场合有时需要使用netsh工具配置Windows系统TCP/IP,下面简要说明常用配置命令. 1.设置固定IP地址.子网掩码: netsh interface ip set address name="本地连接"source=static addr=192.168.0.12 mask=255.255.255.0 2.设置默认网关: netsh interface ip set address name="本地连接"gateway=192.168.0.254 g

Unity3D开发Windows Store应用程序 注意事项

原地址:http://blog.csdn.net/jbjwpzyl3611421/article/details/12704491 针对最近在移植window store项目中遇到的问题,我整理了官方说明, 在此和各位开发者分享,避免在移植过程中走弯路!     Platform status 平台现状   Currently if you want to build a Windows Store apps player, you have to do it on Windows 8, thi

Windows自删除程序和DLL

Windows自删除程序和DLL 参照文章 http://blog.csdn.net/rxxi/article/details/741557 做了个自删除的程序SelfDelete.代码下载(我的FTP服务器,用户名密码=pub): ftp://pub:[email protected]/tarball/SelfDelete_vs2010.tar.gz 其中包含2个项目,一个是SelfDel32,这是个动态链接库项目,生成的 SelfDel32.dll就是 我们需要的.无论使用这个DLL的程序是

BEGINNING SHAREPOINT&#174; 2013 DEVELOPMENT 第5章节--Windows Azure概览 开发Windows Azure应用程序

BEGINNING SHAREPOINT? 2013 DEVELOPMENT 第5章节--Windows Azure概览 开发Windows Azure应用程序 Windows Azure不只是关于服务.