Socket编程UDP

目录

[1. UDP⽹络编程](#1. UDP⽹络编程)

[1.1 服务端初始化实现](#1.1 服务端初始化实现)

[1.2 服务端运行](#1.2 服务端运行)

[1.3 客户端初始化](#1.3 客户端初始化)

[1.4 客户端运行](#1.4 客户端运行)

[1.5 绑定IP地址](#1.5 绑定IP地址)

2.基于UDP⽹络编程实现字典翻译的功能

[2.1 字典](#2.1 字典)

[2.2 字典类,获取IP地址和端口类](#2.2 字典类,获取IP地址和端口类)

[2.3 完整实现](#2.3 完整实现)

[3. 基于UDP⽹络编程实现群聊](#3. 基于UDP⽹络编程实现群聊)

[3.1 UDP⽹络编程实现群聊有关的其他类](#3.1 UDP⽹络编程实现群聊有关的其他类)

[3.2 Server和Client](#3.2 Server和Client)

[3.3 处理任务类Route](#3.3 处理任务类Route)

4.地址转换函数

[4.1 网络转主机字节序](#4.1 网络转主机字节序)

[4.2 主机转网络的字节序](#4.2 主机转网络的字节序)


UDP ( User Datagram Protocol ⽤⼾数据报协议)

传输层协议

⽆连接(对讲机,一方说话,另一方直接就能听见)

不可靠传输(允许丢包,简单,占有资源少)

⾯向数据报(发快递,发邮件,都是确定好的,例如发十个快递,就只能收到十个快递)

1. UDP⽹络编程

对于网络间的通信,需要服务端和客户端,服务端一般要接受客户端的信息进行处理,并发送个客户端,而客户端则需要发送信息个服务端并接收服务端处理完的信息

1.1 服务端初始化实现

对于服务端初始化,需要套接字,此时就需要以下函数创建套接字

cpp 复制代码
头文件
#include <sys/types.h>
#include <sys/socket.h>

说明
创建 socket ⽂件描述符 (TCP/UDP, 客⼾端 + 服务器)

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

参数:
domain:表示这个套接字想要做本地通信还是网络通信
本地通信:传AF_UNIX
网络通信:传AF_INET,使用IPv4进行网络通信
还有其他的参数,这里不进行叙述

type:代表套接字的类型,一般有两种
Udp-->面向数据报,传SOCK_DGRAM
Tcp-->面向字节流,传SOCK_STREAM
还有其他的参数,这里不进行叙述

protocol:代表设定的协议类型
对于网络通信,由于domain和type就可以已经证明是什么协议,因此一般设为0

返回值:
创建成功,返回文件描述符,失败返回-1

对于服务端还需要绑定对应的端口号和IP地址,此时就需要以下函数

cpp 复制代码
头文件:
#include<netinet/in.h>
#include<arpa/inet.h>

说明:
绑定端⼝号和IP地址 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

参数
socket:套接字

address:
网络通信传struct sockaddr_in结构,并进行强转为struct sockaddr
本地通信传struct sockaddr_un结构,并进行强转为struct sockaddr

address_len:
指定 address 结构的长度

接下来说明一下struct sockaddr_in结构

struct sockaddr_in 是 C/C++ 网络编程中用于表示 IPv4 地址的结构体

结构体字段说明

sin_family: 地址族,通常为 AF_INET(IPv4)。

sin_port: 端口号,类型为uint16_t,需使用 htons() 转换为网络字节序(大端)。

sin_addr: 也是一个结构体,内部只有一个成员s_addr,类型为uint32_t,代表IP 地址(点分十进制),需要inet_addr() 或 inet_pton() 网络字节序,转换网络通信的时候,必须是4字节IP吗,例如198.167.0.123(点分十进制)IP地址,每一段代表一字节

还有其他的字段不做说明

cpp 复制代码
// 服务端
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include "Log.hpp" //日志模块

using namespace LogModuls;

const int defaultfd = -1;
class UdpServer // UDP服务端
{
public:
    UdpServer(uint16_t port, const std::string ip)
        : _sockfd(defaultfd),
          _port(port),
          _ip(ip)
    {
    }
    void Init() // 初始化
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "create socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success";
        // 2. 绑定socket信息,ip和端口, ip
        struct sockaddr_in local;
        bzero(&local, sizeof(local));                   // 清空local
        local.sin_family = AF_INET;                     // 使用IPv4协议
        local.sin_port = htons(_port);                  // 端口号转换为网络字节序
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址转换为网络字节序

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "bind socket failed";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success";
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;     // 套接字
    uint16_t _port;  // 端口号
    std::string _ip; // IP地址,使用的是点分十进制,例如192.168.1.100
}

日志类

cpp 复制代码
#ifndef __LOG_HPP__
#define __LOG_HPP__
#include <iostream>
#include "Mutex.hpp"
#include <fstream>
#include <filesystem> //c++17新标准库,用于文件系统操作
#include <ctime>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <memory>
#include <string>

namespace LogModuls
{
    using namespace MutexMudule;
    const std::string gsep = "\r\n"; // 日志分隔符
    // 策略模式,C++多态特性
    // 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;                             // 强制生成默认的析构函数
        virtual void SyncLog(const std::string &message) = 0; // 同步打印日志
    };
    // 显示器刷新策略
    class DisplayStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            // 防止多线程同时访问同一个文件,加锁
            Condition condition(_mutex);
            std::cout << message << gsep;
        }

    private:
        Mutex _mutex;
    };
    // 文件刷新策略
    const std::string DEFAULT_LOG_PATH = "./csdn.76/";
    const std::string DEFAULT_LOG_NAME = "log.txt";

    class FileStrategy : public LogStrategy
    {
    public:
        FileStrategy(const std::string &fileName = DEFAULT_LOG_NAME, const std::string &path = DEFAULT_LOG_PATH)
            : _fileName(fileName), _path(path)
        {
            // 同样的,加锁
            Condition condition(_mutex);
            if (std::filesystem::exists(_path)) // 判断路径是否存在
            {
                return;
            }
            try
            {
                std::filesystem::create_directories(_path); // 创建路径
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n'; // 输出错误信息
            }
        }
        void SyncLog(const std::string &message) override
        {
            // 防止多线程同时访问同一个文件,加锁
            // 创建日志文件,由于可能有多个进程,因此防止创建多个文件,加锁
            Condition condition(_mutex);
            std::string fileName = _path + _fileName;
            std::ofstream ofs(fileName, std::ios::app); // 以追加的方式打开日志文件
            if (!ofs.is_open())
            {
                return;
            }
            ofs << message << gsep;
            ofs.close();
        }

    private:
        std::string _fileName; // 日志文件名
        std::string _path;     // 日志文件路径
        Mutex _mutex;
    };
    // 日志等级
    enum LogLevel
    {
        DEBUG,
        INFO,
        WARN,
        ERROR,
        FATAL
    };
    // 输出字符形式
    std::string LogLevelToString(LogLevel level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARN:
            return "WARN";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }
    // 日志时间
    std::string GetLogTime()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm;            // tm结构体,包含了时间相关的信息,时,分,秒,年,月,日,星期等
        localtime_r(&curr, &curr_tm); // 将时间戳转换为tm结构体
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 curr_tm.tm_year + 1900,
                 curr_tm.tm_mon + 1,
                 curr_tm.tm_mday,
                 curr_tm.tm_hour,
                 curr_tm.tm_min,
                 curr_tm.tm_sec);
        return timebuffer;
    }
    // 日志类
    class Log
    {
    public:
        Log()
        {
            EnableDisplayLogStrategy(); // 默认使用显示器刷新策略
        }
         // 往文件中写入日志
        void EnableFileLogStrategy()
        {
            _strategy = std::make_unique<FileStrategy>(); // 使用make_unique创建策略对象
        }
        // 显示器打印日志
        void EnableDisplayLogStrategy()
        {
            _strategy = std::make_unique<DisplayStrategy>();
        }
        // 代表一条日志信息
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, const std::string &name, int line, Log &log)
                : _level(level),
                  _pid(getpid()),
                  _name(name),
                  _time(GetLogTime()),
                  _line(line),
                  _log(log)
            {
                // 日志的左边部分,合并起来
                std::stringstream ss;
                ss << "[" << _time << "] "
                   << "[" << LogLevelToString(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _name << "] "
                   << "[" << _line << "] "
                   << "- ";
                _message = ss.str();
            }
            // 往日志中添加内容
            template <typename T>
            LogMessage &operator<<(const T &t)
            {
                std::stringstream ss;
                ss << t;
                _message += ss.str();
                return *this;
            }
            //对日志信息进行刷新(打印)
           ~LogMessage()
           {
            if(_log._strategy)
               _log._strategy->SyncLog(_message);
           }
        private:
            LogLevel _level;      // 日志等级
            pid_t _pid;           // 日志id
            std::string _name;    // 日志文件名
            std::string _time;    // 日志时间
            int _line;            // 日志行号
            std::string _message; // 日志信息,合并到一起打印
            Log &_log;           
        };
        //调用日志类,输出日志信息
        //注意返回的是一个临时对象,可以往里面添加内容,当使用完毕后,会自动调用析构,刷新日志
        LogMessage operator()(LogLevel level, const std::string &name, int line)
        {
            return LogMessage(level, name, line, *this);
        }
    private:
        std::unique_ptr<LogStrategy> _strategy; // 日志策略
    };
    // 全局日志对象
    Log logger;

    // 使用宏,简化用户操作,获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__)
    //在显示器上打印日志
    #define Enable_Console_Log_Strategy() logger.EnableDisplayLogStrategy()
    //在文件中写入日志
    #define Enable_File_Log_Strategy() logger->EnableFileLogStrategy()
}
#endif

1.2 服务端运行

对于服务端运行,好比与周围使用的软件,除非用户手动清理后台,不然永远不关闭,因此服务端运行是一个死循环

对于服务端运行,就是要处理客户端所发送的信息,因此需要接收信息,前提是要找到对应的客户端的端口号和IP地址,因此需要以下函数

cpp 复制代码
说明:
主要用于从套接字接收数据

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

参数:
sockfd:一个已打开的套接字描述符。

buf:指向用于存放接收数据的缓冲区的指针。

len:缓冲区的大小,以字节为单位。

flags:控制接收行为的标志,通常可以设置为0代表阻塞状态

src_addr:指向一个sockaddr结构的指针,该结构用于保存发送数据的源地址。

addrlen:一个值-结果参数,表示 src_addr 缓冲区的大小,调用后会被修改为实际地址的长度。

返回值:
成功时,recvfrom 返回接收到的字节数。

如果没有数据可读或套接字已关闭,返回0。

出错时,返回-1,并设置全局变量 errno 以指示错误类型。

同样服务端还需要对处理后的数据发送给服务端,需要一下函数

cpp 复制代码
说明:
函数用于通过指定的 socket 将数据发送到目标主机

int sendto(int s, const void *msg, int len, unsigned int flags, 
const struct sockaddr *to, int tolen);

参数:
s: 已建立连接的 socket 描述符。

msg: 指向要发送的数据内容的指针。

len: 要发送的数据长度。

flags: 一般设为 0。

to: 指定目标地址的 sockaddr 结构体。

tolen: sockaddr 结构体的长度。
cpp 复制代码
void Run() // 运行
{
    _isRun = true;
    while (_isRun)
    {
        char recvbuf[1024] = {0};// 接收缓冲区
        struct sockaddr_in clientaddr;// 客户端地址
        socklen_t len = sizeof(clientaddr);// 地址长度
        ssize_t n = recvfrom(_sockfd, recvbuf, sizeof(recvbuf)-1, 0, (struct sockaddr *)&clientaddr, &len);
        if(n>0)
        {
            LOG(LogLevel::INFO) << "recv data from " << inet_ntoa(clientaddr.sin_addr) << ":" << n;
            // 处理数据
            int client_port = ntohs(clientaddr.sin_port); // 客户端端口号
            std::string client_ip = inet_ntoa(clientaddr.sin_addr); // 客户端IP地址
            recvbuf[n] = 0; // 字符串结尾

            // 处理数据
            LOG(LogLevel::INFO) << "recv data from " << client_ip << ":" << client_port << " data:" << recvbuf;// 打印接收到的数据

            // 服务端向客户端发送处理后数据
            std::string send_data = "server recv data";
            send_data+=recvbuf;
            sendto(_sockfd, send_data.c_str(), send_data.size(), 0, (struct sockaddr *)&clientaddr, len); 
        }
    }
}

1.3 客户端初始化

客户端的初始化在创建socket一样,之后绑定IP地址和端口号和服务端不同,在服务端绑定IP地址和端口号是显示写,但在客户端中不需要显示写,首次发送消息时,OS会自动给客户端进行绑定,OS知道IP,端口号采用随机端口号的方式

为什么客户端的端口号采用随机端口号的方式?

一个端口号,只能被一个进程绑定,为了避免客户端端口冲突,同时在客户端中有的端口号不能被使用,例如(110手机号),当A进程中若把端口号绑定死了,之后退出,B进程也要绑定这个端口号运行,如果这时候运行A进程,就会导致A进程无法运行

cpp 复制代码
    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }
    //填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

1.4 客户端运行

对于客户端来说,首先向相应的服务端发送信息,之后接收服务端处理后的信息

cpp 复制代码
 //  发送数据
    while (true)
    {
        std::string input;
        std::cout << "input message: ";
        std::getline(std::cin, input);//输入要发送的消息

        sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//发送消息

        // 接收数据
        char recv_buf[1024];
        memset(recv_buf, 0, sizeof(recv_buf));
        //注意这里创建client变量,如果服务端有许多个,收到消息,需要知道是哪个服务端发来的
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client, &len);
        if (n > 0)
        {
            recv_buf[n] = '\0';
            std::cout << "recv message: " << recv_buf << std::endl;
        }
    }
    

1.5 绑定IP地址

对于IP地址的绑定,bind不能绑定公网IP,但是对于127.0.0.1这个IP和服务器的内网IP可以进行绑定,同样若服务端绑定127.0.0.1客户端绑定内网IP两端无法通信,同样反过来也是如此,因此当用户显示绑定IP地址,客户端访问时,就必须使用服务端绑定的IP地址

此时如果无法使用bind绑定公网IP,那怎么实现跨网络通信?

因此服务端不建议手动绑定IP

cpp 复制代码
    void Init() // 初始化
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "create socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success";
        // 2. 绑定socket信息,ip和端口, ip
        struct sockaddr_in local;
        bzero(&local, sizeof(local));                   // 清空local
        local.sin_family = AF_INET;                     // 使用IPv4协议
        local.sin_port = htons(_port);                  // 端口号转换为网络字节序
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址转换为网络字节序
        local.sin_addr.s_addr = INADDR_ANY;//绑定到所有地址

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "bind socket failed";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success";
    }

2.基于UDP⽹络编程实现字典翻译的功能

2.1 字典

实现字典翻译,首先就需要一个基本的字典,这里就放一个用来测试的

2.2 字典类,获取IP地址和端口类

对于字典翻译的功能,可以设置成一个类,用于用户创建对应的对象,使用对应的翻译方法,对与字典类,如果要了解到那个用户进行翻译,就可以利用获取IP地址和端口类进行查询

inerAddr.hpp(获取IP地址和端口类)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
//获取IP地址和端口号类
class inerAddr
{
public:
    inerAddr(struct sockaddr_in addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    uint16_t getPort() const { return _port; }
    std::string getIP() const { return _ip; }
    ~inerAddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

Dict.hpp(字典类)

cpp 复制代码
#include <iostream>
#include<unordered_map>
#include<fstream>
#include"Log.hpp"//引入日志模块
#include"inerAddr.hpp"

using namespace LogModuls;

const std::string defaultdict = "./dict.txt";//默认字典路径
const std::string sep = ": ";//字典文件中每行的分隔符

class Dict {
    public:
        Dict(const std::string &path = defaultdict) : _dict_path(path) 
        {}
        bool LoadDict()//加载字典
        {
            std::ifstream in(_dict_path);//打开字典文件
            if(!in.is_open())
            {
                LOG(LogLevel::ERROR) << "Failed to open dict file: " << _dict_path;
                return false;
            }
            std::string line;
            while(std::getline(in,line))
            {
                auto pos = line.find(sep);
                if(pos == std::string::npos)
                {
                    LOG(LogLevel::WARN) << "解析 " << line<<" 失败";
                    continue;
                }
                std::string english = line.substr(0, pos);
                std::string chinese = line.substr(pos + sep.size());
                if(english.empty() || chinese.empty())
                {
                    LOG(LogLevel::WARN) << "没有有效内容" << line;
                    continue;
                }
                _dict_map.insert(std::make_pair(english, chinese));
                LOG(LogLevel::INFO) << "加载 " << english << " -> " << chinese;
            }
            in.close();
            return true;
        }
        std::string Translate(const std::string &english,inerAddr &client) //翻译英文
        {
            auto it = _dict_map.find(english);
            if(it == _dict_map.end())
            {
                LOG(LogLevel::DEBUG) <<"["<< client.getIP() << ":" << client.getPort() <<"]" << " 没有找到 " ;
                return "None";
            }
            //顺便记录一下客户端的IP和端口
            LOG(LogLevel::DEBUG) <<"["<< client.getIP() << ":" << client.getPort() <<"]" << " 请求翻译 " << english << " -> " << it->second;
            return it->second;
        }
        ~Dict()
        {}
    private:
        std::string _dict_path;//字典的路径
        std::unordered_map<std::string, std::string> _dict_map;//字典的map,映射对应的翻译
};

2.3 完整实现

UdpServer.hpp(服务端类)

cpp 复制代码
// 服务端
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "inerAddr.hpp"//网络地址模块
#include "Log.hpp"//日志模块

using namespace LogModuls;


using func_t =std::function<std::string(const std::string&,inerAddr&)>;

const int defaultfd = -1;
class UdpServer // UDP服务端
{
public:
    UdpServer(uint16_t port,func_t func)
        : _sockfd(defaultfd),
          _port(port),
          _func(func)
    {
    }
    void Init() // 初始化
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "create socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success";
        // 2. 绑定socket信息,ip和端口, ip
        struct sockaddr_in local;
        bzero(&local, sizeof(local));                   // 清空local
        local.sin_family = AF_INET;                     // 使用IPv4协议
        local.sin_port = htons(_port);                  // 端口号转换为网络字节序
        local.sin_addr.s_addr = INADDR_ANY;//绑定到所有地址

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "bind socket failed";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success";
    }
    void Run() // 运行
    {
        _isRun = true;
        while (_isRun)
        {
            char recvbuf[1024] = {0};           // 接收缓冲区
            struct sockaddr_in clientaddr;      // 客户端地址
            socklen_t len = sizeof(clientaddr); // 地址长度
            ssize_t n = recvfrom(_sockfd, recvbuf, sizeof(recvbuf) - 1, 0, (struct sockaddr *)&clientaddr, &len);
            if (n > 0)
            {
                inerAddr client_addr(clientaddr);//把IP地址和端口号地址转换
                recvbuf[n] = 0;                   // 字符串结尾

                // 处理数据
                std::string recv_dict=_func(recvbuf,client_addr);//把任务交给用户处理
                // 服务端向客户端发送处理后数据
                sendto(_sockfd, recv_dict.c_str(), recv_dict.size(), 0, (struct sockaddr *)&clientaddr, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;     // 套接字
    uint16_t _port;  // 端口号
    bool _isRun;     // 是否运行
    func_t _func;    // 回调函数,把任务交给用户处理
};

UdpServer.cc(服务端)

cpp 复制代码
#include <iostream>
#include <memory>
#include"Dict.hpp"
#include "UdpServer.hpp"


int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    Enable_Console_Log_Strategy();
    // 创建UDP服务端
    uint16_t port = std::stoi(argv[1]);

    //字典
    Dict dict;
    dict.LoadDict();//加载默认字典

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,[&dict](const std::string &message,inerAddr &cli)->std::string{
        return dict.Translate(message, cli);
    });
    // 初始化UDP服务端
    usvr->Init();
    // 运行UDP服务端
    usvr->Run();
    return 0;
}

UdpClient.cc(客户端)

cpp 复制代码
//客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }
    //填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    //  发送数据
    while (true)
    {
        std::string input;
        std::cout << "input message: ";
        std::getline(std::cin, input);//输入要发送的消息

        sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//发送消息

        // 接收数据
        char recv_buf[1024];
        memset(recv_buf, 0, sizeof(recv_buf));
        
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client, &len);
        if (n > 0)
        {
            recv_buf[n] = '\0';
            std::cout << "recv message: " << recv_buf << std::endl;
        }
    }
    
    return 0;
}

3. 基于UDP⽹络编程实现群聊

对于服务器而言,它可以收到来自客户端发送的消息和对应的IP地址及端口号,如果实现群聊,服务器会把客户端发送的消息转发给所有人,但是由服务端转发效果并不好,此时就可以让服务端给线程池推送任务,线程池中的线程来进行转发,这样效率会高一些,而UDP协议⽀持全双⼯,⼀个sockfd,既可以读取,⼜可以写⼊,对于客⼾端和服务端同样如此,因此需要支持多线程客⼾端,同时读取和写⼊

3.1 UDP⽹络编程实现群聊有关的其他类

在实现这个功能时首先要映入其他的类,例如:线程池类,互斥锁的类,线程类,条件变量类,还有上述字典功能中实现的获取IP地址和端口类,如下

线程类

cpp 复制代码
#ifndef _THREAD_H_
#define _THREAD_H_
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
namespace chuxin
{

    static uint32_t num = 1;
    class Thread
    {
        using func_t = std::function<void()>;

    private:
        void Running()
        {
            _running = true;
        }
        void Detached()
        {
            _detached = true;
        }

        static void *Routine(void *arg)
        {
            Thread *p = static_cast<Thread *>(arg);
            p->Running();
            if (p->_detached)
                p->Detach();
            pthread_setname_np(p->_tid, p->_name.c_str());
            p->_func();
            return nullptr;
        }

    public:
        Thread(func_t func)
            : _tid(0), _running(false), _detached(false), _func(func), _res(nullptr)
        {
            _name = "thread-" + std::to_string(num++);
        }
        ~Thread()
        {
        }

        void Detach()
        {
            if (_detached)
                return;
            if (_running)
                pthread_detach(_tid);
            Detached();
        }
        bool Start()
        {
            if (_running)
                return false;
            int n = pthread_create(&_tid, NULL, Routine, this); // 这里this
            if (n != 0)
            {
    
                return false;
            }
            else
            {

                return true;
            }
        }
        bool Stop()
        {
            if (_running)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
           
                    return false;
                }
                else
                {
                    _running = false;
           
                    return true;
                }
            }
            return false;
        }
        void Join()
        {
            if (_detached)
            {
         
                return;
            }

            int n = pthread_join(_tid, &_res);
            if (n != 0)
            {

            }
            else
            {

            }
        }
         pthread_t Id()
        {
            return _tid;
        }
    private:
        pthread_t _tid;
        std::string _name;
        bool _running;
        bool _detached;
        void *_res;
        func_t _func;
    };
}
#endif

互斥锁类

cpp 复制代码
#pragma once
#include<pthread.h>
#include<iostream>
namespace MutexMudule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&m_mutex, NULL);
        }
        void lock()
        {
            pthread_mutex_lock(&m_mutex);
        }
        void unlock()
        {
            pthread_mutex_unlock(&m_mutex);
        }
        pthread_mutex_t* getMutex()
        {
            return &m_mutex;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&m_mutex);
        }
        private:
            pthread_mutex_t m_mutex;
    };
    class Condition
    {
        public:
            Condition(Mutex& mutex)
            :m_mutex(mutex)
            {
                m_mutex.lock();
            }
            ~Condition()
            {
                m_mutex.unlock();
            }
        private:
         Mutex &m_mutex;
    };
}

条件变量类

cpp 复制代码
#include <iostream>
#include "Mutex.hpp" //互斥量的封装

namespace chuxin
{
    using namespace MutexMudule; //使用互斥量模块
    class Cond
    {
    public:
        Cond()
        {
            pthread_cond_init(&_cond, NULL);
        }
        void Cond_signal_wait()
        {
            pthread_cond_signal(&_cond);
        }
        void Broadcast()
        {
            pthread_cond_broadcast(&_cond);
        }
        void Wait(Mutex &mutex)
        {
            pthread_cond_wait(&_cond, mutex.getMutex());
        }
        ~Cond()
        {
            pthread_cond_destroy(&_cond);
        }

    private:
        pthread_cond_t _cond;
    };
}

线程池类

cpp 复制代码
#pragma once
#include <iostream>
#include "Mutex.hpp" //互斥锁
#include "code.hpp"  //线程
#include "Cond.hpp"  //条件变量
#include "Log.hpp"   //日志
#include <vector>
#include <queue>
#include<string>


namespace ThreanPoolModuls
{
    using namespace chuxin;
    using namespace MutexMudule;
    using namespace LogModuls;
    static const int gnum = 5; // 线程池中线程数量
    template <typename T>
    class ThreadPool
    {
    private:
        void WakeUpAllThread()
        {
            // 加锁
            Condition lock(_mutex);
            // 通知所有线程
            if (_waitnum)
                _cond.Broadcast();
            
        }
        void WakeUpOne()
        {
            _cond.Cond_signal_wait();

        }
         void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name)); // 获取线程名
            while (true)
            {
                T t; // 任务
                {
                    Condition lock(_mutex);              // 互斥锁
                    while (_task_.empty() && _isStarted) // 任务队列为空
                    {
                        _waitnum++; // 等待线程数量加1
                        _cond.Wait(_mutex); // 条件变量等待
                        _waitnum--; // 等待线程数量减1
                    }
                    if (!_isStarted && _task_.empty())
                    {
                        break;
                    }
                    t = _task_.front(); // 取出任务
                    _task_.pop();
                }
                t(); // 执行任务
            }
        }
         ThreadPool(int num = gnum, bool isStart = false, int waitnum = 0)
            : _num(num),
              _isStarted(isStart),
              _waitnum(waitnum)
        {
            for (int i = 0; i < num; i++)
            {
                _thread.emplace_back([this]()
                                     {
                                         HandlerTask(); // 处理任务
                                     });
            }
        }
    public:
       
        void Start()
        {
            if (_isStarted)
            {
                return;
            }
            _isStarted = true;
            for (auto &t : _thread)
            {
                t.Start();
            }
        }
        void Join() // 等待所有线程退出
        {
            for (auto &t : _thread)
            {
                t.Join();
            }
        }

        void Stop()
        {
            if (!_isStarted)
            {
                return;
            }
            _isStarted = false;
            // 首先唤醒所有线程,让它们退出,如果有任务在队列中,则处理完后再退出
            WakeUpAllThread();
        }
        bool Enqueue(const T &in) // 提供给外部线程调用,将任务放入队列
        {

            if (_isStarted)
            {
                Condition lock(_mutex);
                _task_.push(in);
                if (_waitnum == _thread.size())
                    WakeUpOne(); // 唤醒一个线程
                return true;
            }
            return false;
        }
    private:
        //对拷贝构造函数和赋值运算符进行删除
        ThreadPool(const ThreadPool<T> &) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
    public:
        static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr)
            {
                Condition lockguard(_lock);

                if (inc == nullptr)
                {
                    inc = new ThreadPool<T>();
                    inc->Start();
                }
            }
            return inc;
        }  
    private:
        std::vector<Thread> _thread; // 线程池
        int _num;                    // 线程数量

        std::queue<T> _task_; // 任务队列
        Mutex _mutex;         // 互斥锁

       Cond _cond; // 条件变量

        bool _isStarted; // 线程池是否启动
        int _waitnum;    // 等待任务的线程数量


        static ThreadPool<T> *inc; // 单例指针
        static Mutex _lock;
    };
    //静态变量类外初始化
    template <typename T>
    ThreadPool<T> *ThreadPool<T>::inc = nullptr;// 线程池实例

    template <typename T>
    Mutex ThreadPool<T>::_lock;// 互斥锁
}

3.2 Server和Client

Server.hpp

cpp 复制代码
// 服务端
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "inerAddr.hpp"//网络地址模块
#include "Log.hpp"//日志模块

using namespace LogModuls;


using func_t =std::function<void(int sockfd,const std::string&,inerAddr&)>;

const int defaultfd = -1;
class UdpServer // UDP服务端
{
public:
    UdpServer(uint16_t port,func_t func)
        : _sockfd(defaultfd),
          _port(port),
          _func(func)
    {
    }
    void Init() // 初始化
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "create socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket success";
        // 2. 绑定socket信息,ip和端口, ip
        struct sockaddr_in local;
        bzero(&local, sizeof(local));                   // 清空local
        local.sin_family = AF_INET;                     // 使用IPv4协议
        local.sin_port = htons(_port);                  // 端口号转换为网络字节序
        local.sin_addr.s_addr = INADDR_ANY;//绑定到所有地址

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "bind socket failed";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket success";
    }
    void Run() // 运行
    {
        _isRun = true;
        while (_isRun)
        {
            char recvbuf[1024] = {0};           // 接收缓冲区
            struct sockaddr_in clientaddr;      // 客户端地址
            socklen_t len = sizeof(clientaddr); // 地址长度
            ssize_t n = recvfrom(_sockfd, recvbuf, sizeof(recvbuf) - 1, 0, (struct sockaddr *)&clientaddr, &len);
            if (n > 0)
            {
                inerAddr client_addr(clientaddr);//把IP地址和端口号地址转换
                recvbuf[n] = 0;                   // 字符串结尾
                _func(_sockfd,recvbuf,client_addr);//把任务交给用户处理
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;     // 套接字
    uint16_t _port;  // 端口号
    bool _isRun;     // 是否运行
    func_t _func;    // 回调函数,把任务交给用户处理
};

Server.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "ThreadPoll.hpp"//线程池
#include "Route.hpp"//任务类
using namespace ThreanPoolModuls;

using task_t = std::function<void()>;

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    // 创建UDP服务端
    uint16_t port = std::stoi(argv[1]);
    
    // 1. 路由服务
    Route r;

    // 2. 线程池
    auto tp = ThreadPool<task_t>::GetInstance();

    // 3. 网络服务器对象,提供通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r, &tp](int sockfd, const std::string &message, inerAddr&peer){
        task_t t = std::bind(&Route::MessageRoute,&r, sockfd, message, peer);
        tp->Enqueue(t);
    });
    // 初始化UDP服务端
    usvr->Init();
    // 运行UDP服务端
    usvr->Run();
    return 0;
}

Client.cc

cpp 复制代码
//客户端
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include"code.hpp"//线程类
#include"ThreadPoll.hpp"
using namespace chuxin;
using namespace ThreanPoolModuls;

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;

void Recv()
{
    while(true)
    {
          // 接收数据
        char recv_buf[1024];
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int n = recvfrom(sockfd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client, &len);
        if (n > 0)
        {
            recv_buf[n] = 0;
            std::cerr << recv_buf<< std::endl; // 2
        }
    }
     
}
void Send()
{
      //填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
      //  发送数据
    while (true)
    {
        std::string input;
        std::cout << "input message: ";
        std::getline(std::cin, input);//输入要发送的消息

        sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//发送消息
        if(input == "QUIT")
        {
            pthread_cancel(id);
            break;
        }
    }
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }
    server_ip = argv[1];
    server_port = std::stoi(argv[2]);

    // 1. 创建socket
   sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    //创建线程
    Thread recver(Recv);
    Thread sender(Send);
    recver.Start();
    sender.Start();

    id = recver.Id();

    recver.Join();
    sender.Join();
  
    
    return 0;
}

3.3 处理任务类Route

对于任务类Route主要就是用来给所有的客户端进行转发消息,同时还要管理所有的在线用户

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "inerAddr.hpp"
#include "Log.hpp"
#include"Mutex.hpp"//引入互斥锁
using namespace LogModuls;
using namespace MutexMudule;
class Route {
    private:
        bool IsExist(inerAddr &assr)
        {
            for(auto& user : m_route)
            {
                if(user==assr)
                {
                    return true;
                }
            }
            return false;
        }
        void AddUser(inerAddr& user) {
            LOG(LogLevel::INFO) << "添加一个在线用户: " << user.StringAddr();
            m_route.push_back(user);   
        }
        void DeleteUser(inerAddr& assr) 
        {
            for(auto it=m_route.begin();it!=m_route.end();++it)
            {
                if(*it==assr)
                {
                    LOG(LogLevel::INFO) << "删除一个在线用户: " << assr.StringAddr();
                    m_route.erase(it);
                    break;
                }
            }
        }
    public:
        Route() {
        }
        void MessageRoute(int sockfd,const std::string& msg,inerAddr& addr) {
            Condition lock(m_mutex);
            if(!IsExist(addr))
            {
                AddUser(addr);
            }
            std::string message =addr.StringAddr() + "# " + msg; 
            //转发给所有在线用户消息
            for (auto& user : m_route) {
                sendto(sockfd,message.c_str(),message.size(),0,(const struct sockaddr*)&(user.NetAddr()),sizeof(user.NetAddr()));
            }
            if(msg=="QUIT")
            {
                LOG(LogLevel::INFO) << "删除一个在线用户: " << addr.StringAddr();
                DeleteUser(addr);
            }
        }
        ~Route() {}
    private:
        std::vector<inerAddr> m_route;//在线用户
        Mutex m_mutex;
};

4.地址转换函数

IP地址和端口号主机和网络使用的是不一样的,因此需要利用地址转换函数,在上文实现了一个获取IP地址和端口号的类,如下

4.1 网络转主机字节序

cpp 复制代码
//获取IP地址和端口号类
class inerAddr
{
public:
    inerAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        //网络转主机字节序
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    uint16_t getPort() const { return _port; }
    std::string getIP() const { return _ip; }
    ~inerAddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

ntohs函数

cpp 复制代码
说明;
主要用于将网络字节顺序(Network Byte Order)转换为主机字节顺序(Host Byte Order)。

#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);

参数
netshort:端口号

inet_ntoa函数

inet_ntoa这个函数返回了⼀个char*,,很显然是这个函数⾃⼰在内部申请了⼀块内存来保存ip的 结果. man⼿册上说, inet_ntoa函数,,是把这个返回结果放到了静态存储区. 这个时候不需要⼿动进⾏释放,如果调⽤多次这个函数,此时因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区, 这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果.
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
在多线程环境下, 推荐使⽤inet_ntop, 这个函数由调⽤者提供⼀个缓冲区保存结果, 可以规避线程安全问题;

cpp 复制代码
说明:
将网络字节序的二进制地址转换为点分十进制的IP地址格式,
或者对于IPv6地址,转换为冒号十六进制的格式。


#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:
af参数指定地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。

src参数指向要转换的二进制IP地址。

dst参数指向一个足够大的缓冲区,用于存放转换后的文本地址。

size参数指定dst缓冲区的大小。

函数成功时返回一个指向dst的非空指针,失败时返回NULL。

对此可以修改为

cpp 复制代码
class inerAddr
{
public:
    inerAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        //网络转主机字节序
        _port = ntohs(_addr.sin_port);
        //_ip = inet_ntoa(_addr.sin_addr);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    uint16_t getPort() const { return _port; }
    std::string getIP() const { return _ip; }
    ~inerAddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

4.2 主机转网络的字节序

对于IP地址用inet_pton函数,端口号htons函数

htons函数

cpp 复制代码
说明:
其作用是将主机字节序转换为网络字节序


uint16_t htons(uint16_t hostshort);

参数:
hostshort表示主机端口号字节序的值。

返回值: 转换后的网络字节序值。

inet_pton函数

cpp 复制代码
说明:
它可以将 IP 地址从点分十进制的字符串形式转换为网络字节顺序的二进制形式。

int inet_pton(int family, const char *strptr, void *addrptr); 

参数:
family: 地址族,可以是 AF_INET(IPV4) 或 AF_INET6(IPV6)。

strptr: 指向要转换的 IP 地址字符串的指针。

addrptr: 指向存储转换结果的缓冲区的指针。

返回值:

1: 转换成功。

0: 输入的 IP 地址字符串不是有效的表达式。

-1: 发生错误,可以通过 errno 获取错误代码。
cpp 复制代码
//获取IP地址和端口号类
class inerAddr
{
public:
    inerAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        //网络转主机字节序
        _port = ntohs(_addr.sin_port);
        //_ip = inet_ntoa(_addr.sin_addr);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    inerAddr(const std::string &ip, uint16_t port): _ip(ip), _port(port)
    {
        //主机字节序转网络字节序
        _addr.sin_port = htons(_port);
        //_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
    }
    uint16_t getPort() const { return _port; }
    std::string getIP() const { return _ip; }
    ~inerAddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};
相关推荐
北京耐用通信2 小时前
神秘魔法?耐达讯自动化Modbus TCP 转 Profibus 如何为光伏逆变器编织通信“天网”
网络·人工智能·网络协议·网络安全·自动化·信息与通信
Ronin3052 小时前
【Linux网络】Socket编程:UDP网络编程实现Echo Server
linux·网络·udp·网络通信·socket编程
霖.242 小时前
service的两种代理实现
linux·服务器·容器·kubernetes
Lin_Aries_04212 小时前
基于 GitLab 的自动化镜像构建
linux·运维·docker·容器·自动化·gitlab
hkhkhkhkh1233 小时前
Git push 失败(remote unpack failed: Missing tree)解决方案
linux·git
Eloudy3 小时前
制作 Bash Shell 方式的软件发布安装包的原理和方法
linux·bash
霖.243 小时前
K8s实践中的重点知识
linux·云原生·kubernetes
truesnow3 小时前
速通 awk:一篇文章带你理解 awk 原理,大量实战案例让你马上成为 awk 专家
linux
游戏开发爱好者83 小时前
TCP 抓包分析:tcp抓包工具、 iOS/HTTPS 流量解析全流程
网络协议·tcp/ip·ios·小程序·https·uni-app·iphone