【网络】套接字编程——UDP通信

news/2024/11/5 5:59:51 标签: 服务器, php, 运维, 网络, 网络协议, udp

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:UDP网络服务器简单模拟实现。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:网络

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

​​

 一、前言

前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!

 二主体

学习【网络】套接字编程——UDP通信咱们按照下面的图解:

2.1 概述

        在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质的差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。
        以下给出了典型的UDP客户/服务器的函数调用。客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似的,服务器不接受来自客户端的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将接受与所接受的数据报一道返回客户的协议地址,因此服务器可以把响应的发送给正确的客户。

2.2 Udp Server(服务端)

接下来接下来实现一批基于 UDP 协议的网络程序,本节只介绍基于IPv4的socket网络编程。

2.2.1 核心功能

说明:

分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于 echo 指令,该程序的核心在于 使用 socket 套接字接口,以 UDP 协议的方式实现简单网络通信。

2.2.2 核心结构

程序由server.hpp   server.cc   client.hpp   client.cc 组成,大体框架如下:

创建 server.hpp 服务器头文件:

#pragma once
 
#include <iostream>
 
namespace nt_server
{
    class UdpServer
    {
    public:
        // 构造
        UdpServer()
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {}
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        // 字段
    };
}

创建 server.cc 服务器源文件:

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer());//使用智能指针创建了一个UdeServer对象
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}

创建 client.hpp 客户端头文件:

#pragma once
 
#include <iostream>
 
namespace nt_client
{
    class UdpClient
    {
    public:
        // 构造
        UdpClient() 
        {} 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {}
 
        // 启动客户端
        void StartClient() 
        {}
 
    private:
        // 字段
    };
}

创建 client.cc 客户端源文件:

#include <memory>
#include "client.hpp"
 
using namespace std;
using namespace nt_client;
 
int main()
{
  unique_ptr<UdpClient> usvr(new UdpClient());
 
  // 初始化客户端
  usvr->InitClient();
  
  // 启动客户端
  usvr->StartClient();
 
  return 0;
}

创建 Makefile 文件:

.PHONY:all
all:server client
 
server:server.cc
	g++ -o $@ $^ -std=c++11
 
	
client:client.cc
	g++ -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
	rm -rf server client

2.2.3 Udp Server 端代码


2.2.3.1 socket - 创建套接字

语法:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

解释说明:

  • domain:协议域(协议族)。该参数决定了 socket 的地址类型。在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 地址(32位的)与端口号(16位)的组合,AF_UNIX 决定了要用一个绝对路径名作为地址,AF_INET6(IPv6)。
  • type:指定了 socket 的类型,如 SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)等等。
  • protocol:指定协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)、IPPROTO_SCTP(STCP 传输协议)、IPPROTO_TIPC(TIPC 传输协议)。
  • 返回值:一个文件描述符,创建套接字的本质其实就是打开一个文件。

功能说明:

  • socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。 
  • 好了socket函数学完了,接下来在 server.hpp 的 InitServer() 函数中创建套接字,并对创建成功/失败后的结果做打印。

代码呈现:

#pragma once
 
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
 
namespace nt_server
{
    // 错误码
    enum
    {
        SOCKET_ERR = 1
    };
 
    class UdpServer
    {
    public:
        // 构造
        UdpServer()
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)//创建失败
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            // 创建成功
            std::cout << "Create Success Socket: " << sock_ << std::endl;
        }
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        int sock_; // 套接字
    };
}

总结说明:

因为这里是使用 UDP 协议实现的 网络通信,参数1 domain 选择 AF_INET(基于 IPv4 标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3设置为 0,可以根据 SOCK_DGRAM 自动推导出使用 UDP 协议。

2.2.3.2 bind - 将套接字与一个 IP 和端口号进行绑定

语法:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

解释说明:

  • sockfd: 通过socket函数得到的文件描述符 
  • addr: 需要绑定的socket地址,这个地址封装了ip和端口号的信息 
  • addrlen: 第二个参数结构体占的内存大小

详细了解一下这个sockaddr_in结构体:

struct sockaddr_in {
     short            sin_family;    // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
     unsigned short   sin_port;      // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
     struct in_addr   sin_addr;      // 4 字节 ,32位IP地址
     char             sin_zero[8];   // 8 字节 ,不使用
};
struct in_addr {
     unsigned long s_addr;          // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};

获得一个干净可用的 sockaddr_in 结构体后,可以正式绑定 IP 地址 和 端口号了。

代码呈现:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    class UdpServer
    {
    public:
        // 构造
        UdpServer(const std::string ip, const uint16_t port = default_port)
            :port_(port), ip_(ip)
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            // 创建成功
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.绑定IP地址和端口号
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 置0
 
            // 填充字段
            local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            local.sin_port = htons(port_); // 主机序列转为网络序列
            local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
 
            // 绑定IP地址和端口号
            int n = bind(sock_, (const sockaddr*)&local, sizeof(local));
            if(n<0)
            {
                std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
 
            // 绑定成功
            std::cout << "Bind IP&&Port Success" << std::endl;
        }
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        int sock_; // 套接字
        uint16_t port_; // 端口号
        std::string ip_; // IP地址(后面需要删除)
    };
}

总结说明:

  • 端口号会在网络里互相转发,需要把主机序列转换为网络序列,可以使用 htons 函数。
  • 需要把点分十进制的字符串,转换为无符号短整数,可以使用 inet_addr 函数,这个函数在进行转换的同时,会将主机序列转换为网络序列(因为IP地址需要在网络里面发送)。
  • 绑定IP地址和端口号这个行为并非直接绑定到当前主机中,而是在当前程序中,将创建的 socket 套接字,与目标IP地址与端口号进行绑定,当程序终止后,这个绑定关系也会随之消失。
2.2.3.3 地址转换函数 - 字符串和struct in_addr互相转换

我们这里为什么要使用字符串来表示IP地址:

首先大部分用户习惯使用的IP是点分十进制的字符串,就像下面这个这样。

128.11.3.31

我们的IP地址就是下面的第3个成员:

​struct sockaddr_in {
     short            sin_family;    // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
     unsigned short   sin_port;      // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
     struct in_addr   sin_addr;      // 4 字节 ,32位IP地址
     char             sin_zero[8];   // 8 字节 ,不使用
};
struct in_addr {
     unsigned long s_addr;          // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};

点分十进制的IP地址不好输入,我们往往先用更好输入的字符串来存储IP地址,然后将字符串版的IP地址转换为struct in_addr版的IP地址(也就是点分十进制版的)。

字符串转 in_addr 结构体:

语法:

#include <arpa/inet.h>
 
in_addr_t inet_addr(const char *cp);

说明:

  • 该函数将点分十进制的字符串表示的IPv4地址转换为网络字节序的32位整数。返回的是in_addr_t类型,通常用于填充sin_addr.s_addr字段。
  •  这个函数是更通用的函数,支持IPv4和IPv6地址的转换。第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。第二个参数 src 是输入的字符串表示的IP地址,第三个参数 dst 是输出的二进制表示的IP地址。

使用:

#include <arpa/inet.h>
 
struct in_addr ipv4Address;
const char *ipString = "192.168.1.1";
inet_pton(AF_INET, ipString, &(ipv4Address.s_addr));

in_addr 结构体转字符串:

语法:

#include <arpa/inet.h>
 
char *inet_ntoa(struct in_addr in);

说明:

  • 第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。

  • 第二个参数 src 是输入的二进制表示的IP地址。

  • 第三个参数 dst 是输出的字符串表示的IP地址的缓冲区。第四个参数 size 是缓冲区的大小。

使用:

#include <arpa/inet.h>
 
struct in_addr ipv4Address;
ipv4Address.s_addr = inet_addr("192.168.1.1");
char ipString[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(ipv4Address.s_addr), ipString, INET_ADDRSTRLEN);

我们让IP绑定到 0.0.0.0,0.0.0.0 表示任意IP地址:

#pragma once
//.....
namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
    const std::string="0.0.0.0";//注意这里
 
    class UdpServer
    {
    public:
        // 构造
        UdpServer(const std::string ip=defaultip, const uint16_t port = default_port)
            :port_(port), ip_(ip)
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
           //。。。。
 
            // 2.绑定IP地址和端口号
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 置0
 
            // 填充字段
            local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            local.sin_port = htons(port_); // 主机序列转为网络序列
            local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
 
           //。。。。
        }
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        int sock_; // 套接字
        uint16_t port_; // 端口号
        std::string ip_; // IP地址
    };
}

server.cc 服务器源文件:

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer());
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}
2.2.3.3 recvfrom - 从服务器的套接字里读取数据

语法:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

解释说明:

  • buf:接收缓冲区。
  • len:接收缓冲区的大小。
  • flags:默认设置为 0,表示阻塞。
  • src_addr:输出型参数,获取客户端的套接字信息,也就是获取客户端的 ip 和端口号信息。因为是 udp 网络通信,所以这里传入的还是 struct sockaddr_in 类型的对象地址。
  • addrlen:这里就是 struct sockaddr_in 对象的大小。
  • 返回值:成功会返回获取到数据的字节数;失败返回 -1。

使用示例:

struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
char buffer[1024];
 
int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                              (struct sockaddr*)&sender, &sender_len);
if (bytes_received < 0) {
    perror("recvfrom failed");
    // handle error
}

代码呈现:server.hpp 服务器头文件 

namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    class UdpServer
    {
    //.....
 
        // 启动服务器
        void StartServer()
        {
            // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
            char buff[1024]; // 缓冲区
            while (true)
            {
                // 1. 接收消息
                struct sockaddr_in peer;      // 客户端结构体
                socklen_t len = sizeof(peer); // 客户端结构体大小
 
                // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
                // 传入 0 表示当前是阻塞式读取
                ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
 
                if (n > 0)
                    buff[n] = '\0';
                else
                    continue; // 继续读取
 
                // 2.处理数据
                std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
                uint16_t clientPort = ntohs(peer.sin_port);      // 获取端口号
                printf("Server get message from [%s:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
 
                // 3.回响给客户端
                // ...
            }
        }
 
   //.....
    };
}
2.2.3.4 sendto - 向指定套接字中发送数据

语法:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

解释说明:

  • sockfd:当前服务器的套接字,发送网络数据本质就是先向该主机的网卡(本质上就是文件)中进行写入。
  • buf:待发送的数据缓冲区。
  • len:数据缓冲区的大小。
  • flags:默认设置为 0。
  • dest_addr:接收方的套接字信息,这里也就是客户端的套接字信息。
  • addrlen: struct sockaddr_in 对象的大小。

使用示例:

struct sockaddr_in receiver;
receiver.sin_family = AF_INET;
receiver.sin_port = htons(12345);  // Some port number
inet_pton(AF_INET, "192.168.1.1", &receiver.sin_addr);  // Some IP address
 
char message[] = "Hello, World!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0,
                            (struct sockaddr*)&receiver, sizeof(receiver));
if (bytes_sent < 0) {
    perror("sendto failed");
    // handle error
}

代码呈现:server.hpp 服务器头文件 

//。。。
namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    class UdpServer
    {
    //.....
 
        // 启动服务器
        void StartServer()
        {
            // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
            char buff[1024]; // 缓冲区
            while (true)
            {
                // 1. 接收消息
                struct sockaddr_in peer;      // 客户端结构体
                socklen_t len = sizeof(peer); // 客户端结构体大小
 
                // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
                // 传入 0 表示当前是阻塞式读取
                ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
 
                if (n > 0)
                    buff[n] = '\0';
                else
                    continue; // 继续读取
 
                // 2.处理数据
                std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
                uint16_t clientPort = ntohs(peer.sin_port);      // 获取端口号
                printf("Server get message from [%c:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
 
                // 3.回响给客户端
                n = sendto(sock_, buff, strlen(buff), 0, (const struct sockaddr *)&peer, sizeof(peer));
 
                if (n == -1)
                    std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            }
        }
 
   //.....
    };
}

如何证明服务器端正在运行:

可以通过 Linux 中查看网络状态的指令,因为我们这里使用的是 UDP 协议,所以只需要输入下面这条指令,就可以查看有哪些程序正在运行

netstat -nlup

修改 sever.cc 代码:

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer(80));//使用智能指针创建了一个UdeServer对象
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}
2.2.3.5 命令行参数改装服务端

上面的代码中,我们的端口号都是在代码里面指定了的,但是我们不能每次使用的时候都去修改代码吧,我们其实通过命令行参数来指定端口号

server.hpp:

//....
   // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
//.....

server.cc:

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
void Usage(const char* program)
{
  cout << "Usage:" << endl;
  cout << "\t" << program << "  ServerPort" << endl;
}
 
int main(int argc, char* argv[])
{
  if (argc != 2)
  {
    // 错误的启动方式,提示错误信息
    Usage(argv[0]);
    return USAGE_ERR;
  }
 
  //命令行参数都是字符串,我们需要将其转换成对应的类型
  uint16_t port = stoi(argv[1]);//将字符串转换成端口号
 
    unique_ptr<UdpServer> usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}

2.3 Udp Client 客户端

因为一个端口号只能被一个进程 bind,客户端的应用是非常多的,如果在客户端采用静态 bind,那可能会出现两个应用同时 bind 同一个端口号,此时就注定了这两个应用一定是不能同时运行的。为了解决这个问题,一般不建议客户端 bind 一个固定的端口,而是由操作系统来进行动态的 bind,这样就可以避免端口号发生冲突。这也间接说明,对一个 client 端的进程来说,它的端口号是几并不重要,只要能够标识该进程在主机上的唯一性就可以。因为,一般都是由 clinet 端主动的向 server 端发送消息,所以 client 一定是能够知道 client 端的端口号。相反,服务器的端口号必须是确定的。因此,在编写客户端的代码时,第一步就是创建套接字,创建完无需进行 bind,直接向服务器发送数据,发送的时候,操作系统会为我们进行动态 bind。

client.hpp代码:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
  class UdpClient
    {
    public:
        // 构造
        UdpClient(const std::string& ip, uint16_t port)
            :server_ip_(ip), server_port_(port)
        {} 
 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_); // 绑定服务器端口号
        }
 
        // 启动客户端
void StartClient() 
{
    char buff[1024];
 
    while(true)
    {
        // 1.发送消息
        std::string msg;
        std::cout << "Input Message# ";
        std::getline(std::cin, msg);
 
        ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
 
        if(n == -1)
        {
            std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            continue; // 重新输入消息并发送
        }
 
        // 2.接收消息
        socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
        n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
 
        if(n > 0)
            buff[n] = '\0';
        else
            continue;
 
        // 可以再次获取IP地址与端口号
        std::string ip = inet_ntoa(svr_.sin_addr);
        uint16_t port = ntohs(svr_.sin_port);
 
        printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
    }
}
 
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

client.cc代码:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
  class UdpClient
    {
    public:
        // 构造
        UdpClient(const std::string& ip, uint16_t port)
            :server_ip_(ip), server_port_(port)
        {} 
 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_); // 绑定服务器端口号
        }
 
        // 启动客户端
void StartClient() 
{
    char buff[1024];
 
    while(true)
    {
        // 1.发送消息
        std::string msg;
        std::cout << "Input Message# ";
        std::getline(std::cin, msg);
 
        ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
 
        if(n == -1)
        {
            std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            continue; // 重新输入消息并发送
        }
 
        // 2.接收消息
        socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
        n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
 
        if(n > 0)
            buff[n] = '\0';
        else
            continue;
 
        // 可以再次获取IP地址与端口号
        std::string ip = inet_ntoa(svr_.sin_addr);
        uint16_t port = ntohs(svr_.sin_port);
 
        printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
    }
}
 
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

2.4 简易公共聊天室

server.hpp代码:

#pragma once
 
#include <iostream>
#include <string>
#include <functional>//注意这个
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unordered_map>
 
namespace nt_server
{
 
        // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    using func_t = std::function<std::string(std::string)>; 
// 可以简单的理解为func_t是一个参数为string,返回值同样为string的函数的类型
 
    class UdpServer
    {
    public:
 
        // 构造
        UdpServer(const func_t& func, uint16_t port = default_port)//注意这里的func_t
            :port_(port)
            ,serverHandle_(func)
//注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string,返回值同样为string的函数的类型
        {}
 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            // 创建成功
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.绑定IP地址和端口号
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 置0
 
            // 填充字段
            local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            local.sin_port = htons(port_); // 主机序列转为网络序列
            // local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
            local.sin_addr.s_addr = INADDR_ANY; // 绑定任何可用IP地址
 
            // 绑定IP地址和端口号
            if(bind(sock_, (const sockaddr*)&local, sizeof(local)))
            {
                std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
 
            // 绑定成功
            std::cout << "Bind IP&&Port Success" << std::endl;
        }
 
        //检测是不是新用户
        void CheckUser(const struct sockaddr_in & client,const std::string clientIp_,uint16_t clientPort_)
        {
            auto iter= online_user_.find(clientIp_);
            if(iter==online_user_.end())
            {
                online_user_.insert({clientIp_,client});
                std::cout<<"["<<clientIp_<<":"<<clientPort_<<"] add to oniline user."<<std::endl;
            }
        }
 
        //广播给所有人
        void Broadcast(const std::string& respond,const std::string clientIp_,uint16_t clientPort_)
        {
            for(const auto&usr :online_user_)
            {
                std::string message="[";
                message+=clientIp_;
                message+=" : ";
                message+=std::to_string(clientPort_);
                message+="]#";
                message+=respond;
                
                int z = sendto(sock_, respond.c_str(), respond.size(), 0, (const sockaddr*)&usr.second, sizeof(usr.second));
                if(z == -1)
                    std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            }
 
        }
 
        // 启动服务器
        void StartServer()
        {
            // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
            char buff[1024]; // 缓冲区
            while(true)
            {
                // 1. 接收消息
                struct sockaddr_in client; // 客户端结构体
                socklen_t len = sizeof(client); // 客户端结构体大小
 
                // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
                // 传入 0 表示当前是阻塞式读取
                ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&client, &len);
 
                if(n > 0)
                    buff[n] = '\0';
                else
                    continue; // 继续读取
 
                // 2.处理数据
                 std::string clientIp = inet_ntoa(client.sin_addr); // 获取用户的IP地址
            uint16_t clientPort = ntohs(client.sin_port);      // 获取端口号
 
                //2.1.判断是不是新用户,如果是就加入,如果不是就什么也不做
                CheckUser(client,clientIp,clientPort); 
                
 
                //2.2 对数据进行业务处理,并获取业务处理后的结果
                std::string respond = serverHandle_(buff);
                //特别注意这里,业务处理的代码已经放到了这个serverHandle_这个函数了,
 
                // 3.回响给所有在线客户端
                Broadcast(respond,clientIp,clientPort);  
            }
        }
 
    private:
        int sock_; // 套接字
        uint16_t port_; // 端口号
        func_t serverHandle_; // 业务处理函数(回调函数)
        std::unordered_map<std::string,struct sockaddr_in> online_user_;//在线用户列表
    };
}

server.cc代码:

#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
//业务处理函数
std::string ExecCommand(const std::string& request)
{
    
    return request;
}
 
void Usage(const char* program)
{
  cout << "Usage:" << endl;
  cout << "\t" << program << "  ServerPort" << endl;
}
 
int main(int argc, char* argv[])
{
  if (argc != 2)
  {
    // 错误的启动方式,提示错误信息
    Usage(argv[0]);
    return USAGE_ERR;
  }
 
  //命令行参数都是字符串,我们需要将其转换成对应的类型
  uint16_t port = stoi(argv[1]);//将字符串转换成端口号
 
    unique_ptr<UdpServer> usvr(new UdpServer(ExecCommand,port));
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}

client.hpp代码:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include<functional>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
    class UdpClient
    {
    public:
     // 构造
        UdpClient(const std::string &ip, uint16_t port)
            : server_ip_(ip), server_port_(port)
        {
        }
 
        // 析构
        ~UdpClient()
        {
        }
 
        
        static void *send_message(void *argc)//传进来的是this指针
        {
            UdpClient*_this =(UdpClient*)argc;//强制转换为类指针
 
            char buff[1024];
            while (1)
            {
                // 1.发送消息
                std::string msg;
                std::cout << "Input Message# ";
                std::getline(std::cin, msg);
 
                ssize_t n = sendto(_this->sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr *)&_this->svr_, sizeof(_this->svr_));
 
                if (n == -1)
                {
                    std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
                    continue; // 重新输入消息并发送
                }
            }
                return (void*)0;
        }
 
        static void *recv_message(void *argc)//传进来的是this指针
        {
            UdpClient*_this =(UdpClient*)argc;
 
            char buff[1024];
            while (1)
            {
                // 2.接收消息
                socklen_t len = sizeof(_this->svr_); // 创建一个变量,因为接下来的参数需要传左值
                int n = recvfrom(_this->sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&_this->svr_, &len);
 
                if (n > 0)
                    buff[n] = '\0';
                else
                    continue;
 
                // 可以再次获取IP地址与端口号
                std::string ip = inet_ntoa(_this->svr_.sin_addr);
                uint16_t port = ntohs(_this->svr_.sin_port);
 
                printf("Client get message from [%s:%d]# %s\n", ip.c_str(), port, buff);
            }
                return (void*)0;
        }
 
 
        // 初始化客户端
        void InitClient()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if (sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET;                            // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_);                  // 绑定服务器端口号
        }
 
        // 启动客户端
        void StartClient()
        {
            pthread_t recv, sender;
            //只需将类内的线程函数变化为static函数,但是static函数就不能直接访问到我们类内的私有数据,
            //不要忘了这个线程函数的void*参数,我们可以把this指针作为参数传给它,
            //然后就能通过这个this指针来访问到我们的类内的私有数据
            pthread_create(&recv, nullptr, recv_message, (void*)this);
            pthread_create(&sender, nullptr, send_message, (void*)this);
 
            pthread_join(recv,nullptr);
            pthread_join(sender,nullptr);
        }
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;//套接字描述符
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

client.cc保持不变

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
  class UdpClient
    {
    public:
        // 构造
        UdpClient(const std::string& ip, uint16_t port)
            :server_ip_(ip), server_port_(port)
        {} 
 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_); // 绑定服务器端口号
        }
 
        // 启动客户端
void StartClient() 
{
    char buff[1024];
 
    while(true)
    {
        // 1.发送消息
        std::string msg;
        std::cout << "Input Message# ";
        std::getline(std::cin, msg);
 
        ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
 
        if(n == -1)
        {
            std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            continue; // 重新输入消息并发送
        }
 
        // 2.接收消息
        socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
        n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
 
        if(n > 0)
            buff[n] = '\0';
        else
            continue;
 
        // 可以再次获取IP地址与端口号
        std::string ip = inet_ntoa(svr_.sin_addr);
        uint16_t port = ntohs(svr_.sin_port);
 
        printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
    }
}
 
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

makefile代码:

.PHONY:all
all:server client
 
server:server.cc
	g++ -o $@ $^ -std=c++11 -lpthread
 
	
client:client.cc
	g++ -o $@ $^ -std=c++11 -lpthread
 
.PHONY:clean
clean:
	rm -rf server client

三、结束语 

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​ 


http://www.niftyadmin.cn/n/5739043.html

相关文章

MyBatis查询语句专题

1. 基本查询语句 MyBatis查询语句通常是在XML映射文件中定义&#xff0c;使用标签来表示查询。查询语句可以是简单的SQL&#xff0c;也可以是复杂的动态SQL。查询方法可以返回单个对象、列表或特定的数据类型。 <select id"findUserById" parameterType"in…

第17天文件操作项目练习

文件操作 函数名&#xff1a;rewind() 函数原型&#xff1a;void rewind(FILE *stream); 函数功能&#xff1a;光标移动到文件开头 函数参数&#xff1a;FILE *stream&#xff1a;fp 函数返回值&#xff1a;无 函数使用&#xff1a; rewind(fp) fseek(fp,0,0); 函数名&…

【系统架构设计师】2022年真题论文: 论湖仓—体架构及其应用(包括解题思路和素材)

更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 真题题目(2022年 试题4)解题思路湖仓一体架构概念与特点湖仓一体架构技术组成湖仓一体架构的应用场景论文素材参考真题题目(2022年 试题4) 随着5G、大数据、人工智能、物联网等技术的不断成熟,各行各业的业务…

针对告警数量、告警位置、告警类型等参数进行统计,并做可视化处理的智慧能源开源了。

一、简介 AI视频监控平台, 是一款功能强大且简单易用的实时算法视频监控系统。愿景在最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;减少企业级应用约 95%的开发成本&#xff0c;在强大视频算…

Leetcode328奇偶链表,Leetcode21合并两个有序链表,Leetcode206反转链表 三者综合题

题目描述 思路分析 这题的思路就和我们的标题所述一样&#xff0c;可以看作是这3个题的合并&#xff0c;但是稍微还有一点点区别 比如&#xff1a;奇偶链表这道题主要是偶数链在了奇数后面&#xff0c;字节这个的话是奇偶链表分离了 所以字节这题的大概思路就是&#xff1a; …

【测试工具】Fastbot 客户端稳定性测试

背景 做这个主要为了发版之前提前发现崩溃&#xff0c;风险前置。适合客户端很重的业务。 优点&#xff1a;你不改动也能用&#xff0c; 维护成本不高。 缺点&#xff1a;容易进入H5页面无法返回&#xff0c;效果有限。 备注&#xff1a;我这边接手别人维护&#xff0c;公司…

第一章 微服务入门

目录 一、引言 二、传统单体架构的缺点 三、微服务的概念 ‌四、微服务与SOA的区别 4.1. 微服务架构 4.2. SOA架构 4.3. 区别对比 五、微服务特性 ‌5.1. 微服务的核心特性‌ 5.2. 微服务的优点 5.3. 微服务的缺点 一、引言 在了解微服务之前&#xff0c;我们先…

RDD转换算子:【filter】

功能&#xff1a; 过滤数据 对RDD集合中的每个元素调用一次参数中的表达式对数据进行过滤&#xff0c;符合条件就保留&#xff0c;不符合就去除 语法&#xff1a; def filter(self, f: T -> bool ) -> RDD[T]f&#xff1a;代表参数是一个函数 T&#xff1a;代表RDD中的…