Linux 的 UDP 网络编程 -- 回显服务器,翻译服务器
目录
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;
}
相关文章:

Linux 的 UDP 网络编程 -- 回显服务器,翻译服务器
目录 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.…...

C++笔试题(金山科技新未来训练营):
题目分布: 17道单选(每题3分)3道多选题(全对3分,部分对1分)2道编程题(每一道20分)。 不过题目太多,就记得一部分了: 单选题: static变量的初始…...

【RabbitMQ】 RabbitMQ高级特性(二)
文章目录 一、重试机制1.1、重试配置1.2、配置交换机&队列1.3、发送消息1.4、消费消息1.5、运行程序1.6、 手动确认 二、TTL2.1、设置消息的TTL2.2、设置队列的TTL2.3、两者区别 三 、死信队列6.1 死信的概念3.2 代码示例3.2.1、声明队列和交换机3.2.2、正常队列绑定死信交…...
大数据技术全景解析:HDFS、HBase、MapReduce 与 Chukwa
大数据技术全景解析:HDFS、HBase、MapReduce 与 Chukwa 在当今这个信息爆炸的时代,大数据已经成为企业竞争力的重要组成部分。从电商的用户行为分析到金融的风险控制,从医疗健康的数据挖掘到智能制造的实时监控,大数据技术无处不…...

电子电路:什么是电流离散性特征?
关于电荷的量子化,即电荷的最小单位是电子的电荷量e。在宏观电路中,由于电子数量极大,电流看起来是连续的。但在微观层面,比如纳米器件或单电子晶体管中,单个电子的移动就会引起可观测的离散电流。 还要提到散粒噪声,这是电流离散性的表现之一。当电流非常小时,例如在二…...

深入理解位图(Bit - set):概念、实现与应用
目录 引言 一、位图概念 (一)基本原理 (二)适用场景 二、位图的实现(C 代码示例) 三、位图应用 1. 快速查找某个数据是否在一个集合中 2. 排序 去重 3. 求两个集合的交集、并集等 4. 操作系…...

猫番阅读APP:丰富资源,优质体验,满足你的阅读需求
猫番阅读APP是一款专为书籍爱好者设计的移动阅读应用,致力于提供丰富的阅读体验和多样化的书籍资源。它不仅涵盖了小说、非虚构、杂志等多个领域的电子书,还提供了个性化推荐、书架管理、离线下载等功能,满足不同读者的阅读需求。无论是通勤路…...
Java文件读写程序
1.引言 在日常的软件开发中,文件操作是常见的功能之一。不仅要了解如何读写文件,更要知道如何安全地操作文件以避免程序崩溃或数据丢失。这篇文章将深入分析一个简单的 Java 文件读写程序 Top.java,包括其基本实现、潜在问题以及改进建议&am…...
深入解析Java事件监听机制与应用
Java事件监听机制详解 一、事件监听模型组成 事件源(Event Source) 产生事件的对象(如按钮、文本框等组件) 事件对象(Event Object) 封装事件信息的对象(如ActionEvent包含事件源信息…...

MetaMask安装及使用-使用水龙头获取测试币的坑?
常见的异常有: 1.unable to request drip, please try again later. 2.You must hold at least 1 LINK on Ethereum Mainnet to request native tokens. 3.The address provided does not have sufficient historical activity or balance on the Ethereum Mainne…...

AI:OpenAI论坛分享—《AI重塑未来:技术、经济与战略》
AI:OpenAI论坛分享—《AI重塑未来:技术、经济与战略》 导读:2025年4月24日,OpenAI论坛全面探讨了 AI 的发展趋势、技术范式、地缘政治影响以及对经济和社会的广泛影响。强调了 AI 的通用性、可扩展性和高级推理能力,以…...

Linux配置vimplus
配置vimplus CentOS的配置方案很简单,但是Ubuntu的解决方案网上也很多但是有效的很少,尤其是22和24的解决方案,在此我整理了一下我遇到的问题解决方法 CentOS7 一键配置VimForCPP 基本上不会有什么特别难解决的报错 sudo yum install vims…...

服务端HttpServletRequest、HttpServletResponse、HttpSession
一、概述 在JavaWeb 开发中,获取客户端传递的参数至关重要。http请求是客户端向服务端发起数据传输协议,主要包含包含请求行、请求头、空行和请求体四个部分,在这四部分中分别携带客户端传递到服务端的数据。常见的http请求方式有get、post、…...

实验九视图索引
设计性实验 1. 创建视图V_A包括学号,姓名,性别,课程号,课程名、成绩; 一个语句把学号103 课程号3-105 的姓名改为陆君茹1,性别为女 ,然后查看学生表的信息变化,再把上述数据改为原…...

git 本地提交后修改注释
dos命令行进入目录,idea可以点击Terminal 进入命令行 git commit --amend -m "修改内容"...

面向具身智能的视觉-语言-动作模型(VLA)综述
具身智能被广泛认为是通用人工智能(AGI)的关键要素,因为它涉及控制具身智能体在物理世界中执行任务。在大语言模型和视觉语言模型成功的基础上,一种新的多模态模型——视觉语言动作模型(VLA)已经出现&#…...
Thrust库中的Gather和Scatter操作
Thrust库中的Gather和Scatter操作 Thrust是CUDA提供的一个类似于C STL的并行算法库,其中包含两个重要的数据操作:gather(聚集)和scatter(散开)。 Gather操作 Gather操作从一个源数组中按照指定的索引收集元素到目标数组中。 函数原型: t…...

计算机发展的历程
计算机系统的概述 一, 计算机系统的定义 计算机系统的概念 计算机系统 硬件 软件 硬件的概念 计算机的实体, 如主机, 外设等 计算机系统的物理基础 决定了计算机系统的天花板瓶颈 软件的概念 由具有各类特殊功能的程序组成 决定了把硬件的性能发挥到什么程度 软件的分类…...

深度学习驱动下的目标检测技术:原理、算法与应用创新(三)
五、基于深度学习的目标检测代码实现 5.1 开发环境搭建 开发基于深度学习的目标检测项目,首先需要搭建合适的开发环境,确保所需的工具和库能够正常运行。以下将详细介绍 Python、PyTorch 等关键开发工具和库的安装与配置过程。 Python 是一种广泛应用于…...
Python爬虫实战:研究 RPC 远程调用机制,实现逆向解密
1. 引言 在网络爬虫技术的实际应用中,目标网站通常采用各种加密手段保护其数据传输和业务逻辑。这些加密机制给爬虫开发带来了巨大挑战,传统的爬虫技术往往难以应对复杂的加密算法。逆向解密作为一种应对策略,旨在通过分析和破解目标网站的加密机制,获取原始数据。 然而,…...
[学习] RTKLib详解:qzslex.c、rcvraw.c与solution.c
RTKLib详解:qzslex.c、rcvraw.c与solution.c 本文是 RTKLlib详解 系列文章的一篇,目前该系列文章还在持续总结写作中,以发表的如下,有兴趣的可以翻阅。 [学习] RTKlib详解:功能、工具与源码结构解析 [学习]RTKLib详解…...

jenkins流水线常规配置教程!
Jenkins流水线是在工作中实现CI/CD常用的工具。以下是一些我在工作和学习中总结出来常用的一些流水线配置:变量需要加双引号括起来 "${main}" 一 引用无账号的凭据 使用变量方式引用,这种方式只适合只由密码,没有用户名的凭证。例…...
Java中序列化和反序列化的理解
基本概念 序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程,而反序列化(Deserialization)则是将这种形式重新转换为对象的过程。 核心作用 持久化存储:将对象状态保存到文件或数据库中 网络传输:在网络间传递对象…...

基于OpenCV的SIFT特征和FLANN匹配器的指纹认证
文章目录 引言一、概述二、代码解析1. 图像显示函数2. 核心认证函数2.1 创建SIFT特征提取器2.2 检测关键点和计算描述符(源图像)2.3 检测关键点和计算描述符(模板图像)2.4 创建FLANN匹配器2.5 使用K近邻匹配 3. 匹配点筛选4. 认证…...
零基础学Java——第十一章:实战项目 - 桌面应用开发(JavaFX入门)
第十一章:实战项目 - 桌面应用开发(JavaFX入门) 欢迎来到我们实战项目的桌面应用开发部分!在前面的章节中,我们可能已经接触了Swing。现在,我们将目光投向JavaFX,一个更现代、功能更丰富的用于…...
Milvus 视角看主流嵌入式模型(Embeddings)
嵌入是一种机器学习概念,用于将数据映射到高维空间,其中语义相似的数据被紧密排列在一起。嵌入模型通常是 BERT 或其他 Transformer 系列的深度神经网络,它能够有效地用一系列数字(称为向量)来表示文本、图像和其他数据…...

leetcode:58. 最后一个单词的长度(python3解法)
难度:简单 给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 示例 1: 输入:s "Hello World"…...

虹科应用 | 探索PCAN卡与医疗机器人的革命性结合
随着医疗技术的不断进步,医疗机器人在提高手术精度、减少感染风险以及提升患者护理质量方面发挥着越来越重要的作用。医疗机器人的精确操作依赖于稳定且高效的数据通信系统,虹科提供的PCAN四通道mini PCIe转CAN FD卡,正是为了满足这一需求而设…...

entity线段材质设置
在cesium中,我们可以改变其entity线段材质,这里以直线为例. 首先我们先创建一条直线 const redLine viewer.entities.add({polyline: {positions: Cesium.Cartesian3.fromDegreesArray([-75,35,-125,35,]),width: 5,material:material, 保存后可看到在地图上创建了一条线段…...

[STM32] 5-1 时钟树(上)
文章目录 前言5-1 时钟树(上)时钟树的基本介绍时钟树的基本结构大树和小树频率运算简介计数器和分频STM32内部结构树的结构于关键节点SYSCLK(System Clock) 系统时钟 72M maxHCLK(AHB Clock) AHB时钟 36M maxPLCK(APB1 Clock) APB1时钟 36M maxPLCK2(APB…...