前言
本文旨在通過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; }
- 運行結果