Linux__之__基于UDP的Socket编程网络通信

前言

本篇博客旨在使用Linux系统接口进行网络通信, 帮助我们更好的熟悉使用socket套接字网络通信, 学会了socket网络通信, 就能发现所谓网络, 不过都是套路而已, 话不多说, 让我们直接进入代码编写部分.

1. 事先准备

今天我们先来模拟实现一个echo demo, 也就是客户端向服务器发送信息, 服务器能接收信息, 并且进行回显, 为了代码的可读性和可调试性, 我们在其中使用日志信息, 接下来我会带领大家手撕一个日志代码, 之后运用到我们的echo demo中, 在日志中, 如果想访问临界资源, 我们需要进行加锁和解锁, 这里我也会带领大家, 基于linux系统调用进行锁的封装, 使得我们的锁使用起来更加方便.

1.1 Mutex.hpp

想要进行锁的封装, 那我们首先需要了解一下锁, 这里就不过多赘述锁的定义, 简单来说锁是原子性的, 当操作系统在时间片的作用下进行进程间轮转调度时, 会发生进程间切换,多线程同步访问共享资源时, 可能导致我们的数据不安全, 为了保护临界资源所采取的一种措施就是锁.

锁的定义的方式有两种, 一种是全局使用宏进行初始化, 不需要手动释放, 由操作系统进行释放, 一种是局部定义使用init进行初始化,我们这里使用init初始化, 第一个参数是锁, 第二个参数为锁的属性, 默认为nullptr就行了, 然后销毁就是用destory系统调用, 我们对这些进行封装, 下面创建了一个LockGuard类, 我们利用对象的特性, 出了局部作用域会自动释放的特点, 进一步简化了锁的使用.

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace LockMoudle
{
    class Mutex
    {
    public:
        Mutex(const Mutex&) = delete;
        const Mutex& operator=(const Mutex&) = delete;

        Mutex()
        {
            int n = ::pthread_mutex_init(&_lock, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = ::pthread_mutex_destroy(&_lock);
            (void)n;
        }
        void Lock()
        {
            //加锁
            int n = pthread_mutex_lock(&_lock);
            (void)n;
        }
        //获取锁
        pthread_mutex_t *LockPtr()
        {
            return &_lock;
        }
        //解锁
        void Unlock()
        {
            int n = ::pthread_mutex_unlock(&_lock);
            (void)n;
        }
    private:
        pthread_mutex_t _lock;
    };
    class LockGuard
    {
    public:
        LockGuard(Mutex &mtx)
        :_mtx(mtx)
        {
            _mtx.Lock();
        }
        ~LockGuard()
        {
            _mtx.Unlock();
        }
    private:
        Mutex &_mtx;
    };
}

1.2 Log.hpp

在日志类里, 如果使用文件策略, 为了防止多线程并发访问, 创建多个文件, 我们可以进行加锁, 一次只能有一个线程进行访问

首先明确日志策略, 刷新到文件缓冲区, 还是命令行缓冲区, 定义基类, 使用子类继承基类的虚方法, 实现多态, 使用内部类进行日志消息的创建, 并且调用外部类的策略方法进行打印.


cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem> //C++17
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace LockMoudle;
    //获取当前系统时间
    std::string CurrentTime()
    {
        time_t time_stamp = ::time(nullptr);
        struct tm curr;
        localtime_r(&time_stamp, &curr); //时间戳, 获取可读性较强的时间信息
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
            curr.tm_year + 1900,
            curr.tm_mon + 1,
            curr.tm_mday,
            curr.tm_hour,
            curr.tm_min,
            curr.tm_sec);
        return buffer;
    }

    //构成: 1. 构建日志字符串 2.刷新落盘
    //落盘策略(screen, file)
    //1.日志文件的默认路径和文件名
    const std::string defaultlogpath = "./log/";
    const std::string defaultlogname = "log.txt";

    //2.日志等级
    enum class LogLevel
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    //枚举类型转字符串
    std::string Level2String(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "None";
        }
    }

    //3.刷新策略
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default; //虚析构函数,多态,能够正确调用对象进行析构, 编译器自动生成
        virtual void SyncLog(const std::string &message) = 0;//纯虚函数,子类必须手动实现
    };
    //3.1控制台策略
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {}
        ~ConsoleLogStrategy()
        {}
        //向控制台打印日志信息message
        void SyncLog(const std::string &message)
        {
            LockGuard lockguard(_lock);
            std::cout << message << std::endl;
        }
    private:
        Mutex _lock;
    };
    //文件级策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname)
        : _logpath(logpath),
          _logname(logname)
        {
            //确定_logpath是否存在
            LockGuard lockguard(_lock);
            if(std::filesystem::exists(_logpath))
            {
                return ;
            }
            try
            {
                //不存在进行创建
                std::filesystem::create_directories(_logpath);
            }
            catch(std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }
        ~FileLogStrategy(){}
        //重写基类虚方法
        void SyncLog(const std::string &message)
        {
            LockGuard lockguard(_lock);
            std::string log = _logpath + _logname;
            std::ofstream out(log, std::ios::app);
            if(!out.is_open()) {return;}
            out << message << "\n";
            out.close(); 
        }
    private:
        std::string _logpath;
        std::string _logname;

        Mutex _lock;
    };
    //日志类: 构建日志字符串, 根据策略, 进行写入
    class Logger
    {
    public:
        Logger()
        {
            //默认采用控制台策略
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        void EnableConsolelog()
        {
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        void EnableFileLog()
        {
            _strategy = std::make_shared<FileLogStrategy>();
        }
        
        ~Logger(){}
        //一条完整的信息[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] + 日志的可变部分(<< "hello world" << 3.14 << a << b;)
        //外部类实现策略, 内部类实现信息
        class LogMessage
        {
        public:
        LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)
        : _currtime(CurrentTime()),
          _level(level),
          _pid(::getpid()),
          _filename(filename),
          _line(line),
          _logger(logger)
        {
            std::stringstream ssbuffer;
            ssbuffer << "[" << _currtime << "] "
                    << "[" << Level2String(_level) << "] "
                    << "[" << _pid << "] "
                    << "[" << _filename << "] "
                    << "[" << _line << "] - ";
            _loginfo = ssbuffer.str();
        }
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }

        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

        private:
            std::string _currtime; //当前日志的时间
            LogLevel _level; //日志等级
            pid_t _pid; //进程pid
            std::string _filename; //源文件
            int _line; //行号
            Logger &_logger; //策略
            std::string _loginfo; //日志信息
        };  
    //重载operator(), 故意的拷贝
    LogMessage operator()(LogLevel level, const std::string &filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    private:
        std::shared_ptr<LogStrategy> _strategy;
    };
    Logger logger;

    #define LOG(Level) logger(Level, __FILE__, __LINE__)
    #define ENABLE_CONSOLE_LOG() logger.EnableConsolelog()
    #define ENABLE_FILE_LOG() logger.EnableFileLog()
}

2. 编写Echo demo代码

2.1 UdpServer.hpp 和 UdpServer.cc

这里使用套接字进行通信, 套接字可以简单理解为一个文件流, 创建套接字之后填写网络信息, 进行和内核的绑定, 这里我使用的是云服务器, 默认不需要绑定ip, 所以这里我只需绑定端口号, 从命令行获取.

cpp 复制代码
#include "UdpServer.hpp"

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << "localport" << std::endl;
        Die(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);
    ENABLE_CONSOLE_LOG();
    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);
    svr_uptr->InitServer();
    svr_uptr->Start();
    return 0;
}
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "IntAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const static int gsockfd = -1;
//const static std::string gdefaultip = "127.0.0.1" //表示本地主机
const static uint16_t gdefaultport = 8080;

class UdpServer
{
public:
    //命令行输入ip + 端口号进行绑定, 虚拟机无需绑定ip, 只需指定端口号进行绑定即可
    UdpServer(uint16_t port = gdefaultport)
        : _sockfd(gsockfd)
        , _addr(port)
        , _isrunning(false)
        {}
    //都是套路
    void InitServer()
    {
        //1.创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); //指定网络通信模式. 面向数据包, 标记为设置为0
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is" << _sockfd;
        
        //2.1填充网络信息并绑定, 网络信息都在sockaddr_in里面, 这里我们封装一下
        //2.2bind
        int n = ::bind(_sockfd, _addr.Netaddr(), _addr.NetAddrlen());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind" << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }

    //启动, cs模式,进行接受消息, 并且回显回去
    void Start()
    {
        _isrunning = true;
        while(true)
        {
            char inbuffer[1024];
            struct sockaddr_in peer; //客户端信息, 输出型参数
            socklen_t len = sizeof(peer);

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
            if(n > 0)
            {
                InetAddr cli(peer);
                inbuffer[n] = 0;
                std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + '#' + inbuffer;
                LOG(LogLevel::DEBUG) << clientinfo;
                std::string echo_string = "echo#";
                echo_string += inbuffer;
                ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    } 
    ~UdpServer()
    {
        if(_sockfd > gsockfd)
            ::close(_sockfd);
    }
private:
    int _sockfd;
    InetAddr _addr;
    bool _isrunning;
};

2.2 IntAddr.hpp 和 Commm.hpp

这里对IntAddr进行了封装, IntAddr这里包含了网络信息, 网络通信IntAddr_in, 我们需要对他进行强转, C语言版多态

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

class InetAddr
{
private:
    void PortNet2Host()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }
    void IpNet2Host()
    {
        char ipbuffer[64];
        const char *ip = ::inet_ntop(AF_INET,&_net_addr.sin_addr,ipbuffer, sizeof(ipbuffer));
        (void)ip;
    }
public:
    InetAddr(){}
    
    //如果传进来的是一个sockaddr_in, 网络转主机
    InetAddr(const struct sockaddr_in &addr) : _net_addr(addr)
    {
        PortNet2Host();
        IpNet2Host();
    }
    //如果传进来的是端口号, 就转化为网络, 服务器不需要自己绑定ip
    InetAddr(uint16_t port) : _port(port), _ip("")
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }
    struct sockaddr* Netaddr() {return CONV(&_net_addr); }
    socklen_t NetAddrlen() {return sizeof(_net_addr); }
    std::string Ip() {return _ip; }
    uint16_t Port() {return _port; }
    ~InetAddr(){}
private:
    struct sockaddr_in _net_addr;
    std::string _ip;
    uint16_t _port;
};

Comman.hpp

cpp 复制代码
#pragma once

#include<iostream>

#define Die(code) do {exit(code); } while(0)
#define CONV(v) (struct sockaddr *)(v)

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

2.3 Client.cc

客户端进行标准输入获取信息, 并且发送到服务器, 然后接受服务器回显的内容, 并且打印

cpp 复制代码
#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//cs

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

    //1.创建socket
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }
    //1.1填充网络信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = ::htons(serverport);
    server.sin_addr.s_addr = ::inet_addr(serverip.c_str());

    //客户端不需要进行绑定, 端口号由系统进行分配
    while(true)
    {
        std::cout << "Place Enter# ";
        std::string message;
        std::getline(std::cin, message);

        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

3. 运行结果


相关推荐
Hum8le2 小时前
小科普《DNS服务器》
运维·服务器
阿俊仔(摸鱼版)3 小时前
Ubuntu上安装Docker
linux·ubuntu·docker
yunqi12154 小时前
【负载均衡系列】nginx负载高怎么排查
运维·nginx·负载均衡
郑州吴彦祖7725 小时前
【Java】UDP网络编程:无连接通信到Socket实战
java·网络·udp
BigBookX5 小时前
在 Ubuntu 中配置开机自启动脚本并激活 Anaconda 环境
linux·运维·ubuntu
kfepiza5 小时前
netplan是如何操控systemd-networkd的? 笔记250324
linux·网络·笔记·ubuntu
yi个名字6 小时前
Linux中的yum和vim工具使用总结
linux·运维·vim
云观秋毫6 小时前
试试智能体工作流,自动化搞定运维故障排查
运维·数据库·自动化
青花锁7 小时前
Ubuntu 系统部署 Ollama + DeepSeek + Docker + Ragflow
linux·ubuntu·docker·deepseek
不是编程家7 小时前
Linux第九讲:动静态库
linux·运维·服务器