前言
本文旨在通过linux系统接口实现网络通信,帮助我们更好地掌握socket套接字的使用。通过学习socket网络通信,我们将发现网络通信的本质不过是套路。接下来,让我们直接进入代码编写部分。
- 事先准备
今天我们将模拟实现一个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
在日志类中,如果使用文件策略,为了防止多线程并发访问和创建多个文件,我们需要进行加锁,确保一次只有一个线程访问。
首先明确日志策略,是刷新到文件缓冲区还是命令行缓冲区。我们定义基类,使用子类继承基类的虚方法实现多态,并使用内部类创建日志消息,然后调用外部类的策略方法进行打印。
#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() }
- 编写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; }
- 运行结果