文章后面有代码,可以直接复制在Visual Studio 2022中运行(注意:必须是两个项目,客户端服务端各一个,连接在同一网络中,先运行服务端,并且客户端数据发送的目标IP要改为你服务端的IP)
目录
3.创建服务端sockaddr,套接字绑定服务端的IP和端口号(bind函数)
3.做收发的准备,创建服务端sockaddr(用服务端IP和端口号赋值)
前言
在了解完socket编程的一些基本理论知识后,很想把理论应用到实践,直接搜项目实战的教程,但是在看了几篇博客文章和一些B站的教程后,发现大部分都是不易上手的基于UDP/TCP的聊天系统,不太适合刚接触C++网络编程的同学,所以这里用C++实现简单的UDP通信,可以帮助大家更好的了解socket编程中的一些重要步骤。
提示:我们无法准确记住所有函数,在大部分情况下要通过帮助文档编程,本篇文章就是通过帮助文档带领大家一步一步实现基于UDP协议的简单通信。这个是我们要用到的微软帮助文档。
帮助文档
https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
一、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库,那么这个函数应该怎么用呢?这时就要用到帮助文档了。
开始写代码,函数要什么就写什么
返回值是错误码,就定义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库的版本号。
头文件和依赖库
编译器不认识这个函数,可是我也不知道要加什么头文件啊?那我们再看帮助文档,一直往下翻。
#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位是第二个参数。
2.创建套接字(socket函数)
还是先找帮助文档
三个int类型的[in]形参,应该传什么呢
参数1地址族
af这个形参说白了就是IPv4还是IPv6,咱们用IPv4
参数2协议类型
type,socket type描述连接如何工作,常常是stream(用于TCP连接)或dgram(用于UDP服务)。
参数3协议
最后一个参数,protocol,就是协议呗
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
返回值与检查(socket函数)
看返回值要用SOCKET类型接,针对错误再加个判断
如果未发生错误,套接字将返回引用新套接字的描述符。否则,将返回值 INVALID_SOCKET,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
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,怎么用呢,来看看帮助文档
参数1套接字和参数3结构体大小
三个参数,第一个SOCKET类型的肯定就是我们刚才创建的啊。第三个namelen,int类型的一个结构体大小,跟第二个参数有关。
参数2sockaddr指针
那么第二个参数就是关键。因为是[in]参数,还是个指向结构体的指针,我们要手动给它结构体中每一个部分赋值。帮助文档里搜sockaddr。
[in] name
指向要分配给绑定套接字的本地地址的 sockaddr 结构的指针。
这里我们要用第二个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。
接下来是个重点,IP的两种数据类型
IP的两种数据类型
1.十进制四等分的地址字符串类型 "10.10.10.10"
2.网络字节序类型(可以用ulong存)
inet_addr:是将一个IP地址字符串 转换为 32位大端网络字节序整数。
inet_ntoa:是将一个32位大端网络字节序整数 转换为 IP地址字符串。其实就是in_addr结构体 类型转字符串类型,结构体in_addr内部是一个共用体,其中S_addr是ulong类型
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_pton
和 inet_ntop
函数来替代 inet_addr
和 inet_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,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
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,来看看帮助文档
参数
前面写了这么多,传什么其实已经很明显了,第一个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,并且可以通过调用 WSAGetLastError 检索特定的错误代码。
这里打印一下收到的数据,并且把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,来看看帮助文档
参数
sendto的参数和recvfrom的都是一一对应的,很好写。但是你会发现参数全是[in],SendBuf我们自己写个输入就行,但是这个[in] const sockaddr *to呢?
这里就体现出为什么我们这个简单通信要服务端先接收数据了,前面说了,recvfrom的最后两个参数是客户端的sockaddr,它们是[out]参数,所以函数调用完,它们就有值了,存的就是客户端信息。
我们知道客户端的地址信息后,sendto的最后两个参数直接写里就行了。
返回值与检查(sendto)
如果没有发生错误,sendto 将返回发送的总字节数,该字节数可以小于 len 指示的数字。否则,将返回值 SOCKET_ERROR,并且可以通过调用 WSAGetLastError 来检索特定的错误代码。
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 防火墙
如果是一台电脑,右键任务栏的VS,再启动一个项目
先运行服务端,如果有这个错误,可以关闭SDL检查或加这个
#define _WINSOCK_DEPRECATED_NO_WARNINGS
一定要允许,取消的话就要到防火墙那里改了
再运行客户端,并发个数据
多来几次收发
如果是两台电脑,一定要确定它们连接的是同一网络,并且双方都关闭了防火墙
总结(完整代码)
服务端
#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();
}
标签:sockaddr,UDP,socket,C++,参数,addrUDPServer,addr,服务端,客户端
From: https://blog.csdn.net/m0_74203352/article/details/137405601