• Windows网络与通信程序设计实验一:基于TCP的C/S通信仿真


    Windows网络与通信程序设计实验一:基于TCP的C/S通信仿真

    1. 实验要求:

    1.1 实验目的介绍:

    模拟实现TCP协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。 客户端向服务器端发送“我是集美大学网络工程专业学生”,服务器回应“我也是集美大学网络工程专业学生”。

    1.2 实验相关提示:

    服务器端创建监听套接字,并为它关联一个本地地址(指定IP地址和端口号),然后进入监听状态准备接受客户的连接请求。为了接受客户端的连接请求,服务器端必须调用accept函数。
    客户端创建套接字后即可调用connect函数去试图连接服务器监听套接字。当服务器端的accept函数返回后,connect函数也返回。此时客户端使用socket函数创建的套接字,服务器端使用accept函数创建的套接字,双方实现通信。

    2. 实验环境准备:

    1. 想要在Windows操作系统上进行网络协议编程,我们需要首先是一个可以编译C++程序的环境,这个环境可以是VSCode,也可以是VS;我个人选择的集成开发环境是VS2019.

    考虑到版本的变迁问题,担心热爱学习的友友们找不到VS2019版本的下载地址,这里特地给出:
    VS2019
    在这里插入图片描述
    在这里插入图片描述

    1. 使用VS2019要能够进行Winsock编程我们首先要进行如下的操作:

    Windows下使用VS2019搭建Winsock编程环境

    其中最重要的环境有以下的这几步:

    • 我们是使用空项目来开始构建一个Winsock的编程项目:
      在这里插入图片描述
    • 由于我们是要在同一台主机上同时运行服务器端客户端,所以我们必然在同一个解决方案下得有两个工程项目,一个工程项目是服务器端的工程;另一个工程项目是客户端的工程,所以我们在新建工程的时候,不能选择把工程和解决方案放在同一个目录下。
      在这里插入图片描述
    • 接下来是准备对应的环境,首先我们要先将SDL检查给关闭,之所以关闭SDL检查的原因是,VS2019本身的编译等级太高了,在教材中的很多原始的函数在SDL检查打开的时候是无法正常使用的。
      在这里插入图片描述
    • 其次由于Winsock库的使用中,我们必须要使用到WS2_32.lib库,所以在项目中必须添加该库的链接:【每个工程都要!】
      在这里插入图片描述
      在这里插入图片描述
    • 由于每次写网络程序都必须编写代码载入和释放Winsock库,所以书本上给定了一个封装好的CInitSock类来管理Winsock库,代码如下:
    • 该代码以initsock.h的头文件的形式保存。
    // initsock.h文件
    #include 
    #pragma comment(lib, "WS2_32")  // 链接到 WS2_32.lib
    
    class CInitSock
    {
    public:
        /*CInitSock 的构造器*/
        CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
        {
            // 初始化WS2_32.dll
            WSADATA wsaData;
            WORD sockVersion = MAKEWORD(minorVer, majorVer);
            if (::WSAStartup(sockVersion, &wsaData) != 0)
            {
                exit(0);
            }
        }
    
        /*CInitSock 的析构器*/
        ~CInitSock()
        {
            ::WSACleanup();
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 所有的Winsock函数都是从WS2_32.DLL导出的,VS2019在默认情况下并没有链接到该库,如果想使用Winsock API就必须包含相应的库文件:
      #pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib
    • 以上代码在进行封装的时候,构造器使用的函数是WSAStartup()函数,该函数用于加载Winsock库,该函数需要传入两个参数,一个参数是WORD wVersionRequested这个参数用于指定想要加载的Winsock库的版本号,该结构体有两个元素,一个是minorVer次版本号,另一个是majorVer主版本号,在创建这样的结构体的时候需要用MAKEWORD函数来创建这样的一个结构体。
    • 当然,每一个对WSAStartup的调用都必须对应一个对WSACleanup的调用,所以我们需要在析构器中调用这个函数来实现对Winsock库的释放。

    3. 实验步骤和具体代码理解:

    • TCP通信过程的流程图:
      在这里插入图片描述

    3.1 以下是服务器端的代码:

    #include "initsock.h"
    #include 
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套接字
        SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sListen == INVALID_SOCKET)
        {
            cout << "Failed socket()" << endl;
            return 0;
        }
    
        // 填充sockaddr_in结构
        sockaddr_in sin;
        sin.sin_family = AF_INET;
        sin.sin_port = htons(4567);
        sin.sin_addr.S_un.S_addr = INADDR_ANY;
    
        // 绑定这个套接字到一个本地地址
        if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            cout << "Failed bind()" << endl;
            return 0;
        }
    
        // 进入监听模式
        if (::listen(sListen, 2) == SOCKET_ERROR)
        {
            cout << "Failed listen()" << endl;
            return 0;
        }
    
        // 循环接受客户的连接请求
        sockaddr_in remoteAddr;
        int nAddrLen = sizeof(remoteAddr);
        SOCKET sClient;
        char szText[] = "你好,客户端:我也是集美大学网络工程专业学生!";
        while (TRUE)
        {
            cout << "服务端已启动,正在监听!\n" << endl;
    
            // 接受一个新连接
            sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
            if (sClient == INVALID_SOCKET)
            {
                cout << "Failed accept()" << endl;
                continue;
            }
    
            cout << "与主机 " << inet_ntoa(remoteAddr.sin_addr) << "建立连接:" << endl;
    
            // 接收数据
            char buff[256];
            int nRecv = ::recv(sClient, buff, 256, 0);
            if (nRecv > 0)
            {
                buff[nRecv] = '\0';
                cout << "接收到数据:" << buff << endl;
            }
    
            // 向客户端发送数据
            ::send(sClient, szText, strlen(szText), 0);
            // 关闭同客户端的连接
            ::closesocket(sClient);
        }
    
        // 关闭监听套接字
        ::closesocket(sListen);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    3.2 以下是客户端的代码:

    #include "initsock.h"
    #include 
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套接字
        SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (s == INVALID_SOCKET)
        {
            cout << " Failed socket()" << endl;
            return 0;
        }
    
        // 也可以在这里调用bind函数绑定一个本地地址,否则系统将会自动安排
        // 填写远程地址信息
        sockaddr_in servAddr;
        servAddr.sin_family = AF_INET;
        servAddr.sin_port = htons(4567);
        // 填写服务器程序(TCPServer程序)所在机器的IP地址
        char serverAddr[] = "127.0.0.1";
        servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
    
        //与服务器建立连接
        if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
        {
            cout << " Failed connect()" << endl;
            return 0;
        }
    
        cout << "与服务器 " << serverAddr << "建立连接" << endl;
    
        //向服务器发送数据
        char szText[] = "你好,服务器:我是集美大学网络工程专业学生!";
        int slen = send(s, szText, 100, 0);
        if (slen > 0)
        {
            cout << "向服务器发送数据:" << szText << endl;
        }
    
        // 接收数据
        char buff[256];
        int nRecv = ::recv(s, buff, 256, 0);
        if (nRecv > 0)
        {
            buff[nRecv] = '\0';
            cout << "接收到数据:" << buff << endl;
        }
    
        // 关闭套接字
        ::closesocket(s);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    3.3 代码细节补充:

    3.3.1 套接字
    套接字的创建函数(socket()函数):
    SOCKET socket(
    	int af,	//用来指定套接字使用的地址格式,WinSock中只支持AF_INET
    	int type,	//用来指定套接字的类型
    	int protocol	//配合type参数使用,用来指定使用的协议类型。可以是IPPPROTO_TCP等
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5

    除了socket()函数之外,Winsock2还提供了WSASocket()函数来创建套接字,与socket()相比,它提供了更多的参数。

    套接字的分类:
    1. SOCK_STREAM:流式套接字,使用TCP提供有连接的可靠的传输。
    2. SOCK_DGRAM:数据报套接字,使用UDP提供无连接的不可靠的传输。
    3. SOCK_RAW:原始套接字,Winsock编程并不使用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部。
    补充说明:
    • 当type参数指定为SOCK_STREAM或者SOCK_DGRAM时,系统已经明确使用TCP和UDP来工作,所以protocol参数可以指定为0.
    • 函数执行失败会返回INVALID_SOCKET
    套接字的关闭:
    int closesocket(SOCKET s);	//函数唯一的参数就是要关闭的套接字的句柄
    
    • 1
    3.3.2 Winsock寻址
    最基本的sockaddr结构
    struct sockaddr
    {
    	u_short sa_family;
    	char sa_data[14];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    sockaddr_in结构
    struct sockaddr_in{
    	short	sin_family;	//地址家族(即指定地址格式),应为AF_INET
    	u_short	sin_port;	//端口号
    	struct	in_addr	sin_addr;	//IP地址
    	char	sin_zero[8];	//空字节,要设为0,主要是为了和struct sockaddr的长度一致
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    struct in_addr【存IP地址】
    struct in_addr {
    	union{
    		struct {u_char	s_b1,s_b2,s_b3,s_b4;}	S_un_b;	//以4个u_char来描述
    		struct {u_short s_w1,s_w2;}	S_un_w;	//以2个u_short来描述
    		u_long	S_addr;	//以1个u_long来描述
    	} S_un;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    两个重要的地址转换函数:
    unsigned long inet_addr(const char* cp); //将一个“aa.bb.cc.dd”(点分十进制)类型的IP地址字符串转化为32位的二进制数。
    char* inet_ntoa(struct in_addr in);	//将32位的二进制数转化为IP地址字符串。
    
    • 1
    • 2
    • inet_addr()返回的32位二进制数是用网络顺序存储【也成为大尾方式】的。
    • 如何区分大尾顺序小尾顺序
      • 0x12345678为例:
      • 大尾:按0x120x340x560x78的顺序来存。
      • 小尾:按0x780x560x340x12的顺序来存。
    四个重要的字节序转换函数:
    • 之所以要使用字节序转换函数的原因是,网络字节顺序和IntelCPU的字节顺序刚好相反所以需要字节序转换函数来进行处理。
    u_short htons(u_short hostshort) //将u_short类型的变量从主机字节顺序转化到TCP/IP的网络字节顺序
    u_long htonl(u_long hostlong) //将u_long类型的变量从主机字节顺序转化到TCP/IP的网络字节顺序
    u_short ntohs(u_short netshort) //将u_short类型的变量从转化TCP/IP的网络字节顺序到主机字节顺序
    u_long ntohl(u_long netlong) //将u_long类型的变量从转化TCP/IP的网络字节顺序到主机字节顺序
    
    • 1
    • 2
    • 3
    • 4
    • 这些字节序的转换函数将会在sockaddr_in的填充中使用:
        // 填充sockaddr_in结构
        sockaddr_in sin;
        sin.sin_family = AF_INET;
        sin.sin_port = htons(4567);
        sin.sin_addr.S_un.S_addr = INADDR_ANY;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 当应用程序不关心所使用的地址,我们就可以将Internet地址的值指定为INADDR_ANY,指定端口号为0.
    • 如果Internet地址为INADDR_ANY,系统会自动使用当前主机配置的所有IP地址,简化程序设计。
    • 如果端口号等于0,程序执行时系统会为这个应用程序分配唯一的端口号,其值在1024-5000之间。

    应用程序可以在bind之后,使用getsockname来知道它分配的地址。但是得注意,得等连接上之后才能看到。

        // 填写远程地址信息
        sockaddr_in servAddr;
        servAddr.sin_family = AF_INET;
        servAddr.sin_port = htons(4567);
        // 填写服务器程序(TCPServer程序)所在机器的IP地址
        char serverAddr[] = "127.0.0.1";
        servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    127.0.0.1是本地的回环地址,用于本地测试所使用的地址,可以视为是在本地的服务器所在的IP地址,本地的客户端可以通过绑定该IP地址来实现与本地的服务器之间的通信。

    3.3.3 TCP通信过程的重要函数
    bind函数
    int bind(
    	SOCKET s,	//套接字的句柄
    	const struct sockaddr* name, //要关联的本地地址
    	int namelen //地址的长度,一般涉及sizeof函数
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 往往我们填写的地址的用sockaddr_in的结构来填充的,所以往往涉及一个类型转换 (LPSOCKADDR)
    	 // 绑定这个套接字到一个本地地址
        if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            cout << "Failed bind()" << endl;
            return 0;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    listen函数
    int listen(
    	SOCKET s,	//套接字的句柄
    	int backlog	//监听队列中允许保持的尚未处理的最大连接数量
    );
    
    • 1
    • 2
    • 3
    • 4

    listen函数为到达的连接指定backlog
    listen函数仅应用在支持连接的套接字上,如SOCKET_STREAM。

        // 进入监听模式
        if (::listen(sListen, 2) == SOCKET_ERROR)
        {
            cout << "Failed listen()" << endl;
            return 0;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    accept函数
    SOCKET accept(
    	SOCKET s,	//套接字句柄
    	struct sockaddr* addr,	//一个指向sockaddr_in结构的指针,包含了要连接的服务器的地址信息
    	int* addrlen	//一个指向地址长度的指针 
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5

    该函数会在s中取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是专门用来处理实际连接中的通信的套接字。

        // 循环接受客户的连接请求
        sockaddr_in remoteAddr;
        int nAddrLen = sizeof(remoteAddr);
        SOCKET sClient;
        char szText[] = "你好,客户端:我也是集美大学网络工程专业学生!";
        while (TRUE)
        {
            cout << "服务端已启动,正在监听!\n" << endl;
    
            // 接受一个新连接
            sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
            if (sClient == INVALID_SOCKET)
            {
                cout << "Failed accept()" << endl;
                continue;
            }
    
            cout << "与主机 " << inet_ntoa(remoteAddr.sin_addr) << "建立连接:" << endl;
    
            // 接收数据
            char buff[256];
            int nRecv = ::recv(sClient, buff, 256, 0);
            if (nRecv > 0)
            {
                buff[nRecv] = '\0';
                cout << "接收到数据:" << buff << endl;
            }
    
            // 向客户端发送数据
            ::send(sClient, szText, strlen(szText), 0);
            // 关闭同客户端的连接
            ::closesocket(sClient);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    connect函数
    int connect(
    	SOCKET s,	//套接字句柄
    	const struct sockaddr FAR* name,	//一个指向sockaddr_in结构的指针,包含了要连接的服务器的地址信息
    	int namelen	//sockaddr_in结构的长度
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5

    s是客户端的套接字,name和namelen用于寻址正在监听的处在服务器端的套接字。

        // 填写远程地址信息
        sockaddr_in servAddr;
        servAddr.sin_family = AF_INET;
        servAddr.sin_port = htons(4567);
        // 填写服务器程序(TCPServer程序)所在机器的IP地址
        char serverAddr[] = "127.0.0.1";
        servAddr.sin_addr.S_un.S_addr = inet_addr(serverAddr);
    
        //与服务器建立连接
        if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
        {
            cout << " Failed connect()" << endl;
            return 0;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    send函数和recv函数
    int send(
    	SOCKET s,	//套接字的句柄
    	const cahr FAR* buf,	//要发送数据的缓冲区的地址
    	int len,	//缓冲区的长度
    	int flags	//指定了调用方式,通常设为0
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    int recv(
    	SOCKET s,	//套接字的句柄
    	cahr FAR* buf,	//要接收的数据的缓冲区的地址
    	int len,	//缓冲区的长度
    	int flags	//指定了调用方式,通常设为0
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4. 实验结果展示:

    1. 产生一个解决方案:
      在这里插入图片描述
    2. 打开两个终端:
      在这里插入图片描述
    3. 打开刚才生成的解决方案所在的文件目录下的Debug文件:
      可以看到其中生成了两个可执行的文件:
      在这里插入图片描述
    4. 先启动服务器,把Server.exe拖入命令行中,并随意指定一个端口号,这里用9190:
      在这里插入图片描述
    5. 然后用同样的方法打开客户端的文件,但是要额外指定设定好的服务器所在的主机IP和端口号。
      在这里插入图片描述
    • 成功向服务器发送数据,也成功接收到来自服务器发送的数据信息。
    1. 由于服务器是循环监听,所以处理完客户端的通信请求后,服务器端会依然保持监听的状态,而客户端则是处理完自己的事情后就将Socket消解了。
      在这里插入图片描述
  • 相关阅读:
    处理乱码的问题oracle字符集WE8MSWIN1252和WE8ISO8859P1
    为什么我设置了DHCP,无法给eth1分配IP地址啊
    C++前缀和算法的应用:统计得分小于K的子数组数目
    Alpha-Beta 剪枝
    一元多项式的乘法与加法运算
    5-13sqli暴力破解在sqli漏洞中的应用
    Typora收费了,于是乎我自己写了一个
    大学生必看!这些关乎你“钱途”的内容你一定要知道NISP一级证书
    C++ 设计模式:工厂模式
    Spring request工具类
  • 原文地址:https://blog.csdn.net/m0_54524462/article/details/127456199