• 【计算机网络】基于UDP的简单通讯(服务端)


    流程

    我们UDP通讯就像是在做小买卖,主要就是进行收发数据
    在这里插入图片描述

    实现UDP协议的服务端需要经过五步操作:

    1. 加载库(Ws2_32.lib)
    2. 创建套接字(socket())
    3. 绑定IP(bind())
    4. 收发数据(recvfrom()、sendto())
    5. 关闭套接字、卸载库(closesocket()、WSACleanup())

    代码实现

    加载库

    在加载库时我们使用一个WSAStartup接口函数,它的返回值是int类型,是用来看是否加载成功的,参数有两个,第一个是输入参数,为WORD类型,用来输入版本号,第二个是输出参数,为WSADATA结构体类型,输出参数一般都为指针类型,所以我们要创建三个变量。由于用到的函数和数据类型都是WinSock2.h库中的,所以我们要先加载头文件

    #include
    #include
    using namespace std;
    
    • 1
    • 2
    • 3

    加载库:

        int err = 0;
        WORD version = MAKEWORD(2, 2);
        WSADATA wsaData;
        err = WSAStartup(version, &wsaData);
        //判断返回值
        if (0 != err) {
            cout << "WSAStartup error" << endl;
            return 1;
        }
        //判断加载的版本是否是2.2版本
        if (2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion)) {
            cout << "WSAStartup version error" << endl;
            //卸载库
            WSACleanup();
            return 1;
        }else {
                cout << "WSAStartup success" << endl;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    创建套接字

    创建套接字我们使用socket()函数,它的返回值为SOCKET类型,如果返回INVALID_SOCKET那么创建失败,我们可以通过WSAGetLastError()来打印错误码

    socket()有三个参数,都为int类型,第一个参数af是address family的缩写,我们使用AF_INET(ipv4),第二个参数是type,我们使用Udp协议的类型SOCK_DGRAM,第三个参数是protocol,我们使用UDP协议的IPPROTO_UDP。

    	SOCKET sock = socket(AF_INET,SOCK_DGRAM, IPPROTO_UDP);
    	if (INVALID_SOCKET == sock) {
    		cout << "socket error:" << WSAGetLastError() << endl;
    		//卸载库
    		WSACleanup();
    		return 1;
    	}
    	else {
    		cout << "socket success" << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    绑定ip

    使用bind()函数,返回值为int类型,如果返回值为SOCK_ERROR那就说明绑定失败了,有三个输入参数,第一个参数为SOCKET,第二个参数为sockaddr*,他是一个结构体指针,第三个参数为指针长度

    因为结构体为输入参数,所以我们要为里面的参数赋值,它一共有两个参数,第一个是一个ushort类型,第二个是char数组,那么我们对char数组赋值时会特别麻烦,因为要按照一定的顺序进行赋值,所以这里还给了另一个和sockaddr一样大小的数组——sockaddr_in,这个数组就是将char数组分解成了好几个变量,我们只需要对这几个变量进行赋值就可以了。第一个变量是ip地址类型,我们用的ipv4类型,第二个是端口号,第三个是ip地址

    在定义端口号时,由于不同计算机可能存储方式不同,可能是大端存储也可能是小端存储,所以我们有一个规定——网络字节序,是TCP/IP中规定好的一种数据表示格式,可以保证数据在不同主机之间传输时能够被正确解释。用到一个函数htons(),再绑定IP地址时,因为我们是接收所有网卡收到的数据,所以我们对主机内任意网卡都进行绑定。

    	//是操作系统里面注册端口和ip地址,也就是说当前操作系统收到发给某个端口号和ip地址的数据,就是咱么程序要接收的
    	sockaddr_in addr;
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(456789);  //转换成网络字节序,也就是大端存储,本机是小端存储
    	addr.sin_addr.S_un.S_addr = INADDR_ANY;  //绑定所有网卡
    
    	err = bind(sock,(sockaddr*)&addr,sizeof(addr));
    	if (SOCKET_ERROR == err) {
    		cout << "bind error" << endl;
    		//关闭套接字
    		closesocket(sock);
    		//卸载库
    		WSACleanup();
    		return 1;
    	}
    	else {
    		cout << "bind success" << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    接收数据

    接收数据我们使用recvfrom()函数,它的返回值有三种,如果接收数据成功就返回接收到的字节的个数,等于0就证明连接失败了,如果等于SOCK_ERROR就是接收失败了

    函数的参数有六个,第一个为socket,意为使用哪个socket进行接收,第二个参数为char*,是一个输出参数,是用来接收数据的缓冲区,第三个参数为这个缓冲区的大小,第四个参数是一个标志位,用来决定当前的接收方式,我们在这里不做特殊设置,用默认的即可,下一个参数也是一个sockaddr *输出类型的参数,用来存放数据是从哪里来的,最后一个参数当然就是上一个参数的长度,但由于它属于是输出类型的参数,所以要变为指针类型

    	int nRecvNum = 0;
    	char recvBuf[1024] = "";
    	sockaddr_in addrClient;
    	int addrClientSize = sizeof(addrClient);
    	while (true) {
    		//4、接收数据
    		nRecvNum = recvfrom(sock, recvBuf,sizeof(recvBuf),0, (sockaddr*)&addrClient,&addrClientSize);
    		if (nRecvNum > 0) {
    			//接收成功,打印一下接收到的数据内容和发送端的ip地址
    			//"192.168.3.145"十进制四等分字符串类型ip地址
    			//ulong类型的ip地址:addrClient.sin_addr.S_un.S_addr
    			cout << "ip:" << inet_ntoa(addrClient.sin_addr) << " say: " << recvBuf << endl;
    			//从ulong转换成字符串类型ip:inet_ntoa(addrClient.sin_addr);
    			//从字符串类型转换成ulong类型的ip地址:inet_addr();
    		}
    		else {
    			//接收失败,打印失败日志,结束循环
    			cout << "recvfrom error" << WSAGetLastError() << endl;
    			break;
    		}
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    发送数据

    发送数据使用的是sendto()函数,他也需要卸载循环里,接在上面接收数据后面即可,比如我们发送一个“hahaha”,sendto函数返回值为int类型,如果等于SOCKET_ERROR,那么就是发送失败,它也有六个参数,和接收数据也十分相似,首先是发送用到的socket,然后是发送数据缓冲区和缓冲区大小,然后是标志位,最后是要发送的目标和它的大小,这些都为输入参数

    因为我们这里是服务端,所以谁给我们发我们就会给谁一个hahaha,所以目标我们就填接收数据时用来接收的sockaddr

        char msg[] = "hahaha";
        nSendNum = sendto(sock,msg,sizeof(msg),0,(sockaddr*)&addrClient, addrClientSize);
        if (SOCKET_ERROR == nSendNum) {
            //发送失败,打印失败日志,结束循环
            cout << "sendto error" << WSAGetLastError() << endl;
            break;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    关闭套接字、卸载库

    关闭套接字用到的函数为closesocket(),卸载库就是WSACleanup(),这两个函数在上面也都用到过了,这里就不在赘述了

    	closesocket(sock);
    	WSACleanup();
    
    • 1
    • 2

    现在代码部分我们都写好了,还有一些可能需要的操作,首先我们在尝试运行的时候会发现inet_ntoa会报错,我们可以到项目属性中去将SDL检查关闭即可

    在这里插入图片描述

    在这里插入图片描述

    再次运行,我们会发现出现了许多无法解析的外部符号的错误,那么是因为编译期找不到函数的实现,那么这些函数都是我们直接调用的,所以解决方法就是加载所需要的库

    #pragma comment(lib,"Ws2_32.lib")
    
    • 1

    那么到此为止,我们的UDP服务端就写好了,测试一下也没什么问题,接下来我们就要写客户端了

    在这里插入图片描述

  • 相关阅读:
    tkinter: 变量类别
    可视化开源自定义表单的特点表现在哪里?
    Day2讲课习题题解
    测试用例设计方法之等价类划分方法
    16.0、Java多线程——线程同步机制
    出海季,互联网出海锦囊之本地化
    ROS 摄像头标定-camera_calibration
    动态规划之最长公共子序列
    乐信—高级Java开发工程师二面(偏业务)
    一对一语音直播系统源码——如何解决音视频直播技术难点
  • 原文地址:https://blog.csdn.net/jia_03/article/details/133284311