目錄
1. 回顯服務器 -- echo server
1.1 相關函數介紹
1.1.1 socket()
1.1.2 bind()
1.1.3 recvfrom()
1.1.4 sendto()?
1.1.5 inet_ntoa()
1.1.6 inet_addr()
1.2?Udp 服務端的封裝 -- UdpServer.hpp
1.3?服務端代碼 -- UdpServer.cc
1.4?客戶端代碼 -- UdpClient.cc
1.4.1 Linux版本的客戶端
1.4.2 Windows 版本的客戶端
1.5 demo 演示
1.6 網絡相關命令
2. 翻譯服務器 -- Translation server
2.1 Udp 服務端封裝 -- UdpServer.hpp
2.2 字典結構體的封裝 -- Dict.hpp
2.3 網絡地址轉主機地址的封裝 -- InetAddr.hpp
2.4?Udp 服務端 -- UdpServer.cc
2.5?Udp 客戶端 -- UdpClient.cc
1. 回顯服務器 -- echo server
? ? ? ? 使用C++實現一個回顯服務器,該代碼的作用是客戶端向服務端發送消息,然后回顯到客戶端的顯示器上。
? ? ? ? 先給出需要使用的互斥鎖的封裝模塊和線程安全的日志模塊。
// Mutex.hpp#pragma once
#include <pthread.h>// 將互斥量接口封裝成面向對象的形式
namespace MutexModule
{class Mutex{public:Mutex(){int n = pthread_mutex_init(&_mutex, nullptr);(void)n;}~Mutex(){int n = pthread_mutex_destroy(&_mutex);(void)n;}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}pthread_mutex_t* Get() // 獲取原生互斥量的指針{return &_mutex;}private:pthread_mutex_t _mutex;};// 采用RAII風格進行鎖管理,當局部臨界區代碼運行完的時候,局部LockGuard類型的對象自動進行釋放,調用析構函數釋放鎖class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex& _mutex;};
}
// Log.hpp#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <ctime>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式 -- 利用C++的多態特性// 1. 刷新策略 a: 向顯示器打印 b: 向文件中寫入// 刷新策略基類class LogStrategy{public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};// 顯示器打印日志的策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{// 加鎖使多線程原子性的訪問顯示器LockGuard lockGuard(_mutex);std::cout << message << gsep;}~ConsoleLogStrategy(){}private:Mutex _mutex;};// 文件打印日志策略// 默認的日志文件路徑和日志文件名const std::string defaultPath = "./log";const std::string defaultFile = "my.log";class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultPath, const std::string &file = defaultFile): _path(path),_file(file){// 加鎖使多線程原子性的訪問文件LockGuard lockGuard(_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{LockGuard lockGuard(_mutex);// 追加方式向文件中寫入std::string fileName = _path + (_path.back() == '/' ? "" : "/") + _file;// std::ofstream是C++標準庫中用于輸出到文件的流類,主要用于將數據寫入文件std::ofstream out(fileName, std::ios::app);if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:std::string _path; // 日志文件所在路徑std::string _file; // 日志文件本身Mutex _mutex;};// 2. 形成完整日志并刷新到指定位置// 2.1 日志等級enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};// 2.2 枚舉類型的日志等級轉換為字符串類型std::string Level2Str(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 "UNKNOWN";}}// 2.3 獲取當前時間的函數std::string GetCurTime(){// time 函數參數為一個time_t類型的指針,若該指針不為NULL,會把獲取到的當前時間值存儲在指針指向的對象中// 若傳入為NULL,則僅返回當前時間,返回從1970年1月1日0點到目前的秒數time_t cur = time(nullptr);struct tm curTm;// localtime_r是localtime的可重入版本,主要用于將time_t類型表示的時間轉換為本地時間,存儲在struct tm 結構體中localtime_r(&cur, &curTm);char timeBuffer[128];snprintf(timeBuffer, sizeof(timeBuffer), "%4d-%02d-%02d %02d:%02d:%02d",curTm.tm_year + 1900,curTm.tm_mon + 1,curTm.tm_mday,curTm.tm_hour,curTm.tm_min,curTm.tm_sec);return timeBuffer;}// 2.4 日志形成并刷新class Logger{public:// 默認刷新到顯示器上Logger(){EnableConsoleLogStrategy();}void EnableConsoleLogStrategy(){// std::make_unique用于創建并返回一個std::unique_ptr對象_fflushStrategy = std::make_unique<ConsoleLogStrategy>();}void EnableFileLogStrategy(){_fflushStrategy = std::make_unique<FileLogStrategy>();}// 內部類默認是外部類的友元類,可以訪問外部類的私有成員變量// 內部類LogMessage,表示一條日志信息的類class LogMessage{public:LogMessage(LogLevel &level, std::string &srcName, int lineNum, Logger &logger): _curTime(GetCurTime()),_level(level),_pid(getpid()),_srcName(srcName),_lineNum(lineNum),_logger(logger){// 日志的基本信息合并起來// std::stringstream用于在內存中進行字符串的輸入輸出操作, 提供一種方便的方式處理字符串// 將不同類型的數據轉換為字符串,也可以將字符串解析為不同類型的數據std::stringstream ss;ss << "[" << _curTime << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _srcName << "] "<< "[" << _lineNum << "] "<< "- ";_logInfo = ss.str();}// 使用模板重載運算符<< -- 支持不同數據類型的輸出運算符重載template <typename T>LogMessage &operator<<(const T &info){std::stringstream ss;ss << info;_logInfo += ss.str();return *this;}~LogMessage(){if (_logger._fflushStrategy){_logger._fflushStrategy->SyncLog(_logInfo);}}private:std::string _curTime; // 日志時間LogLevel _level; // 日志等級pid_t _pid; // 進程pidstd::string _srcName; // 輸出日志的文件名int _lineNum; //輸出日志的行號std::string _logInfo; //完整日志內容Logger &_logger; // 方便使用策略進行刷新};// 使用宏進行替換之后調用的形式如下// logger(level, __FILE__, __LINE__) << "hello world" << 3.14;// 這里使用仿函數的形式,調用LogMessage的構造函數,構造一個匿名的LogMessage對象// 返回的LogMessage對象是一個臨時對象,它的生命周期從創建開始到包含它的完整表達式結束(可以簡單理解為包含// 這個對象的該行代碼)// 代碼調用結束的時候,如果沒有LogMessage對象進行臨時對象的接收,則會調用析構函數,// 如果有LogMessage對象進行臨時對象的接收,會調用拷貝構造或者移動構造構造一個對象,并析構臨時對象// 所以通過臨時變量調用析構函數進行日志的打印LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflushStrategy;};// 定義一個全局的Logger對象Logger logger;// 使用宏定義,簡化用戶操作并且獲取文件名和行號#define LOG(level) logger(level, __FILE__, __LINE__) // 使用仿函數的方式進行調用#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}#endif
1.1 相關函數介紹
1.1.1 socket()
????????在網絡編程領域,socket
?是一個基礎且關鍵的函數,主要用于創建網絡通信的端點,也就是 “套接字”。
原型:int socket(int domain, int type, int protocol);頭文件:#include <sys/types.h>#include <sys/socket.h>參數:domain(協議族):此參數用于確定網絡通信所使用的協議棧,常見的取值有:AF_INET:代表 IPv4 協
議,AF_INET6:表示 IPv6 協議,AF_UNIX:用于本地通信的 Unix 域套接字。type(套接字類型):該參數決定了通信的特性,常用的類型有:SOCK_STREAM:提供面向連接的、可靠
的數據流服務,TCP 協議就屬于這種類型。SOCK_DGRAM:實現無連接的、不可靠的數據報服務,UDP 協議是其
典型代表。SOCK_RAW:允許直接訪問底層協議,可用于自定義協議的開發。protocol(協議):當套接字類型不能唯一確定使用的協議時,就需要通過這個參數來明確指定。一般情
況下,將其設置為 0 即可,系統會自動選擇合適的協議。對于 SOCK_STREAM 類型,系統通常會選擇 TCP 協
議。對于 SOCK_DGRAM 類型,系統一般會選擇 UDP 協議。返回值:成功,返回一個非負整數,即調節子描述符,類似文件描述符。失敗,返回-1,并設置errno來指示具體的錯誤原因。功能:創建網絡通信的套接字
1.1.2 bind()
????????在網絡編程中,bind()
?函數是一個關鍵的系統調用,主要用于將一個套接字(通過?socket()
?函數創建)與特定的網絡地址和端口號進行綁定。
原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);頭文件:#include <sys/types.h>#include <sys/socket.h>參數:sockfd:這是通過 socket() 函數返回的套接字描述符,它標識了要進行綁定操作的套接字。addr:這是一個指向 struct sockaddr 類型的指針,其中包含了要綁定的地址和端口信息。不過,
在實際編程中,通常會使用特定協議的地址結構,比如 struct sockaddr_in(用于 IPv4)或 struct
sockaddr_in6(用于 IPv6),然后再將其強制轉換為 struct sockaddr 類型。addrlen:該參數表示 addr 結構的長度,其類型為 socklen_t返回值:成功,返回0.失敗,返回-1,并設置 errno 來指示具體的錯誤原因。功能:用于將一個套接字(通過 socket() 函數創建)與特定的網絡地址和端口號進行綁定。
1.1.3 recvfrom()
????????在網絡編程里,recvfrom
?函數主要用于從 UDP 套接字接收數據并獲取發送方的套接字信息。
原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);頭文件:#include <sys/types.h>#include <sys/socket.h>參數:sockfd:這是通過 socket() 函數返回的套接字描述符,它標識了要接收數據的套接字。buf:這是一個指向緩沖區的指針,用于存儲接收到的數據。len:表示緩沖區 buf 的最大長度,即最多可以接收的字節數。flags:這是一個可選的標志參數,通常設置為 0。常見的標志選項有:MSG_DONTWAIT:將操作設置為非
阻塞模式。MSG_PEEK:查看數據但不將其從接收隊列中移除。src_addr:這是一個指向 struct sockaddr 類型的指針,用于存儲發送方的地址信息。addrlen:這是一個指向 socklen_t 類型的指針,用于指定 src_addr 結構的長度。函數返回時,該參
數會被更新為實際存儲的地址結構長度。返回值:成功,返回實際接收到的字節數。返回0,表示連接已關閉(對于TCP套接字而言)。返回-1,表示調用失敗,此時會設置 errno 來指示具體的錯誤原因。功能:用于從 UDP 套接字接收數據和獲取發送方的套接字信息。
1.1.4 sendto()?
????????sendto()
?是 C 語言網絡編程中的一個關鍵函數,主要用于在無連接的套接字(如 UDP)上發送數據。sendto()
?在發送數據時需要指定目標地址,這使得它非常適合 UDP 這種無連接的通信模式。
原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);頭文件:#include <sys/types.h>#include <sys/socket.h>參數:sockfd:這是通過 socket() 函數創建的套接字描述符,用于標識發送數據的套接字。buf:指向要發送數據的緩沖區的指針。len:要發送數據的長度(以字節為單位)。flags:可選的標志參數,通常設置為 0。常見的標志選項有:MSG_DONTWAIT:將操作設置為非阻塞模
式。MSG_NOSIGNAL:避免在連接斷開時發送 SIGPIPE 信號。dest_addr:指向目標地址的指針,類型為 struct sockaddr。對于 IPv4,通常使用 struct
sockaddr_in;對于 IPv6,則使用 struct sockaddr_in6。addrlen:目標地址結構的長度,類型為 socklen_t。返回值:成功,返回實際發送的字節數(可能小于請求發送的字節數)。失敗,返回-1,并設置 errno 來指示具體的錯誤原因。功能:主要用于在無連接的套接字(如 UDP)上發送數據。
1.1.5 inet_ntoa()
????????inet_ntoa()
?是 C 語言網絡編程中的一個關鍵函數,其主要作用是將?32 位二進制 IPv4 地址轉換為?點分十進制字符串(如?192.168.1.1
)。
原型:char *inet_ntoa(struct in_addr in);頭文件:#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>參數:in:struct in_addr 類型的結構體,該結構體內部有一個 s_addr 成員,用于存儲 32 位的 IPv4 地址
(以網絡字節序表示)。返回值:返回一個指向點分十進制字符串風格的ip地址。功能:將 32 位二進制 IPv4 網絡字節序的 ip 地址轉換為點分十進制字符串(如 192.168.1.1)
1.1.6 inet_addr()
????????inet_addr()
?是 C 語言網絡編程中的一個基礎函數,其主要功能是將點分十進制格式(如?192.168.1.1
)的 IPv4 地址轉換為?32 位二進制網絡字節序整數。
原型:in_addr_t inet_addr(const char *cp);頭文件:#include <sys/types.h>#include <netinet/in.h>#include <arpa/inet.h>參數:cp:指向點分十進制字符串的指針,例如 "127.0.0.1"。返回值:成功,返回 in_addr_t 類型的 32 位整數(網絡字節序)。失敗,返回 INADDR_NONE(通常為 0xFFFFFFFF),這意味著無法解析輸入的字符串。功能:將點分十進制字符串風格的 ip 地址,轉換為4字節的網絡字節序整數。
1.2?Udp 服務端的封裝 -- UdpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"using namespace LogModule;
using func_t = std::function<std::string(const std::string&)>; // 參數為string& 返回值為 string 的函數類型const int defaultfd = -1;class UdpServer
{
public:UdpServer(uint16_t port, func_t func): _sockfd(defaultfd),_port(port),_isrunning(false),_func(func){}void Init(){// 1. 創建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){// 創建套接字失敗LOG(LogLevel::FATAL) << "socket create error!";exit(1);}LOG(LogLevel::INFO) << "socket create seccess, sockfd: " << _sockfd; // 創建成功只是打開文件// 2. 綁定 socket 信息,ip 和 端口號// 2.1 填充 sockaddr_in 結構體struct sockaddr_in local; // 用于網絡通信的結構體bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 主機字節序轉成網絡字節序// 服務端不建議手動bind特定ip// 當一個機器有多張網卡的時候,服務端 ip 綁定INADDR_ANY,就可以接收任意ip中端口號為portlocal.sin_addr.s_addr = INADDR_ANY;// 2.2 綁定服務器的套接字信息// 為什么服務器端要顯式的bind?// 服務器的ip和端口號必須是眾所周知且不能輕易改變的.int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2);}LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;}void Start(){_isrunning = true;while(_isrunning) // 啟動服務器之后是死循環{// 1. 創建用于接收消息的緩沖器變量 buffer 以及接收遠端主機的套接字變量 peerchar buffer[1024];struct sockaddr_in peer; // 客戶端套接字結構體socklen_t len = sizeof(peer);// 2. 收消息,服務端收取客戶端的數據,對數據進行處理// 從 _sockfd 指向的網絡文件中收取客戶端 peer 發送的 sizeof(buffer) - 1 個字節以及客戶端的套接字信息// 第四個參數為0,表示阻塞讀ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);if (s > 0) // 收到消息,s表示收到數據的字節數{int peer_port = ntohs(peer.sin_port); // 將客戶端端口號轉成主機字節序std::string peer_ip = inet_ntoa(peer.sin_addr); // 將客戶端ip轉為字符串風格的ipbuffer[s] = 0;// 服務端顯式發送消息的客戶端信息LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;// 2. 發消息,將消息進行處理后回發給客戶端std::string result = buffer;result = _func(buffer);sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);}}}~UdpServer(){}private:int _sockfd; // 套接字描述符uint16_t _port; // 端口號bool _isrunning;// 運行標志位func_t _func; // 服務端處理數據的回調函數
};
1.3?服務端代碼 -- UdpServer.cc
#include <memory>
#include "UdpServer.hpp"std::string defaultHandler(const std::string &message)
{std::string s = "server say@ ";s += message;return s;
}// 通過命令行 ./udpserver port 啟動服務器
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Enable_Console_Log_Strategy();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaultHandler);usvr->Init();usvr->Start();return 0;
}
1.4?客戶端代碼 -- UdpClient.cc
1.4.1 Linux版本的客戶端
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 通過命令行 ./udpclient server_ip server_port 啟動客戶端
int main(int argc, char *argv[])
{// 客戶端訪問目標服務器需要知道什么// 需要服務器的ip和端口// 怎么知道服務器的ip和端口呢 -- 內置的ipif (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. 創建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket create error" << std::endl;return 2;}// 2. 填充服務端的套接字信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET; // AF_INET 或者 PF_INETserver.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// client不需要顯式的bind,首次發送消息,操作系統自動給client進行bind,// 端口號采用隨機端口號,一個端口號只能被一個進程bind,為了避免client端口沖突// client端口號是多少不重要,只要是唯一的就行while(true){// 1. 給客戶端發消息std::string input;std::cout << "Please Enter# ";if (input.empty()) continue;std::getline(std::cin, input);int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));(void)n;// 2. 回顯消息char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}}return 0;
}
1.4.2 Windows 版本的客戶端
#define _CRT_SECURE_NO_WARNINGS#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
// Windows中需要包含的頭文件
#include <WinSock2.h>
#include <Windows.h>#pragma warning(disable : 4996) // 屏蔽一些 warning 報錯#pragma comment(lib, "ws2_32.lib") // 引入 ws2_32.lib 庫std::string server_ip = "服務器ip地址"; // 服務器ip
uint16_t server_port = 8888; // 服務器端口號int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd); // 構建 2.2 版本// 1. 創建 udp 套接字SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0); // SOCKET == intif (sockfd == SOCKET_ERROR){std::cerr << "socket create error" << std::endl;return 1;}// 2. 填充 sockaddr_in 結構體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());std::string message;char buffer[1024];while (true){// 3. 發信息給服務端std::cout << "Please Enter# ";std::getline(std::cin, message);if (message.empty()) continue;sendto(sockfd, message.c_str(), sizeof(buffer), 0, (struct sockaddr*)&server, sizeof(server));// 4. 收消息,并顯示到顯示器上struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}
?????????WinSock2.h 是 Windows Sockets API(應用程序接口)的頭文件,用于在Windows 平臺上進行網絡編程。它包含了 Windows Sockets 2(Winsock2)所需的數據類型、函數聲明和結構定義,使得開發者能夠創建和使用套接字(sockets)進行網絡通信。
????????在編寫使用 Winsock2 的程序時,需要在源文件中包含 WinSock2.h 頭文件。這樣,編譯器就能夠識別并理解 Winsock2 中定義的數據類型和函數,從而能夠正確地編譯和鏈接網絡相關的代碼。
????????此外,與 WinSock2.h 頭文件相對應的是 ws2_32.lib 庫文件。在鏈接階段,需要將這個庫文件鏈接到程序中,以確保運行時能夠找到并調用 Winsock2 API 中實現的函數。
????????在 WinSock2.h 中定義了一些重要的數據類型和函數,如:
????????????????WSADATA:保存初始化 Winsock 庫時返回的信息。
????????????????SOCKET:表示一個套接字描述符類型,用于在網絡中唯一標識一個套接字。
????????????????sockaddr_in:IPv4 地址結構體,用于存儲 IP 地址和端口號等信息。
????????????????socket():創建一個新的套接字。
????????????????bind():將套接字與本地地址綁定。
????????????????listen():將套接字設置為監聽模式,等待客戶端的連接請求。
????????????????accept():接受客戶端的連接請求,并返回一個新的套接字描述符,用于與客戶端進行通信。
????????WSAStartup 函數是 Windows Sockets API 的初始化函數,它用于初始化Winsock 庫。該函數在應用程序或 DLL 調用任何 Windows 套接字函數之前必須首先執行,它扮演著初始化的角色。
????????以下是 WSAStartup 函數的一些關鍵點:
????????它接受兩個參數:wVersionRequested 和 lpWSAData。wVersionRequested 用于指定所請求的 Winsock 版本,通常使用 MAKEWORD(major, minor)宏,其中major 和 minor 分別表示請求的主版本號和次版本號。lpWSAData 是一個指向 WSADATA?結構的指針,用于接收初始化信息。函數調用成功,它會返回 0;否則,返回錯誤代碼。
????????在調用 WSAStartup 函數后,如果應用程序完成了對請求的 Socket 庫的使用,應調用 WSACleanup 函數來解除與 Socket 庫的綁定并釋放所占用的系統資源。
1.5 demo 演示
? ? ? ? (1)本地使用客戶端和服務端進行通信。
? ? ? ? 服務端因為服務端 ip 進行綁定的時候綁定的是 INADDR_ANY,所以服務端啟動的時候僅需要傳入端口號。
? ? ? ? 客戶端啟動的時候,可以傳入 內網 ip 或者 本地環回 ip:127.0.0.1 和端口號。
? ? ? ? ?客戶端和服務端啟動之后即可進行通信,服務端顯式客戶端的套接字信息以及客戶端發送的信息,客戶端回顯發送的信息:
? ? ? ? (2)跨網絡使用客戶端和服務端進行通信。
? ? ? ? 服務端啟動的時候也僅傳入端口號。
? ? ? 客戶端啟動的時候傳入服務端進程的公網 ip 和端口號。Windows 系統下也一樣,但是Windows下需要啟動 Windows 版本的客戶端。
1.6 網絡相關命令
? ? ? ? ping [-選項] [網址或ip]
? ? ? ? 功能:用于檢測主機是否與網絡進行了連接。
? ? ? ? 常用選項:
? ? ? ? ????????c[次數],默認情況下 ping 是會一直持續下去的,這個選項表示 ping 的次數。
? ? ? ? 上述表示對百度的網站 ping 3 次。?
? ? ? ? netstat [-選項]
? ? ? ? 功能:查看網絡狀態信息。
? ? ? ? 常用選項:
? ? ? ? ? ? ? ? n:拒絕顯示別名,能顯示數字的全部轉化成數字。
? ? ? ? ? ? ? ? l:僅列出有在 Listen(監聽)的服務狀態。
? ? ? ? ? ? ? ? p:顯示建立相關鏈接的程序名和pid。
? ? ? ? ? ? ? ? t:僅顯示 tcp 相關服務。
? ? ? ? ? ? ? ? u:僅顯示 udp 相關服務。
? ? ? ? ? ? ? ? a:顯示所有選項,默認是不顯示 LISTEN 相關。
? ? ? ? 上述命令顯示所有與 udp 相關的網絡服務。?
? ? ? ? 增加 p 選項會顯示進程名和進程 pid,這里沒有顯示是因為 netstat 命令是用普通用戶啟動的,而這幾個服務都是使用超級用戶啟動的,有權限問題。?
? ? ? ? n 選項可以將能用數字顯示的信息用數字顯示出來。?
? ? ? ? watch 命令可以周期性的指向命令。?
? ? ? ? watch -n 1 netstat -nuap -- 每個 1 秒執行一次 netstat -nuap 命令。
? ? ? ? pidof [進程名]
? ? ? ? 功能:查看進程的 pid。
? ? ? ? xargs [命令]
? ? ? ? 功能:將上一個命令傳入管道的內容轉換成后一個命令的參數。
? ? ? ? 通過上述命令快速殺掉啟動的 udpserver 進程。?
2. 翻譯服務器 -- Translation server
2.1 Udp 服務端封裝 -- UdpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogModule;
using func_t = std::function<std::string(const std::string&, InetAddr&)>; // 參數為string& 返回值為 string 的函數類型const int defaultfd = -1;class UdpServer
{
public:UdpServer(uint16_t port, func_t func): _sockfd(defaultfd),_port(port),_isrunning(false),_func(func){}void Init(){// 1. 創建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket create error!";exit(1);}LOG(LogLevel::INFO) << "socket create seccess, sockfd: " << _sockfd;// 2. 綁定 socket 信息,ip 和 端口號struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;// 2.2 綁定服務器的套接字信息int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(2);}LOG(LogLevel::INFO) << "bind success, sockfd: " << _sockfd;}void Start(){_isrunning = true;while(_isrunning){// 1. 創建用于接收消息的緩沖器變量 buffer 以及接收遠端主機的套接字變量 peerchar buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);// 2. 收消息,服務端收取客戶端的數據,對數據進行處理ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);if (s > 0){InetAddr client(peer);int peer_port = ntohs(peer.sin_port);std::string peer_ip = inet_ntoa(peer.sin_addr);buffer[s] = 0;// 2. 發消息,將消息進行處理后回發給客戶端std::string result = _func(buffer, client); // 處理數據sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);}}}~UdpServer(){}private:int _sockfd; // 套接字描述符uint16_t _port; // 端口號bool _isrunning;// 運行標志位func_t _func; // 服務端處理數據的回調函數
};
2.2 字典結構體的封裝 -- Dict.hpp
? ? ? ? 字典文件 -- dictionary.txt
apple: 蘋果
banana: 香蕉
cat: 貓
dog: 狗
book: 書
pen: 筆
happy: 快樂的
sad: 悲傷的
hello:
: 你好run: 跑
jump: 跳
teacher: 老師
student: 學生
car: 汽車
bus: 公交車
love: 愛
hate: 恨
hello: 你好
goodbye: 再見
summer: 夏天
winter: 冬天
#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"const std::string defaultDictPath = "./dictionary.txt";
const std::string sep = ": ";using namespace LogModule;class Dict
{
public:Dict(const std::string &path = defaultDictPath): _dict_path(path){}bool LoadDict(){std::ifstream in(_dict_path);if (!in.is_open()){LOG(LogLevel::DEBUG) << "打開字典:" << _dict_path << " 失敗";return false;}// 1. 循環加載字典的每行數據std::string line;while(std::getline(in, line)){auto pos = line.find(sep);// 1.1 排除字典中無效內容if (pos == std::string::npos){LOG(LogLevel::WARNING) << "解析: " << line << " 失敗";continue; }// 1.2 將有效內容進行加載std::string english = line.substr(0, pos);std::string chinese = line.substr(pos + sep.size());_dict.insert(std::make_pair(english, chinese));if (english.empty() || chinese.empty()){LOG(LogLevel::WARNING) << line << "沒有有效內容";continue;}_dict.insert(std::make_pair(english, chinese));LOG(LogLevel::DEBUG) << "加載: " << line << " 成功";}in.close();return true;}std::string Translate(const std::string &word, InetAddr &client){auto iter = _dict.find(word);if (iter == _dict.end()){LOG(LogLevel::DEBUG) << "進入到了翻譯模塊,[" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";return "None";}LOG(LogLevel::DEBUG) << "進入到了翻譯模塊,[" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;return iter->second;}~Dict(){}private:std::string _dict_path; // 路徑 + 文件名std::unordered_map<std::string, std::string> _dict;
};
2.3 網絡地址轉主機地址的封裝 -- InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
public:InetAddr(struct sockaddr_in &addr) : _addr(addr){_port = ntohs(_addr.sin_port);_ip = inet_ntoa(_addr.sin_addr);}uint16_t Port() {return _port;}std::string Ip() {return _ip;}~InetAddr(){}
private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
2.4?Udp 服務端 -- UdpServer.cc
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"// 回顯服務經常用于檢測
std::string defaultHandler(const std::string &message)
{std::string s = "server say@ ";s += message;return s;
}// 通過命令行 ./udpserver port 啟動服務器
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Enable_Console_Log_Strategy();// 1. 字典對象,提供翻譯功能Dict dict;dict.LoadDict();// 2. 網絡服務器對象,提供通信功能std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client)->std::string{return dict.Translate(word, client);});usvr->Init();usvr->Start();return 0;
}
2.5?Udp 客戶端 -- UdpClient.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 通過命令行 ./udpclient server_ip server_port 啟動客戶端
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. 創建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket create error" << std::endl;return 2;}// 2. 填充服務端的套接字信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET; // AF_INET 或者 PF_INETserver.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 3. 循環讀取客戶端消息while(true){// 3.1. 給客戶端發單詞std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);if (input.empty()) continue;int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));(void)n;// 3.2. 顯示翻譯后的中文char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}}return 0;
}