🔥博客主页: 我要成为C++领域大神
🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】
❤️感谢大家点赞👍收藏⭐评论✍️
本博客致力于分享知识,欢迎大家共同学习和交流。
UDP服务器搭建流程
1、加载库
2、创建套接字
3、绑定IP
4、接收客户端发送的数据
5、返回客户端一些信息。
6、关闭套接字
7、卸载库
服务器端:
加载库
要用到Winsock2库里的WSAStartup函数,需要包含相应的头文件。
函数的返回值是一个整型,通过查看VS官方帮助文档,我们判断加载库是否成功。
#include<winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"Ws2_32.lib")表示链接Ws2_32.lib这个库。
//1、加载库
WORD version = MAKEWORD(2, 2);
WSADATA data = {};
int err=WSAStartup(version,&data);
if (0 != err) {
cout << "WSAStartup fail" << endl;
return 1;
}
else {
cout << "WSAStartup success" << endl;
}
if (2 != HIBYTE(data.wVersion) || 2 != LOBYTE(data.wVersion)) {
//虽然加载库成功了,但是版本加载不对
cout << "version error" << endl;
WSACleanup();
return 1;
}
else {
cout << "WSAStartup version success" << endl;
}
创建套接字
创建套接字需要使用socket函数。函数的3个参数,分别代表 地址族,套接字类型,通讯协议
根据返回值是否为异常,来判断套接字创建是否成功。
若创建失败,需要卸载库,并终止运行。
SOCKET WSAAPI socket(
//VS官方帮助文档中的[in]表示输入的参数
[in] int af,
[in] int type,
[in] int protocol
);
//2、创建套接字
SOCKET sock=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET != sock) {
cout << "socket success" << endl;
}
else {
cout << "socket fail" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
绑定IP
绑定IP地址使用的函数是bind函数。函数的返回值是一个整型,根据函数的返回值来判断IP绑定是否成功,失败返回SOCKET_ERROR,此时不仅要卸载库,还要关闭套接字。
bind函数有三个参数,套接字,sockaddr结构体类型的指针,结构体大小
int WSAAPI bind(
[in] SOCKET s,
[in] const sockaddr *name,
[in] int namelen
);
通过帮助文档查看sockaddr结构体,给出了sockaddr和sockaddr_in。为了方便设置端口号和IP,我们使用sockaddr_in,传入函数时,再强制类型转换将其地址转换为sockaddr*类型。
typedef struct sockaddr {
u_short sa_family;
char sa_data[14];
} SOCKADDR, *PSOCKADDR, *LPSOCKADDR;
typedef struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN;
sockaddr_in结构体的成员有一个in_addr结构体,在帮助文档中搜索查看,发现结构体里面是个联合体。用于传入IP。
struct in_addr {
union {
struct {
u_char s_b1;
u_char s_b2;
u_char s_b3;
u_char s_b4;
} S_un_b;
struct {
u_short s_w1;
u_short s_w2;
} S_un_w;
u_long S_addr;
} S_un;
};
//3、绑定IP和端口号
sockaddr_in sockServer;
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(9876); //设置端口号
sockServer.sin_addr.S_un.S_addr = ADDR_ANY; //绑定任意所有IP
err=bind(sock,(sockaddr*)&sockServer,sizeof(sockServer));
if (SOCKET_ERROR != err) {
cout << "bind success" << endl;
}
else
{
cout << "bind fail" << WSAGetLastError() << endl;
//绑定失败,不仅要卸载库,还要关闭套接字
closesocket(sock);
WSACleanup();
return 1;
}
char recvBuf[4096] = "";
char sendBuf[4096] = "";
sockaddr_in sockClient = {};
int sockClientSize=sizeof(sockClient);
这里需要注意的是,我们在设置端口号时,需要转一下网络字节序。如果不转换的话,PC之间的客户端和服务端进行数据传输,由于都是小端存储,所以不会有影响。但是如果对端不是PC,可能就会有问题。
sockServer.sin_port = htons(9876); //设置端口号,并转换网络字节序
接收消息
接收消息使用的是recvfrom函数。函数有6个参数,第一个参数是上面定义的套接字;第二个参数是输出参数,字符串类型,用来存放收到的消息;第三个参数是字符串长度;第四个参数是标志位(填0即可);第五个参数是输出参数,是sockaddr类型的结构体指针,表示消息的来源;第七个参数是消息来源结构体的长度的地址。
若能正常接收,则返回值>0;如果连接关闭,则返回0,如果接收异常返回错误信息。
在能正常收到消息的情况下,我们打印对端IP地址 和 用来存放消息的字符串即消息内容。这里我们使用 inet_ntoa函数将ulong型IP表示转换为十进制字符串四等分型IP表示。
int WSAAPI recvfrom(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags,
[out] sockaddr *from,
[in, out, optional] int *fromlen
);
//4、接收数据
recv=recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&sockClient, &sockClientSize);
if (recv > 0)
{
//接收成功,输出client的ip和发送的内容
//ip存放有两种方式,ulong型和十进制字符串四等分型
//inet_ntoa();
//inet_addr();
cout << "server recv: " ;
cout << "ip:" << inet_ntoa(sockClient.sin_addr) << " say:" << recvBuf << endl;
}
else if (recv == 0)
{
cout << "connection closed" << endl;
}
else {
cout << "recvfrom error" << WSAGetLastError() << endl;
break;
}
发送消息
发送消息使用的是sendto函数,函数有6个参数。第一个参数是上面定义的套接字,第二个参数是字符串,用来存放我们发送的内容,第三个参数是字符串的长度,第四个参数是标志位,填0即可;第五个参数是客户端的地址,第六个参数是客户端的大小
int WSAAPI sendto(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags,
[in] const sockaddr *to,
[in] int tolen
);
//5、发送数据
cout << "server send: ";
gets_s(sendBuf);
sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&sockClient, sockClientSize);
关闭套接字
关闭我们上面创建的套接字,使用的是closesocket函数,传入的参数是套接字
//6、关闭套接字
closesocket(sock);
卸载库
使用WSACleanup函数即可,没有参数。
//7、卸载库
WSACleanup();
完整代码
#include<iostream>
#include<winsock2.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {
//1、加载库
WORD version = MAKEWORD(2, 2);
WSADATA data = {};
int err=WSAStartup(version,&data);
if (0 != err) {
cout << "WSAStartup fail" << endl;
return 1;
}
else {
cout << "WSAStartup success" << endl;
}
if (2 != HIBYTE(data.wVersion) || 2 != LOBYTE(data.wVersion)) {
//虽然加载库成功了,但是版本加载不对
cout << "version error" << endl;
WSACleanup();
return 1;
}
else {
cout << "WSAStartup version success" << endl;
}
//2、创建套接字
SOCKET sock=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET != sock) {
cout << "socket success" << endl;
}
else {
cout << "socket fail" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
//3、绑定IP和端口号
sockaddr_in sockServer;
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(9876); //设置端口号
sockServer.sin_addr.S_un.S_addr = ADDR_ANY; //绑定任意所有IP
err=bind(sock,(sockaddr*)&sockServer,sizeof(sockServer));
if (SOCKET_ERROR != err) {
cout << "bind success" << endl;
}
else
{
cout << "bind fail" << WSAGetLastError() << endl;
//绑定失败,不仅要卸载库,还要关闭套接字
closesocket(sock);
WSACleanup();
return 1;
}
char recvBuf[4096] = "";
char sendBuf[4096] = "";
sockaddr_in sockClient = {};
int sockClientSize=sizeof(sockClient);
cout << "-----Server init over-----" << endl;
while (true)
{
//4、接收数据
cout << "server recv: " ;
int recv=recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&sockClient, &sockClientSize);
if (recv > 0)
{
//接收成功,输出client的ip和发送的内容
//ip存放有两种方式,ulong型和十进制字符串四等分型
//inet_ntoa();
cout << "ip:" << inet_ntoa(sockClient.sin_addr) << " say:" << recvBuf << endl;
}
else if (recv == 0)
{
cout << "connection closed" << endl;
}
else {
cout << "recvfrom error" << WSAGetLastError() << endl;
break;
}
//5、发送数据
cout << "server send: ";
gets_s(sendBuf);
sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&sockClient, sockClientSize);
}
//6、关闭套接字
closesocket(sock);
//7、卸载库
WSACleanup();
return 0;
}
运行截图
recvfrom阻塞函数,等待客户端发送消息。
客户端:
与服务端不同的是,客户端不需要进行绑定IP。
但是这里,我们发送数据的IP不能是所有IP了,而是指定为服务端的IP。
//3、配置IP和端口号
sockaddr_in sockServer;
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(9876); //设置端口号
sockServer.sin_addr.S_un.S_addr = inet_addr("192.168.3.108"); //客户端IP
其它部分与服务端基本相同。
客户端代码:
#include<iostream>
#include<winsock2.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {
//1、加载库
WORD version = MAKEWORD(2, 2);
WSADATA data = {};
int err = WSAStartup(version, &data);
if (0 != err) {
cout << "WSAStartup fail" << endl;
return 1;
}
else {
cout << "WSAStartup success" << endl;
}
if (2 != HIBYTE(data.wVersion) || 2 != LOBYTE(data.wVersion)) {
//虽然加载库成功了,但是版本加载不对
cout << "version error" << endl;
WSACleanup();
return 1;
}
else {
cout << "WSAStartup version success" << endl;
}
//2、创建套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET != sock) {
cout << "socket success" << endl;
}
else {
cout << "socket fail" << WSAGetLastError() << endl;
WSACleanup();
return 1;
}
//3、配置IP和端口号
//设置有限广播权限
BOOL bval = TRUE;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char*)&bval, sizeof(bval));
sockaddr_in sockServer;
sockServer.sin_family = AF_INET;
//sockServer.sin_port = htons(9876); //设置端口号
//sockServer.sin_addr.S_un.S_addr = inet_addr("192.168.1.186"); //客户端IP
sockServer.sin_port = htons(98765); //设置端口号
sockServer.sin_addr.S_un.S_addr = inet_addr("255.255.255.255"); //客户端IP
char recvBuf[4096] = "";
char sendBuf[4096] = "";
int recv = 0;
int send = 0;
cout << "-----Client init over-----" << endl;
while (true)
{
//5、发送数据
cout << "Client send: ";
gets_s(sendBuf);
send=sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&sockServer, sizeof(sockServer));
if (SOCKET_ERROR == send) {
cout << "sendto fail" << endl;
}
}
//6、关闭套接字
closesocket(sock);
//7、卸载库
WSACleanup();
return 0;
}
让我们来看看通信成功的结果:
UDP特点
面向非连接,可以接收任意方发来的数据,可以是1对1,也可以是1对多(广播和组播)
数据报文的通讯方式,数据包不可拆分
传输效率高(相对TCP)
没有效验和检查,容易丢包,也可能会出现乱序。