【计算机网络】基于UDP的服务器端和客户端

🔥博客主页: 我要成为C++领域大神

🎥系列专栏【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】

❤️感谢大家点赞👍收藏⭐评论✍️

本博客致力于分享知识,欢迎大家共同学习和交流。

UDP客户端搭建过程
1、加载库
2、创建socket套接字
3、收发数据
4、关闭套接字
5、卸载库

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)

没有效验和检查,容易丢包,也可能会出现乱序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值