【Linux】UDP 网络编程

【Linux】UDP 网络编程

目录

一. 认识相关网络接口

1. socket 套接字

2. sockaddr_in 网络地址结构体

3. bind绑定

4. recvfrom 接收网络数据

5. sendto 发送网络数据

6. ntohs / ntohl 网络字节序转换

7. htons / htonl 主机字节序端口号转换

8. inet_ntoa / inet_ntop 网络字节序IP地址转换

二. Echo Server 实现

1. 前备日志和封装锁

(1)Mutex.hpp

(2)Log.hpp

2. 服务端封装

3. 客户端/服务端运行

(1)服务端

(2)客户端

三. Dict Server 实现

1. 前备日志,封装锁,地址结构体封装

2. 字典封装

3. 服务端封装

4. 客户端/服务端运行

(1)客户端

(2)服务端

四. Chat Server 实现

1. 前备日志,封装锁,地址结构体封装,条件变量封装,线程封装,线程池封装

(1)条件变量封装

(2)线程封装

(3)线程池封装

2. 执行路径封装

3. 服务端封装

4. 服务端/客户端运行

(1)服务端运行

(2)客户端运行


UDP 的使用比较格式化,首先先绑定 socket 套接字,接着进行端口的绑定(一般客户端不需要绑定),然后进行发送或者接收数据,最后关闭 socket 套接字。

一. 认识相关网络接口

1. socket 套接字

socket:

socket 套接字是网络编程的基础,它就类似于网络通信的传递数据的管道,我们可以通过不同的参数来塑造这个管道。

参数:

domain:
指定套接字使用的地址族/协议族,套接字使用的类型决定了数据传输的网络协议层,常见的地址族/协议层

{

 AF_INET:
IPv4 地址族,使用32位IP地址

AF_INET6:IPv6 地址族,使用128位IP地址

AF_UNIX/AF_LOCAL:本地进程间通信,使用文件系统路径作为地址

}

type:
指定套接字类型,决定传输层通信特性

{

SOCK_DGRAM:
数据报套接字(基于UDP协议)

SOCK_STREAM:流式套接字(基于TCP协议)

}

protocol:
指定具体传输层协议,我们通常使用默认,设置为0即可

返回值:

成功返回一个大于等于0的数,失败返回-1

2. sockaddr_in 网络地址结构体

sockaddr_in:

sockaddr_in 用于表示 IPv4 地址结构的核心结构体,主要用于存储和传递 IPv4 协议的地址信息。

参数:

sin_family:
指定结构体存储的地址类型,AF_INET 固定为 IPv4 地址结构

sin_port:指定结构体存储的通信端口号

sin_addr:指定结构体存储的地址

s_addr:将地址转化为网络字节序大端进行存储

3. bind绑定

bind:

bind 的作用是将套接字与特定的 IP port 进行绑定,我们已经将 IP 和端口号写入结构体 struct sockaddr_in 中,我们只需要传递结构体即可

参数:

sockfd:
传递的套接字socket

addr:指向存储 IP 和 port 的结构体指针

addrlen:地址结构体的大小

返回值:

成功返回0,失败返回-1

4. recvfrom 接收网络数据

recvfrom:

主要用于UDP协议接收网络数据报内容,同时获取发送方的地址信息

参数:

sockfd:
套接字描述符

buf:接收数据的数组指针

len:数组的大小

flags:接收方式标志,通常为0,

src_addr:输出参数,指向地址结构体,用于储存发送方的IP和port

addrlen:结构体大小

返回值:

成功返回接收到的字节数,失败返回-1

5. sendto 发送网络数据

sendto:

主要用于UDP协议发送网络数据报内容,同时发送发送方的IP和port

参数:

sockfd:套接字描述符

buf:储存发送信息的数组指针

len:数组的大小

flags:发送方式标志,默认为0

dest_addr:指向结构体存储的接收方的IP和端口号

addrlen:结构体的大小

返回值:

成功返回实际字节数,失败返回-1

6. ntohs / ntohl 网络字节序转换

nthos / ntohl:

nthos ntohl 都是将网络字节序转换为主机字节序,nthos 将 16 位网络字节序转换为 16 位的主机字节序(Network to Host Short),nthol 将 32 位网络字节序转换为 32 位的主机字节序(Network to Host Long)

参数:

netshort / netlong:
需要转换的端口号

7. htons / htonl 主机字节序端口号转换

htons / htonl:

htons htonl 都是将主机字节序转换为网络字节序,htons 将 16 位主机字节序转换为 16 位的网络字节序(Host to Network Short),htonl 将 32 位主机字节序转换为 32 位的网络字节序(Host to Network Long)

参数:

hostshort / hostlong:
需要转换的端口号

8. inet_ntoa / inet_ntop 网络字节序IP地址转换

inet_ntoa / inet_ntop:

将 32位网络字节序的 IPv4 地址转换为点分十进制字符串,inet_ntoa 仅支持 IPv4 在多线程中容易出问题,现代编程更加推荐 inet_ntop

参数:

af:
地址族

src:指向 IP 地址的指针

dst:指向数组的指针用于储存转换后的字符串

size:数组大小

二. Echo Server 实现

主要实现简单的回显服务端和客户端发送的信息

1. 前备日志和封装锁

(1)Mutex.hpp

我们对锁接口进行封装

#pragma once #include <iostream> #include <pthread.h> namespace MutexModule { class Mutex { public: Mutex() { pthread_mutex_init(&_mutex, nullptr); } void Lock() { int n = pthread_mutex_lock(&_mutex); (void)n; } void Unlock() { int n = pthread_mutex_unlock(&_mutex); (void)n; } ~Mutex() { pthread_mutex_destroy(&_mutex); } pthread_mutex_t *Get() { return &_mutex; } private: pthread_mutex_t _mutex; }; class LockGuard { public: LockGuard(Mutex &mutex):_mutex(mutex) { _mutex.Lock(); } ~LockGuard() { _mutex.Unlock(); } private: Mutex &_mutex; }; }

(2)Log.hpp

制作简易的日志用于后续打印日志报告

#ifndef __LOG_HPP__ #define __LOG_HPP__ #include <iostream> #include <cstdio> #include <string> #include <filesystem> //C++17 #include <sstream> #include <fstream> #include <memory> #include <ctime> #include <unistd.h> #include "Mutex.hpp" namespace LogModule { using namespace MutexModule; const std::string gsep = "\r\n"; // 策略模式,C++多态特性 // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入 // 刷新策略基类 class LogStrategy { public: ~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; // "./log/" + "my.log" 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; }; // 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式 // 1. 形成日志等级 enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; 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"; } } std::string GetTimeStamp() { time_t curr = time(nullptr); struct tm curr_tm; localtime_r(&curr, &curr_tm); char timebuffer[128]; snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d", curr_tm.tm_year+1900, curr_tm.tm_mon+1, curr_tm.tm_mday, curr_tm.tm_hour, curr_tm.tm_min, curr_tm.tm_sec ); return timebuffer; } // 1. 形成日志 && 2. 根据不同的策略,完成刷新 class Logger { public: Logger() { EnableConsoleLogStrategy(); } void EnableFileLogStrategy() { _fflush_strategy = std::make_unique<FileLogStrategy>(); } void EnableConsoleLogStrategy() { _fflush_strategy = std::make_unique<ConsoleLogStrategy>(); } // 表示的是未来的一条日志 class LogMessage { public: LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger) : _curr_time(GetTimeStamp()), _level(level), _pid(getpid()), _src_name(src_name), _line_number(line_number), _logger(logger) { // 日志的左边部分,合并起来 std::stringstream ss; ss << "[" << _curr_time << "] " << "[" << Level2Str(_level) << "] " << "[" << _pid << "] " << "[" << _src_name << "] " << "[" << _line_number << "] " << "- "; _loginfo = ss.str(); } // LogMessage() << "hell world" << "XXXX" << 3.14 << 1234 template <typename T> LogMessage &operator<<(const T &info) { // a = b = c =d; // 日志的右半部分,可变的 std::stringstream ss; ss << info; _loginfo += ss.str(); return *this; } ~LogMessage() { if (_logger._fflush_strategy) { _logger._fflush_strategy->SyncLog(_loginfo); } } private: std::string _curr_time; LogLevel _level; pid_t _pid; std::string _src_name; int _line_number; std::string _loginfo; // 合并之后,一条完整的信息 Logger &_logger; }; // 这里故意写成返回临时对象 LogMessage operator()(LogLevel level, std::string name, int line) { return LogMessage(level, name, line, *this); } ~Logger() { } private: std::unique_ptr<LogStrategy> _fflush_strategy; }; // 全局日志对象 Logger logger; // 使用宏,简化用户操作,获取文件名和行号 #define LOG(level) logger(level, __FILE__, __LINE__) #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() } #endif

2. 服务端封装

服务端封装划分为服务端初始化和服务端启动,初始化阶段需要进行套接字创建,地址结构体初始化,再将套接字与结构体进行绑定。启动阶段创建缓冲区,不断的接受客户端传递的数据,在服务端打印后,再将数据传回给客户端。

下面是服务端代码

#include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <functional> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include "Log.hpp" using namespace std; using namespace LogModule; const int defaultnum = -1; using func_t = function<string(const string &)>; class UdpServer { public: UdpServer(uint16_t port, func_t func) : _isrunning(false), _port(port), _func(func), _socket(defaultnum) { } void Init() { _socket = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议 UDP模式 默认值 if (_socket < 0) { LOG(LogLevel::FATAL) << "socket error"; exit(1); } LOG(LogLevel::INFO) << "socket success" << _socket; // 标识符为3 012被占用 struct sockaddr_in local; // IPv4 网络地址结构体 bzero(&local, sizeof(local)); // 清空结构体 local.sin_family = AF_INET; // A=Address, F=Family, IN=Internet),表示使用 IPv4 协议; local.sin_port = htons(_port); // hton 大端序 shost = 主机,network = 网络,short=16 位整数 local.sin_addr.s_addr = INADDR_ANY; // 统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0) // 这里为什么服务端要 bind !!!! // 服务端需要稳定固定的地址,但客户端用临时地址即可 int n = bind(_socket, (struct sockaddr *)&local, sizeof(local)); if (n < 0) { LOG(LogLevel::FATAL) << "bind error"; exit(2); } LOG(LogLevel::INFO) << "bind success,socket:" << socket; } void Start() { _isrunning = true; while (_isrunning) { char buffer[1024]; // 接收客户端数据 struct sockaddr_in peer; // IPv4 数据接收结构体 socklen_t len = sizeof(peer); // 接收完客户端信息,在进行发送 ssize_t s = recvfrom(_socket, &buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len);//接收客户端信息,buffer数组最后一位要0 if (s > 0) { int peer_port = ntohs(peer.sin_port); // string peer_ip = inet_ntoa(peer.sin_addr); //将 IP转换为string类格式 buffer[s] = 0; string result = _func(buffer); //将信息执行函数 cout<<result<<endl; sendto(_socket,&buffer,result.size(),0,(sockaddr*)&peer,len); //发送回给客户端 } } } ~UdpServer() { } private: bool _isrunning; int _socket; uint16_t _port; func_t _func; };

3. 客户端/服务端运行

(1)服务端

进行基础信息的创建调用函数即可

#include <iostream> #include <memory> #include "UdpServer.hpp" string fun_c(const string&kk) { string a = "hello,"; a += kk; return a; } // 输入 ./UdpServer port int main(int argc, char *argv[]) { if (argc != 2) { cerr << "Usages:" << argv[0] << " port" << endl; return 1; } uint16_t _port = stoi(argv[1]); Enable_Console_Log_Strategy(); unique_ptr<UdpServer> Udp = make_unique<UdpServer>(_port, fun_c); Udp->Init(); Udp->Start(); return 0; }

(2)客户端

客户端首先创建自己的地址结构体,进行地址 IP 和 port 初始化,接着创建字符串进行输入,发送给服务端,接着创建缓冲区接收服务端发送回来的信息。

#include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <functional> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> using namespace std; // 客户端 // 输入 ./UdpClient ip port int main(int argc, char *argv[]) { // 先通过输入拿到 ip 和 port if (argc != 3) { cerr << "Usage:" << argv[0] << " Client_ip Client_port" << endl; return 1; } uint16_t _port = stoi(argv[2]); string _ip = argv[1]; // 选定网络协议 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { cerr << "socket error" << endl; return 2; } cout << "Client socket success" << endl; // 初始化发送信息的结构体信息 struct sockaddr_in Client; bzero(&Client, sizeof(Client)); Client.sin_family = AF_INET; Client.sin_port = htons(_port); Client.sin_addr.s_addr = inet_addr(_ip.c_str()); while (true) { // 输入信息发送结构体 string message; cout << "Please Enter:"; getline(cin, message); sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr *)&Client, sizeof(Client)); // 接收返回信息 char buffer[1024]; struct sockaddr_in peer; socklen_t len; int n = recvfrom(sockfd, &buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len); if (n > 0) { buffer[n] = 0; cout << "recvfrom success:" << buffer << endl; } } return 0; }

三. Dict Server 实现

1. 前备日志,封装锁,地址结构体封装

这里的日志和锁封装就不再添加,详细参考上面的代码。

我们这里来看看地址结构的封装

地址结构体封装接收 IP 和 port 并进行32位网络字节序转换以及端口号网络字节序转换为主机字节序

#pragma once #include <iostream> #include <string> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> using namespace std; 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; } string Ip() { return _ip; } ~InetAddr() { } private: uint16_t _port; string _ip; struct sockaddr_in _addr; };

2. 字典封装

字典主要实现两个功能,一个是“下载字典”,将字典的内容打印到屏幕上,第二个功能是进行翻译,输入中文返回英文。

首先我们要对字典文本进行解析,以:为分隔,对左右两边的字符串进行截取并放入到字典数据结构中。翻译功能找到对应的单词打印即可

#pragma once #include <istream> #include <iostream> #include <unordered_map> #include <string> #include "Log.hpp" #include "InetAddr.hpp" using namespace std; using namespace LogModule; const string defpath = "./dictionary.txt"; const string sep = ":"; class Dict { public: Dict(const string &path = defpath) : _dict_path(path) { } bool LoadDict() { ifstream in(_dict_path); if (!in.is_open()) { LOG(LogLevel::DEBUG) << "打开字典:" << _dict_path << " 失败"; return false; } LOG(LogLevel::INFO) << "打开字典:" << _dict_path << " 成功"; string line; while (getline(in, line)) { auto pos = line.find(sep); if (pos == string::npos) { LOG(LogLevel::WARNING) << "字典格式错误"; continue; } auto english = line.substr(0, pos); auto chinese = line.substr(pos + sep.size()); if (chinese.empty() || english.empty()) { LOG(LogLevel::WARNING) << "字典识别错误:" << line; continue; } _dict.insert(make_pair(english, chinese)); LOG(LogLevel::INFO) << "加载" << line; } in.close(); return true; } string translate(const string &word, InetAddr &client) { auto iter = _dict.find(word); if (iter == _dict.end()) { LOG(LogLevel::DEBUG) << "进入翻译[" << client.Ip() << "," << client.Port() << "]#" << word << "->None"; return "unknown"; } LOG(LogLevel::INFO) << "进入翻译[" << client.Ip() << "," << client.Port() << "]#" << word << "->" << iter->second; return iter->first + "->" + iter->second; } ~Dict() { } private: string _dict_path; unordered_map<string, string> _dict; };

3. 服务端封装

服务端执行逻辑和上面的服务端逻辑一致,只是将端口和IP封装成了一个类进行处理

#pragma once #include <iostream> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <functional> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include "Log.hpp" #include "InetAddr.hpp" using namespace std; using namespace LogModule; using func_t = function<string(const string &, InetAddr &)>; const int defaultnum = -1; class UdpServer { public: UdpServer(uint16_t port, func_t func) : _port(port), _isrunning(false), _func(func), _sockfd(defaultnum) { } void Init() { _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "fail to socket"; exit(1); } LOG(LogLevel::INFO) << "sucess to socket,socket: " << _sockfd; 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; int m = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); if (m < 0) { LOG(LogLevel::FATAL) << "bind error"; exit(2); } LOG(LogLevel::INFO) << "bind success, socket:" << _sockfd; } void Start() { _isrunning = true; while (_isrunning) { char buffer[1024]; struct sockaddr_in peer; socklen_t len = sizeof(peer); ssize_t s = recvfrom(_sockfd, &buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len); if (s > 0) { InetAddr client(peer); buffer[s] = 0; string result = _func(buffer, client); sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len); } } } ~UdpServer() { } private: uint16_t _port; bool _isrunning; func_t _func; int _sockfd; };

4. 客户端/服务端运行

(1)客户端

#include <iostream> #include <string> #include <cstring> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> using namespace std; // ./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. 创建socket int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd < 0) { std::cerr << "socket error" << std::endl; return 2; } // 2. 本地的ip和端口是什么?要不要和上面的“文件”关联呢? // 问题:client要不要bind?需要bind. // client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式 // 为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突 // client端的端口号是几,不重要,只要是唯一的就行! // 填写服务器信息 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()); while(true) { std::string input; std::cout << "Please Enter# "; std::getline(std::cin, input); int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server)); (void)n; 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; }

(2)服务端

服务端需要增加字典结构

#include <iostream> #include <memory> #include "Dict.hpp" // 翻译的功能 #include "UdpServer.hpp" // 网络通信的功能 // ./UdpServer port int main(int argc, char *argv[]) { if (argc != 2) { cerr << "Usage: " << argv[0] << " port" << endl; return 1; } uint16_t port = stoi(argv[1]); Enable_Console_Log_Strategy(); Dict dict; dict.LoadDict(); unique_ptr<UdpServer> udp = make_unique<UdpServer>(port, [&dict](const string &word, InetAddr addr) { return dict.translate(word, addr); }); udp->Init(); udp->Start(); return 0; }

四. Chat Server 实现

1. 前备日志,封装锁,地址结构体封装,条件变量封装,线程封装,线程池封装

日志,锁,地址结构在上文已经完成,可对上文进行参考

(1)条件变量封装

#pragma once #include <iostream> #include <pthread.h> #include "Mutex.hpp" using namespace MutexModule; namespace CondModule { class Cond { public: Cond() { pthread_cond_init(&_cond, nullptr); } void Wait(Mutex &mutex) { int n = pthread_cond_wait(&_cond, mutex.Get()); (void)n; } void Signal() { // 唤醒在条件变量下等待的一个线程 int n = pthread_cond_signal(&_cond); (void)n; } void Broadcast() { // 唤醒所有在条件变量下等待的线程 int n = pthread_cond_broadcast(&_cond); (void)n; } ~Cond() { pthread_cond_destroy(&_cond); } private: pthread_cond_t _cond; }; };

(2)线程封装

#ifndef _THREAD_H_ #define _THREAD_H_ #include <iostream> #include <string> #include <pthread.h> #include <cstdio> #include <cstring> #include <functional> namespace ThreadModlue { static uint32_t number = 1; // bug class Thread { using func_t = std::function<void()>; // 暂时这样写,完全够了 private: void EnableDetach() { _isdetach = true; } void EnableRunning() { _isrunning = true; } static void *Routine(void *args) // 属于类内的成员函数,默认包含this指针! { Thread *self = static_cast<Thread *>(args); self->EnableRunning(); if (self->_isdetach) self->Detach(); pthread_setname_np(self->_tid, self->_name.c_str()); self->_func(); // 回调处理 return nullptr; } // bug public: Thread(func_t func) : _tid(0), _isdetach(false), _isrunning(false), res(nullptr), _func(func) { _name = "thread-" + std::to_string(number++); } void Detach() { if (_isdetach) return; if (_isrunning) pthread_detach(_tid); EnableDetach(); } bool Start() { if (_isrunning) return false; int n = pthread_create(&_tid, nullptr, Routine, this); if (n != 0) { return false; } else { return true; } } bool Stop() { if (_isrunning) { int n = pthread_cancel(_tid); if (n != 0) { return false; } else { _isrunning = false; return true; } } return false; } void Join() { if (_isdetach) { return; } int n = pthread_join(_tid, &res); if (n != 0) { } else { } } pthread_t Id() { return _tid; } ~Thread() { } private: pthread_t _tid; std::string _name; bool _isdetach; bool _isrunning; void *res; func_t _func; }; } #endif

(3)线程池封装

#pragma once #include <iostream> #include <string> #include <vector> #include <queue> #include "Log.hpp" #include "Thread.hpp" #include "Cond.hpp" #include "Mutex.hpp" // .hpp header only namespace ThreadPoolModule { using namespace ThreadModlue; using namespace LogModule; using namespace CondModule; using namespace MutexModule; static const int gnum = 5; template <typename T> class ThreadPool { private: void WakeUpAllThread() { LockGuard lockguard(_mutex); if (_sleepernum) _cond.Broadcast(); LOG(LogLevel::INFO) << "唤醒所有的休眠线程"; } void WakeUpOne() { _cond.Signal(); //LOG(LogLevel::INFO) << "唤醒一个休眠线程"; } ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleepernum(0) { for (int i = 0; i < num; i++) { _threads.emplace_back( [this]() { HandlerTask(); }); } } void Start() { if (_isrunning) return; _isrunning = true; for (auto &thread : _threads) { thread.Start(); // LOG(LogLevel::INFO) << "start new thread success: " << thread.Name(); } } ThreadPool(const ThreadPool<T> &) = delete; ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; public: static ThreadPool<T> *GetInstance() { if (inc == nullptr) { LockGuard lockguard(_lock); LOG(LogLevel::DEBUG) << "获取单例...."; if (inc == nullptr) { LOG(LogLevel::DEBUG) << "首次使用单例, 创建之...."; inc = new ThreadPool<T>(); inc->Start(); } } return inc; } void Stop() { if (!_isrunning) return; _isrunning = false; // 唤醒所有的线层 WakeUpAllThread(); } void Join() { for (auto &thread : _threads) { thread.Join(); } } void HandlerTask() { char name[128]; pthread_getname_np(pthread_self(), name, sizeof(name)); while (true) { T t; { LockGuard lockguard(_mutex); // 1. a.队列为空 b. 线程池没有退出 while (_taskq.empty() && _isrunning) { _sleepernum++; _cond.Wait(_mutex); _sleepernum--; } // 2. 内部的线程被唤醒 if (!_isrunning && _taskq.empty()) { LOG(LogLevel::INFO) << name << " 退出了, 线程池退出&&任务队列为空"; break; } // 一定有任务 t = _taskq.front(); // 从q中获取任务,任务已经是线程私有的了!!! _taskq.pop(); } t(); // 处理任务,需/要在临界区内部处理吗?1 0 } } bool Enqueue(const T &in) { if (_isrunning) { LockGuard lockguard(_mutex); _taskq.push(in); if (_threads.size() == _sleepernum) WakeUpOne(); return true; } return false; } ~ThreadPool() { } private: std::vector<Thread> _threads; int _num; // 线程池中,线程的个数 std::queue<T> _taskq; Cond _cond; Mutex _mutex; bool _isrunning; int _sleepernum; // bug?? static ThreadPool<T> *inc; // 单例指针 static Mutex _lock; }; template <typename T> ThreadPool<T> *ThreadPool<T>::inc = nullptr; template <typename T> Mutex ThreadPool<T>::_lock; } 

2. 执行路径封装

我们实现群聊的逻辑是服务端接收到客户端发来的信息,将信息添加上发送者的信息之后,再打包发送给每一个群聊用户,这样就实现了群聊。

原理类似,我们用 vector 容器存储地址结构体,从地址结构体当中提取出发送的信息,接着把信息发送给容器中每一个成员。同时为了解决线程并发冲突问题这里引入了锁

#pragma once #include <iostream> #include <vector> #include "Log.hpp" #include "InetAddr.hpp" #include "Mutex.hpp" using namespace std; using namespace LogModule; using namespace MutexModule; class Route { private: bool IsExit(InetAddr &peer) { for (auto &e : _online_user) { if (e == peer) { return true; } } return false; } void User_Add(InetAddr &peer) { LOG(LogLevel::INFO) << "添加一个聊天用户"; _online_user.push_back(peer); } void User_Delete(InetAddr &peer) { //LOG(LogLevel::DEBUG) << "删除一个用户"; for (auto iter = _online_user.begin(); iter < _online_user.end(); iter++) { if (*iter == peer) { LOG(LogLevel::INFO) << "成功删除用户:" << peer.StringAddr(); _online_user.erase(iter); break; } } } public: Route() { } void MessageRoute(int _sockfd, const string &messages, InetAddr &addr) { LockGuard lockguard(_lock); if (!IsExit(addr)) { User_Add(addr); } string result_messeage = "[" + addr.StringAddr() + "]" + messages; LOG(LogLevel::INFO)<<result_messeage; for (auto &e : _online_user) { sendto(_sockfd, result_messeage.c_str(), result_messeage.size(), 0, (const struct sockaddr *)&e.Addr(), sizeof(e.Addr())); } if (messages == "QUIT") { LOG(LogLevel::INFO) << "删除一个用户" << addr.StringAddr(); User_Delete(addr); } } ~Route() { } private: vector<InetAddr> _online_user; Mutex _lock; };

3. 服务端封装

服务端处与原先逻辑类似

#pragma once #include <iostream> #include <string> #include <functional> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "Log.hpp" #include "InetAddr.hpp" using namespace LogModule; using func_t = std::function<void(int sockfd, const std::string&, InetAddr&)>; const int defaultfd = -1; // 你是为了进行网络通信的! class UdpServer { public: UdpServer(uint16_t port, func_t func) : _sockfd(defaultfd), // _ip(ip), _port(port), _isrunning(false), _func(func) { } void Init() { // 1. 创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "socket error!"; exit(1); } LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd; // 2. 绑定socket信息,ip和端口,ip(比较特殊,后续解释) // 2.1 填充sockaddr_in结构体 struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; // 我会不会把我的IP地址和端口号发送给对方? // IP信息和端口信息,一定要发送到网络! // 本地格式->网络序列 local.sin_port = htons(_port); // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp); //local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO local.sin_addr.s_addr = INADDR_ANY; // InetAddr addr("0", _port); // addr.NetAddr(); // 那么为什么服务器端要显式的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) { char buffer[1024]; struct sockaddr_in peer; socklen_t len = sizeof(peer); // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。 ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (s > 0) { InetAddr client(peer); buffer[s] = 0; // TODO _func(_sockfd, buffer, client); // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的?? // 2. 发消息 // std::string echo_string = "server echo@ "; // echo_string += buffer; // sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len); } } } ~UdpServer() { } private: int _sockfd; uint16_t _port; // std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1" bool _isrunning; func_t _func; // 服务器的回调函数,用来进行对数据进行处理 };

4. 服务端/客户端运行

(1)服务端运行

此处我们引入线程池,为的是解决信息的传递和接收无法并发的问题,我们将任务放到线程池当中,由线程池来分配线程来执行接收发送的工作。服务端的逻辑也很简单,就是将数据打包成任务,放到线程池中。

#include <iostream> #include <memory> #include "Route.hpp" #include "ChatServer.hpp" // 网络通信的功能 #include "ThreadPool.hpp" using namespace ThreadPoolModule; using task_t = function<void()>; // ./Chat_Server port int main(int argc, char *argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " port" << std::endl; return 1; } // std::string ip = argv[1]; uint16_t port = std::stoi(argv[1]); Enable_Console_Log_Strategy(); Route r; auto tp = ThreadPool<task_t>::GetInstance(); // bind 绑定函数,第一个参数为调用方法,第二个参数是实体,剩下的参数是传递到第一个参数的参数 unique_ptr<UdpServer> udp = make_unique<UdpServer>(port,[&r,&tp] (int sockfd, const std::string& messages, InetAddr& addr){ task_t t = std::bind(&Route::MessageRoute,&r, sockfd, messages, addr); tp->Enqueue(t); }); udp->Init(); udp->Start(); return 0; }

(2)客户端运行

客户端的操作无疑是接收信息和发送信息,我们将接受信息发送信息打包成两个线程进行管理

#include <iostream> #include <string> #include <cstring> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include "Thread.hpp" int sockfd = 0; std::string server_ip; uint16_t server_port = 0; pthread_t id; using namespace ThreadModlue; void Recv() { while (true) { 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::cerr << buffer << std::endl; // 2 } } } void Send() { 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()); const std::string online = "inline"; sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server)); while (true) { std::string input; std::cout << "Please Enter# "; // 1 std::getline(std::cin, input); // 0 int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server)); (void)n; if (input == "QUIT") { pthread_cancel(id); break; } } } // ./Chat_Client ip port int main(int argc, char *argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl; return 1; } server_ip = argv[1]; server_port = std::stoi(argv[2]); // 1. 创建socket sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { std::cerr << "socket error" << std::endl; return 2; } Thread recv(Recv); Thread send(Send); recv.Start(); send.Start(); id = send.Id(); recv.Join(); send.Join(); return 0; }

感谢各位观看,请大佬们多多支持!!!

Read more

Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲

Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲

Spring Framework 作为 Java 生态中最流行的企业级开发框架,提供了丰富的模块化支持。其中,Spring Web 模块是支撑 Web 开发的基础组件,无论是传统的 MVC 应用,还是 REST API 及微服务架构,都离不开它的核心能力。 本篇文章将深入解析 Spring Web 模块的核心概念、依赖关系、作用及关键组件,并通过实际案例展示如何使用 Spring Web 进行 RESTful API 调用。本文力求内容精炼、干货满满,帮助你掌握 Spring Web 的核心技术点。 文章目录 * 1、Spring-Web 模块介绍 * 1.1、Spring-Web 模块概述 * 1.2、Spring-Web

By Ne0inhk
《Web 自动化测试入门:从概念到百度搜索实战全拆解》

《Web 自动化测试入门:从概念到百度搜索实战全拆解》

一、自动化的核心概念 1. 定义:通过自动方式替代人工操作完成任务,生活中常见案例(自动洒水机、自动洗手液、超市闸机)体现了 “减少人力消耗、提升效率 / 质量” 的特点。 2. 软件自动化测试的核心目的: * 用于回归测试:软件迭代新版本时,验证新增功能是否影响历史功能的正常运行。 3. 常见面试题解析: * 自动化测试不能完全取代人工测试:需人工编写脚本,且功能变更后需维护更新,可靠性未必优于人工。 * 自动化测试不能 “大幅度降低工作量”:仅能 “一定程度” 减少重复工作,需注意表述的严谨性。 二、自动化测试的分类 自动化是统称,包含多种类型,核心分类及说明如下: 分类说明接口自动化针对软件接口的测试,目的是验证接口的功能、性能、稳定性等。UI 自动化 针对软件界面的测试,包含: 1. 移动端自动化:通过模拟器在电脑上编写脚本,测试手机应用;稳定性较差(受设备、

By Ne0inhk

如何高效调用Qwen3-VL?这个WEBUI镜像让你事半功倍

如何高效调用Qwen3-VL?这个WEBUI镜像让你事半功倍 在多模态AI迅速演进的今天,开发者面临的最大挑战已不再是“有没有模型可用”,而是“能否快速、低成本地将模型集成到实际业务中”。尽管许多视觉-语言大模型(VLM)在技术指标上表现惊艳,但复杂的部署流程、高昂的硬件门槛和漫长的环境配置,往往让大多数团队望而却步。 而 Qwen3-VL-WEBUI 镜像的出现,彻底改变了这一局面。作为阿里开源的一站式多模态推理解决方案,它内置了强大的 Qwen3-VL-4B-Instruct 模型,封装了完整的运行时环境与交互界面,真正实现了“一键启动、开箱即用”的极致体验。无需拉代码、不需手动安装依赖、不必配置GPU驱动——你只需要一个支持Docker的环境,就能在几分钟内拥有自己的多模态AI助手。 这不仅是一次技术升级,更是一种使用范式的跃迁:从“工程部署”走向“服务调用”。 为什么你需要 Qwen3-VL-WEBUI? 传统方式调用多模态模型通常涉及以下步骤: 1. 下载模型权重(数十GB) 2. 安装PyTorch、Transformers等深度学习框架 3. 编写推理

By Ne0inhk
WebGIS开发实战:WKT转GeoJSON的多种技巧与Leaflet加载应用详解

WebGIS开发实战:WKT转GeoJSON的多种技巧与Leaflet加载应用详解

目录 前言 一、WKT后台转换实现 1、基于PostGIS实现 2、GeoTools实现 二、wellknown.js转换 1、wellknown.js是什么? 2、wellknown.js的方法 三、在Leaflet.js中集成wellknow.js 1、资源引入 2、将wkt转为geojson 四、总结 前言         在当今数字化浪潮中,地理信息系统(GIS)技术正以前所未有的速度融入我们的生活与工作。从城市规划到环境监测,从物流配送到旅游出行,地理空间数据的价值日益凸显。而 WebGIS,作为 GIS 技术与 Web 技术的深度融合,更是为地理信息的共享与交互开辟了广阔天地。它让地理数据能够通过网络在各种终端设备上轻松呈现,极大地拓展了 GIS 的应用场景和受众群体。然而,在 WebGIS

By Ne0inhk