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

前言

本文旨在通过linux系统接口实现网络通信,帮助我们更好地掌握socket套接字的使用。通过学习socket网络通信,我们将发现网络通信的本质不过是套路。接下来,让我们直接进入代码编写部分。

  1. 事先准备

今天我们将模拟实现一个echo demo,即客户端向服务器发送信息,服务器接收并回显这些信息。为了提高代码的可读性和调试性,我们将使用日志信息。我将带领大家手动编写日志代码,并将其应用于echo demo中。在日志中,如果需要访问临界资源,我们需要进行加锁和解锁操作。这里我将引导大家基于Linux系统调用封装锁,使得锁的使用更加便捷。

1.1 Mutex.hpp

想要封装锁,我们首先需要了解锁的概念。简而言之,锁是原子性的操作,用于保护在线程环境下共享资源的安全。锁的定义有两种方式:一种是使用宏进行全局初始化,无需手动释放,由操作系统自动释放;另一种是局部定义并使用init进行初始化。我们将使用init初始化方法,第一个参数是锁,第二个参数为锁的属性,默认为nullptr。销毁时使用destroy系统调用。我们将这些操作封装在一个LockGuard类中,利用对象的特性,离开局部作用域时自动释放,进一步简化锁的使用。

#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

在日志类中,如果使用文件策略,为了防止多线程并发访问和创建多个文件,我们需要进行加锁,确保一次只有一个线程访问。

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

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

#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;     };     //3.2文件策略     class FileLogStrategy : public LogStrategy {     public:         FileLogStrategy(const std::string &path = defaultlogpath, const std::string &name = defaultlogname)         : _path(path), _name(name) {             _file.open(_path + _name, std::ios::app);         }         ~FileLogStrategy() {             if(_file.is_open()) {                 _file.close();             }         }         //向文件中写入日志信息message         void SyncLog(const std::string &message) {             LockGuard lockguard(_lock);             _file << message << std::endl;             _file.flush();         }     private:         std::string _path;         std::string _name;         std::ofstream _file;         Mutex _lock;     };     //4.日志记录器     class Logger {     public:         Logger() : _strategy(nullptr) {}         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] + 日志的可变部分(         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)             {}             //重载operator<<, 记录日志信息             template<typename T>             LogMessage& operator<<(const T &data) {                 std::ostringstream oss;                 oss << data;                 _loginfo += oss.str();                 return *this;             }             //同步日志信息             ~LogMessage() {                 std::ostringstream oss;                 oss << "[" << _currtime << "] [" << Level2String(_level) << "] [" << _pid << "] [" << _filename << "] [" << _line << "] " << _loginfo;                 _logger.SyncLog(oss.str());             }         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() }
  1. 编写Echo demo代码

2.1 udpServer.hpp 和 UdpServer.cc

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

#include "UdpServer.hpp" int main(int argc, char *argv[]) {     if(argc != 2) {         std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;         Die(USAGE_ERR);     }     uint16_t port = static_cast<uint16_t>(std::atoi(argv[1]));     std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);     svr_uptr->InitServer();     svr_uptr->Start();     return 0; }
#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 "InetAddr.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::ERROR) << "socket error: " << strerror(errno);             Die(SOCKET_ERR);         }         //2.绑定套接字         if(::bind(_sockfd, _addr.Netaddr(), _addr.NetAddrlen()) < 0) {             LOG(LogLevel::ERROR) << "bind error: " << strerror(errno);             Die(BIND_ERR);         }         _isrunning = true;     }     void Start() {         char inbuffer[1024];         struct sockaddr_in peer;         socklen_t peerlen = sizeof(peer);         while(_isrunning) {             memset(inbuffer, 0, sizeof(inbuffer));             int n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &peerlen);             if(n < 0) {                 LOG(LogLevel::ERROR) << "recvfrom error: " << strerror(errno);                 continue;             }             InetAddr cli(peer);             inbuffer[n] = 0;             std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + '#' + inbuffer;             LOG(LogLevel::DEBUG) << "recvfrom client: " << clientinfo;             //回显信息             n = ::sendto(_sockfd, inbuffer, n, 0, (struct sockaddr*)&peer, peerlen);             if(n < 0) {                 LOG(LogLevel::ERROR) << "sendto error: " << strerror(errno);             }         }     }     ~UdpServer() {         if(_sockfd != gsockfd)             ::close(_sockfd);     } private:     int _sockfd;     InetAddr _addr;     bool _isrunning; };

2.2 IntAddr.hpp 和 Commm.hpp

这里对IntAddr进行了封装,IntAddr包含了网络信息。网络通信中,我们需要对InetAddr进行强转,实现c语言版本的多态。

#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

#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

客户端通过标准输入获取信息并发送到服务器,然后接收并打印服务器回显的内容。

#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> int main(int argc, char *argv[]) {     if(argc != 3) {         std::cerr << "Usage: " << argv[0] << " <ip> <port>" << std::endl;         Die(USAGE_ERR);     }     std::string ip = argv[1];     uint16_t port = static_cast<uint16_t>(std::atoi(argv[2]));     UdpClient client(ip, port);     client.InitClient();     char buffer[1024];     while(true) {         std::cout << "请输入要发送的信息: ";         std::cin.getline(buffer, sizeof(buffer));         if(strcmp(buffer, "quit") == 0) {             break;         }         client.Send(buffer);         int n = client.Recv(buffer, sizeof(buffer) - 1);         if(n > 0) {             buffer[n] = 0;             std::cout << "服务器回显: " << buffer << std::endl;         }     }     return 0; }
  1. 运行结果

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

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享