目录

socket编程C实现基于UDP协议的简单通信含详解

socket编程——C++实现基于UDP协议的简单通信(含详解)

文章后面有代码,可以直接复制在Visual Studio 2022中运行(注意:必须是两个项目,客户端服务端各一个,连接在同一网络中,先运行服务端,并且客户端数据发送的目标IP要改为你服务端的IP)

目录



前言

在了解完socket编程的一些基本理论知识后,很想把理论应用到实践,直接搜项目实战的教程,但是在看了几篇博客文章和一些B站的教程后,发现大部分都是不易上手的基于UDP/TCP的聊天系统,不太适合刚接触C++网络编程的同学,所以这里用C++实现简单的UDP通信,可以帮助大家更好的了解socket编程中的一些重要步骤。

提示:我们无法准确记住所有函数,在大部分情况下要通过帮助文档编程,本篇文章就是通过帮助文档带领大家一步一步实现基于UDP协议的简单通信。这个是我们要用到的微软帮助文档。

帮助文档


一、UDP通信框架

在VS中,如果你没关SDL检查,就在前面加这个

#define _WINSOCK_DEPRECATED_NO_WARNINGS

1.服务端

#include<iostream>
using namespace std;
int main() {
	//1.加载库
	
	//2.创建socket
	
	//3.创建服务端sockaddr,socket绑定服务端的IP和端口号

    //4.做收发的准备,创建客户端sockaddr(不用赋值)

    while (true)
    {
    	//5.接收数据

    	//6.发送数据

    }

    //7.关闭套接字

    //8.卸载库

}

2.客户端

#include<iostream>
using namespace std;
int main() {
//1.加载库

    //2.创建socket

    //3.做收发的准备,创建服务端sockaddr(用服务端IP和端口号赋值)
    while (true)
    {
    	//4.发送数据

    	//5.接收数据

    }

    //6.关闭套接字

    //7.卸载库

}

二、服务端实现

1.加载库(WSAStartup 函数)

WSAStartup

我们 socket 编程第一个要用到的函数就是 WSAStartup,嘶?这是干嘛的呢?见名知意,W 代表 Windows 操作系统,S 代表 socket(套接字),A 代表应用程序编程接口(API),函数用于初始化 winsock 库,那么这个函数应该怎么用呢?这时就要用到帮助文档了。

https://i-blog.csdnimg.cn/blog_migrate/eab4da617fcbbdb57741ac3d8374f8df.png

开始写代码,函数要什么就写什么

返回值是错误码,就定义 int 变量接。

wVersionRequested 是版本号,类型是 WORD,因为是[in]参数,需要我们调用者给值。lpWSAData 是指向结构体 WSADATA 的指针,因为是[out]参数,不用给值,调用完函数就有值了。

WORD wVersion = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(wVersion, &data);

MAKEWORD(a,b) 是一个宏,用于将两个字节大小的数合并成一个 WORD 类型的值。在这里,MAKEWORD(2, 2) 的作用是将高位字节设为 2,低位字节也设为 2,从而构造出一个 WORD 类型的数值,表示了 Winsock 库的版本号。

头文件和依赖库

编译器不认识这个函数,可是我也不知道要加什么头文件啊?那我们再看帮助文档,一直往下翻。

https://i-blog.csdnimg.cn/blog_migrate/53d69ba1d75a7e301ee53a42e67b55c9.png

#include<winsock2.h>
#pragma comment(lib, "Ws2_32.lib")

返回值与检查(WSAStartup 函数)

最后再来个判断

if (err != 0) {
cout << "加载库失败" << endl;
return 1;
}
else if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "库版本号错误" << endl;
WSACleanup();//卸载错误的库
return 1;
}
else {
cout << "加载库成功" << endl;
}

LOBYTE 和 HIBYTE 都是宏用来判断高位和低位的版本号,这个不用了解太深,MAKEWORD(2, 2) 将参数 2 和 2 组合成一个 16 位的无符号整数,高 8 位是第一个参数,低 8 位是第二个参数。

https://i-blog.csdnimg.cn/blog_migrate/ede50cd41ebb02ed2b58a3f507fbc985.png

2.创建套接字(socket 函数)

还是先找帮助文档

https://i-blog.csdnimg.cn/blog_migrate/41f8bdc5d0b0d888b29536e656bb69ca.png

三个 int 类型的[in]形参,应该传什么呢

参数 1 地址族

af 这个形参说白了就是 IPv4 还是 IPv6,咱们用 IPv4

https://i-blog.csdnimg.cn/blog_migrate/d258e60d1de29c080fa47bdac733702b.png

参数 2 协议类型

type,socket type 描述连接如何工作,常常是 stream(用于 TCP 连接)或 dgram(用于 UDP 服务)。

https://i-blog.csdnimg.cn/blog_migrate/bb7eaeb3e72d7c3c7df18141819f3919.png

参数 3 协议

最后一个参数,protocol,就是协议呗

https://i-blog.csdnimg.cn/blog_migrate/4ca313499204419286e8ba418da119b0.png

SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

返回值与检查(socket 函数)

看返回值要用 SOCKET 类型接,针对错误再加个判断

如果未发生错误, 套接字 将返回引用新套接字的描述符。否则,将返回值 INVALID_SOCKET,并且可以通过调用 来检索特定的错误代码。

SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//阅读帮助文档了解具体含义,三个参数均为宏定义或枚举类型
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
//如果日志中出现错误,上方工具栏->错误查找->输入编号
WSACleanup();//卸载库
return 1;
}
else {
cout << "创建套接字成功" << endl;
}

3.创建服务端 sockaddr,套接字绑定服务端的 IP 和端口号(bind 函数)

绑定的函数是 bind,怎么用呢,来看看帮助文档

https://i-blog.csdnimg.cn/blog_migrate/7f9455e1df23b249d30a3d9287fc8168.png

参数 1 套接字和参数 3 结构体大小

三个参数,第一个 SOCKET 类型的肯定就是我们刚才创建的啊。第三个 namelen,int 类型的一个结构体大小,跟第二个参数有关。

参数 2sockaddr 指针

那么第二个参数就是关键。因为是[in]参数,还是个指向结构体的指针,我们要手动给它结构体中每一个部分赋值。帮助文档里搜 sockaddr。

[in] name

指向要分配给绑定套接字的本地地址的 结构的指针。

https://i-blog.csdnimg.cn/blog_migrate/c9e71d27208fcff5d9b5b12dfc367aae.png

这里我们要用第二个 sockaddr_in,因为它详细分成了第一个指定地址族(Address Family),第二个存储端口号,第三个 in_addr 类型的结构体成员,用于存储 IP 地址,第四个用于填充的空间,以保证 sockaddr_in 的大小与 sockaddr 结构体相同。

sockaddr_in addrUDPServer;
addrUDPServer.sin_family = ;
addrUDPServer.sin_port = ;
addrUDPServer.sin_addr.S_un.S_addr = ;
err = bind(sock, (sockaddr*)&addrUDPServer, sizeof(addrUDPServer));

写好后,开始赋值

1. addrUDPServer.sin_family

addrUDPServer.sin_family 和前面的 af 一样就是问你 IPv4 还是 IPv6,连赋值的宏都是一个 AF_INET

在网络编程中,struct sockaddr_in 是用于表示 IPv4 地址的数据结构。其中,sin_family 成员用于指定地址族(Address Family)。

addrUDPServer.sin_family = AF_INET;
2. addrUDPServer.sin_port

addrUDPServer.sin_port 就是端口号的意思,这个端口要自己设置,我的建议自己查查你的电脑都占用了哪些端口号,找一个空闲的,方法:命令行指令 netstat -ano

这里有一个重点,要用 htons 函数转换成网络字节序

绑定端口号,接入网络的设备有很多种,有的用小端有的用大端,网络上统一规定用网络字节序,

绑定端口号表示当先应用程序可以接收发给这个端口号的数据,网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用 big-endian(大端)存储方式。一般操作系统采用的都是小端模式,而通讯协议采用大端模式。

我这里就随便写一个了 htons(12345),应该没有哪个程序占用【狗头】

addrUDPServer.sin_port = htons(12345);
3. addrUDPServer.sin_addr

接下来更是重量级,要绑定网卡了,一看类型又是结构体,服了,谁知道内部结构是啥,只能上帮助文档里搜了,结果进去一看里面是一个 union 共用体,因为它的所有成员共享同一块内存,也就是说我们只要给一个赋值就行,我一眼就盯到最简单的 u_long 类型的 S_addr。

https://i-blog.csdnimg.cn/blog_migrate/2a948b451c2cf5d2571567456e5a3db1.png

接下来是个重点,IP 的两种数据类型

IP 的两种数据类型

1.十进制四等分的地址字符串类型   “10.10.10.10”

2.网络字节序类型(可以用 ulong 存)

inet_addr:是将一个 IP 地址字符串   转换为   32 位大端网络字节序整数。

inet_ntoa:是将一个 32 位大端网络字节序整数   转换为   IP 地址字符串。其实就是 in_addr 结构体       类型转字符串类型,结构体 in_addr 内部是一个共用体,其中 S_addr 是 ulong 类型

https://i-blog.csdnimg.cn/blog_migrate/88c3ba5dc605c1587bc84f351ae83d2d.png

https://i-blog.csdnimg.cn/blog_migrate/80c53d16b2dd7a5a11bbceb27339267b.png

addrUDPServer.sin_addr.S_un.S_addr = INADDR_ANY;//用这宏定义绑定所有网卡
addrUDPServer.sin_addr.S_un.S_addr=inet_addr("10.10.10.10");
//单独绑定,只能得到这个 IP 接收到的消息

其实在 C++ 中,推荐使用 inet_ptoninet_ntop 函数来替代 inet_addrinet_ntoa 函数。这两个函数在处理 IPv4 和 IPv6 地址时更加灵活和安全,而且能够支持更广泛的地址类型。

完成赋值

sockaddr_in addrUDPServer;
addrUDPServer.sin_family = AF_INET;
addrUDPServer.sin_port = htons(12345);
addrUDPServer.sin_addr.S_un.S_addr = INADDR_ANY;
err = bind(sock, (sockaddr*)&addrUDPServer, sizeof(addrUDPServer));

返回值与检查(bind 函数)

如果未发生错误, 则 bind 返回零。否则,它将返回 SOCKET_ERROR,并且可以通过调用 来检索特定的错误代码。

if (err == SOCKET_ERROR) {
cout << "绑定失败:" << WSAGetLastError() << endl;
closesocket(sock);//关闭套接字
WSACleanup();//卸载库
return 1;
}
else {
cout << "绑定成功" << endl;
}

4.做收发的准备,创建客户端 sockaddr(不用赋值)

int nRecvNum = 0;//储存接收到的数据的大小
int nSendNum = 0;//储存要发送数据的大小
char recvBuf[1024] = "";//储存接收的数据
char SendBuf[1024] = "";//储存发送的数据
sockaddr_in addrUDPClient;
int addrUDPClientSize = sizeof(addrUDPClient);

我们创建服务端的 sockaddr 是用来绑定 IP 和端口号的,同样我们服务端收到客户端的数据后,也要知道客户端的 IP 和端口号,所以要创建客户端的 sockaddr 好用来存值

5.接收数据(recvfrom 函数)

这里用到的函数是 recvfrom,来看看帮助文档 https://i-blog.csdnimg.cn/blog_migrate/4447404a4811f3b996c94201bdc29d31.png

https://i-blog.csdnimg.cn/blog_migrate/7601cc1bb471d05628a85bd33e6387f9.png

参数

前面写了这么多,传什么其实已经很明显了,第一个 SOCKET 传的就是咱们创建的 sock。第二个[out]参数,就是服务端收到的数据要存在一个地方,在准备工作中已经写好了 recvBuf。第三个是大小 sizeof(recvBuf)。

第四个参数 flags 参数用于指定接收操作的行为。具体来说,它是一个位掩码,可以用于设置一些特定的选项。在 Windows Socket API 中,常见的 recvfrom 函数使用的 flags 参数通常为 0,表示没有特殊的选项。

这里写的是服务端的接收,recvfrom 的最后两个参数一定是对方的,也就是客户端,传的参是没赋值的客户端 sockaddr

nRecvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&addrUDPClient, &addrUDPClientSize);

返回值与检查(recvfrom 函数)

如果没有发生错误, recvfrom 将返回接收到的字节数。如果连接已正常关闭,则返回值为零。否则,将返回值 SOCKET_ERROR,并且可以通过调用 检索特定的错误代码。

这里打印一下收到的数据,并且把 IP 也用 inet_ntoa 函数转换成咱们能看懂的字符串。

nRecvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&addrUDPClient, &addrUDPClientSize);
//注意 recvfrom 的最后一个参数是 int*类型,接收数据时,由于接收到的数据长度是动态变化的,因此使用指针传递长度信息能够更好地应对这种情况(动态更新)。
if (nRecvNum > 0) {
//代表数据接收成功,打印接收到的数据
cout << "Client " << inet_ntoa(addrUDPClient.sin_addr) << ":" << recvBuf << endl;
}
else if (nRecvNum == 0) {
//连接已正常关闭,返回值为 0
cout << "连接已断开" << endl;
break;
}
else {
cout << "接收数据失败:" << WSAGetLastError() << endl;
break;
}

6.发送数据(sendto)

这里用到的函数是 sendto,来看看帮助文档

https://i-blog.csdnimg.cn/blog_migrate/a3670c82beb6b6aad1320479e2b0faca.png

参数

sendto 的参数和 recvfrom 的都是一一对应的,很好写。但是你会发现参数全是[in],SendBuf 我们自己写个输入就行,但是这个[in] const sockaddr *to 呢?

这里就体现出为什么我们这个简单通信要服务端先接收数据了,前面说了,recvfrom 的最后两个参数是客户端的 sockaddr,它们是[out]参数,所以函数调用完,它们就有值了,存的就是客户端信息。

我们知道客户端的地址信息后,sendto 的最后两个参数直接写里就行了。

返回值与检查(sendto)

如果没有发生错误, sendto 将返回发送的总字节数,该字节数可以小于 len 指示的数字。否则,将返回值 SOCKET_ERROR,并且可以通过调用 来检索特定的错误代码。

cin >> SendBuf;
nSendNum = sendto(sock, SendBuf, sizeof(SendBuf), 0, (sockaddr*)&addrUDPClient, addrUDPClientSize);
//注意 sendto 的最后一个参数是 int 类型,在发送数据时,通常已经知道目标地址结构体的长度,所以用 int 就行
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}

7.关闭套接字

closesocket(sock);

8.卸载库

WSACleanup();

三、客户端实现

服务端写完再客户端就轻松多了,只需要单独理解一下第三步和第五步就可以。

1.加载库

 //1.加载库
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(wVersionRequested, &data);
if (err != 0) {
cout << "加载库失败" << endl;
return 1;
}
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "库版本号错误" << endl;
WSACleanup();//卸载错误的库
return 1;
}
else {
cout << "加载库成功" << endl;
}

2.创建 socket

 //2.创建套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();//卸载库
return 1;
}
else {
cout << "创建套接字成功" << endl;
}

3.做收发的准备,创建服务端 sockaddr(用服务端 IP 和端口号赋值)

因为是客户端先发数据,所以要知道目标 IP,注意这里并不是绑定(bind),只是创建服务端的 sockaddr 并用服务端的地址信息赋值,用来存服务端的地址信息,咱们这个简单通信不需要给客户端绑定哈(偷懒狗头)。

运行服务端的电脑要完成以下步骤

1.命令行指令:ipconfig

2.找到无线局域网适配器 WLAN,复制 IPv4 地址

3.客户端

addrUDPServer.sin_addr.S_un.S_addr = inet_addr("复制的地址");

这个客户端为什么不需要绑定 IP 和端口号

绑定 IP 和端口号是在告诉操作系统可以接收发给这个 IP 和端口号的数据,

因为客户端先发送数据,操作系统发现没有绑定,会自动分配任意网卡+

空闲端口号,如果不想要操作系统分配的,也可以自己绑定。

 int nRecvNum = 0;
int nSendNum = 0;
char recvBuf[1024] = "";
char SendBuf[1024] = "";
sockaddr_in addrUDPServer;
addrUDPServer.sin_family = AF_INET;
addrUDPServer.sin_port= htons(12345);
addrUDPServer.sin_addr.S_un.S_addr = inet_addr("10.10.10.10");
//服务端 IP,这个端口号要与服务端绑定的一致
int addrUDPServerSize = sizeof(addrUDPServer);

4.发送数据

服务端的 sockaddr 有值,我们发送数据就直接用就行

cin>>SendBuf;
nSendNum = sendto(sock, SendBuf, sizeof(SendBuf), 0, (sockaddr*)&addrUDPServer, addrUDPServerSize);
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}

5.接收数据

服务端先接收数据会得到什么,得到数据和客户端的地址信息,服务端通过 recvfrom 接收到数据并得到客户端的地址信息后,才能用 sendto 发给客户端数据。

但是客户端有直接带值的服务端 sockaddr,客户端知道发给谁,还知道谁给我发。所以 recvfrom 的最后两个参数没啥用了,传 nullptr 就行。(强调:实现简单通信,如果是聊天系统的项目实战可别这么写)

nRecvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, nullptr, nullptr);
if (nRecvNum > 0) {
//代表数据接收成功,打印接收到的数据
cout << "Server " << inet_ntoa(addrUDPServer.sin_addr) << ":" << recvBuf << endl;
}
else if (nRecvNum == 0) {
//连接已正常关闭,返回值为 0
cout << "连接已断开" << endl;
break;
}
else {
cout << "接收数据失败:" << WSAGetLastError() << endl;
break;
}

6.关闭套接字

closesocket(sock);

7.卸载库

WSACleanup();

四、执行过程

可以接上手机热点测试,确保客户端的 sockaddr_in  addrUDPServer 里存的信息正确,端口号与服务端绑定的一致,IP 是服务端的 IP。这个代码只能客户端发一句,服务端发一句,不能一端连发。

关闭防火墙路径:控制面板\系统和安全\Windows Defender 防火墙

https://i-blog.csdnimg.cn/blog_migrate/778aec6a088d314a4bda3c502ef08b4d.png

如果是一台电脑,右键任务栏的 VS,再启动一个项目

https://i-blog.csdnimg.cn/blog_migrate/77a5e886c9365a255b7cd82a785150e9.png

先运行服务端,如果有这个错误,可以关闭 SDL 检查或加这个

#define _WINSOCK_DEPRECATED_NO_WARNINGS

https://i-blog.csdnimg.cn/blog_migrate/7be2be2582dd6bf6baf4e2061171ed7b.png

一定要允许,取消的话就要到防火墙那里改了

https://i-blog.csdnimg.cn/blog_migrate/d140e0ec7a903dcb4ed3a6b45817db36.png

再运行客户端,并发个数据

https://i-blog.csdnimg.cn/blog_migrate/ab30ee44b2f0377b0285eaa2a281dbef.png

多来几次收发

https://i-blog.csdnimg.cn/blog_migrate/93dda2f86d8f8ff830e7217167781669.png

如果是两台电脑,一定要确定它们连接的是同一网络,并且双方都关闭了防火墙

https://i-blog.csdnimg.cn/blog_migrate/fc62cbc0cb9209e151adace600038d3e.png

https://i-blog.csdnimg.cn/blog_migrate/e0bcf9986d7bf84f25a5d5466cdd9cea.png


总结(完整代码)

服务端

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<winsock2.h>
#pragma comment(lib, "Ws2_32.lib")
using namespace std;
int main() {
//1.加载库
WORD wVersion = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(wVersion, &data);
if (err != 0) {
cout << "加载库失败" << endl;
return 1;
}
else if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "库版本号错误" << endl;
WSACleanup();//卸载错误的库
return 1;
}
else {
cout << "加载库成功" << endl;
}
//2.创建 socket
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//阅读帮助文档了解具体含义,三个参数均为宏定义或枚举类型
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
//如果日志中出现错误,上方工具栏->错误查找->输入编号
WSACleanup();//卸载库
return 1;
}
else {
cout << "创建套接字成功" << endl;
}
//3.创建服务端 sockaddr,socket 绑定服务端的 IP 和端口号
sockaddr_in addrUDPServer;
addrUDPServer.sin_family = AF_INET;
addrUDPServer.sin_port = htons(12345);
addrUDPServer.sin_addr.S_un.S_addr = INADDR_ANY;//用这宏定义绑定所有网卡
err = bind(sock, (sockaddr*)&addrUDPServer, sizeof(addrUDPServer));
if (err == SOCKET_ERROR) {
cout << "绑定失败:" << WSAGetLastError() << endl;
closesocket(sock);//关闭套接字
WSACleanup();//卸载库
return 1;
}
else {
cout << "绑定成功" << endl;
}
//4.做收发的准备,创建客户端 sockaddr(不用赋值)
int nRecvNum = 0;//储存接收到的数据的大小
int nSendNum = 0;//储存要发送数据的大小
char recvBuf[1024] = "";//储存接收的数据
char SendBuf[1024] = "";//储存发送的数据
sockaddr_in addrUDPClient;
int addrUDPClientSize = sizeof(addrUDPClient);
while (true)
{
//5.接收数据
nRecvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&addrUDPClient, &addrUDPClientSize);
//注意 recvfrom 的最后一个参数是 int*类型,接收数据时,由于接收到的数据长度是动态变化的,因此使用指针传递长度信息能够更好地应对这种情况(动态更新)。
if (nRecvNum > 0) {
//代表数据接收成功,打印接收到的数据
cout << "Client " << inet_ntoa(addrUDPClient.sin_addr) << ":" << recvBuf << endl;
}
else if (nRecvNum == 0) {
//连接已正常关闭,返回值为 0
cout << "连接已断开" << endl;
break;
}
else {
cout << "接收数据失败:" << WSAGetLastError() << endl;
break;
}
//6.发送数据
cin >> SendBuf;
nSendNum = sendto(sock, SendBuf, sizeof(SendBuf), 0, (sockaddr*)&addrUDPClient, addrUDPClientSize);
//注意 sendto 的最后一个参数是 int 类型,在发送数据时,通常已经知道目标地址结构体的长度,所以用 int 就行
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}
}

    //7.关闭套接字
    closesocket(sock);
    //8.卸载库
    WSACleanup();

}

客户端

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<winsock2.h>
#pragma comment(lib, "Ws2_32.lib")
using namespace std;
int main() {
//1.加载库
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA data;
int err = WSAStartup(wVersionRequested, &data);
if (err != 0) {
cout << "加载库失败" << endl;
return 1;
}
if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) {
cout << "库版本号错误" << endl;
WSACleanup();//卸载错误的库
return 1;
}
else {
cout << "加载库成功" << endl;
}
//2.创建套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET) {
cout << "创建套接字失败:" << WSAGetLastError() << endl;
WSACleanup();//卸载库
return 1;
}
else {
cout << "创建套接字成功" << endl;
}
//3.做收发的准备,创建服务端 sockaddr(用服务端 IP 和端口号赋值)
int nRecvNum = 0;
int nSendNum = 0;
char recvBuf[1024] = "";
char SendBuf[1024] = "";
sockaddr_in addrUDPServer;
addrUDPServer.sin_family = AF_INET;
addrUDPServer.sin_port = htons(12345);
addrUDPServer.sin_addr.S_un.S_addr = inet_addr("服务端 IP");
//服务端 IP,这个端口号要与服务端绑定的一致
int addrUDPServerSize = sizeof(addrUDPServer);
while (true)
{
//4.发送数据
cin >> SendBuf;
nSendNum = sendto(sock, SendBuf, sizeof(SendBuf), 0, (sockaddr*)&addrUDPServer, addrUDPServerSize);
if (nSendNum == SOCKET_ERROR) {
cout << "发送数据失败:" << WSAGetLastError() << endl;
break;
}
//5.接收数据
nRecvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, nullptr, nullptr);
if (nRecvNum > 0) {
//代表数据接收成功,打印接收到的数据
cout << "Server " << inet_ntoa(addrUDPServer.sin_addr) << ":" << recvBuf << endl;
}
else if (nRecvNum == 0) {
//连接已正常关闭,返回值为 0
cout << "连接已断开" << endl;
break;
}
else {
cout << "接收数据失败:" << WSAGetLastError() << endl;
break;
}
}
//6.关闭套接字
closesocket(sock);
//7.卸载库
WSACleanup();
}