前言
本文旨在通过Linux系统接口实现网络通信,帮助我们更好地掌握socket套接字的使用。通过学习socket网络通信,我们将发现网络通信的本质不过是套路。接下来,让我们直接进入代码编写部分。
今天我们将模拟实现一个echo demo,即客户端向服务器发送信息,服务器接收并回显这些信息。为了提高代码的可读性和调试性,我们将使用日志信息。我将带领大家手动编写日志代码,并将其应用于echo demo中。在日志中,如果需要访问临界资源,我们需要进行加锁和解锁操作。这里我将引导大家基于Linux系统调用封装锁,使得锁的使用更加便捷。
1.1 Mutex.hpp
想要封装锁,我们首先需要了解锁的概念。简而言之,锁是原子性的操作,用于保护在多线程环境下共享资源的安全。锁的定义有两种方式:一种是使用宏进行全局初始化,无需手动释放,由操作系统自动释放;另一种是局部定义并使用init进行初始化。我们将使用init初始化方法,第一个参数是锁,第二个参数为锁的属性,默认为nullptr。销毁时使用destroy系统调用。我们将这些操作封装在一个LockGuard类中,利用对象的特性,离开局部作用域时自动释放,进一步简化锁的使用。
立即进入“豆包AI人工智官网入口”;
立即学习“豆包AI人工智能在线问答入口”;
#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()
}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;
}
以上就是Linux__之__基于UDP的Socket编程网络通信的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号