【Linux网络编程】UDP套接字编程:系统编程组件、基础回显服务、InetAddr与地址转换、翻译服务与线程池聊天室

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言
我们了解完计算机网络基础,知道了 IP 加端口号才能定位到一台主机上的那一个进程,下面就用Linux网络编程里的 UDP 套接字,把这份认识落到一份能跑起来的代码上。
刚学完网络基础面对真正的 socket 代码可能会有这些疑问:一个 UDP 服务端到底要填哪些参数才能收消息?客户端明明没写 bind,回复又是怎么送回去的?
| 现象 | 你可能以为的原因 | 真正发生的事情 |
|---|---|---|
| 服务端能收到消息 | 把 IP 写死在结构体里就行 | 绑 INADDR_ANY 才能监听本机所有网卡 |
| 客户端没 bind 也能通信 | 客户端不需要地址 | 首次 sendto 时 OS 自动分配 IP 和随机端口 |
| 消息能原样回显 | recvfrom 只负责拿数据 | 它顺带带回了对端的 sockaddr_in,回发全靠这个地址 |
| 换个功能就要重写服务端 | 收发逻辑得跟着业务一起改 | 用 std::function 把业务从网络层剥出去,外面传什么就做什么 |
| 多人聊天室不卡 | 一个单线程循环就够用 | 线程池接任务,客户端收和发各起一个线程 |
问题来了: 地址为什么要在字符串和整数之间转来转去?
inet_ntoa为什么不能连着用两次?一个回显服务器又是怎么一步步变成翻译服务器、再变成聊天室的?
答案就是------socket + bind + recvfrom/sendto 这套固定骨架!先把地址填对、把端口绑上,剩下的业务全交给外部传进来的函数;再往上加一层 InetAddr 封装和线程池,同一份 UdpServer 就能从回显走到翻译,最后走到多人聊天室。
通过本文,你将掌握:
| 技能 | 应用场景 |
|---|---|
| Log / Mutex / Thread / Cond / ThreadPool | 复用日志、互斥锁、线程与线程池,把系统编程的轮子直接搬过来 |
| socket、bind、recvfrom、sendto | 写出第一个能收能回的 UDP 服务端与客户端 |
| INADDR_ANY 与网络字节序 | 多网卡时不漏收消息,看懂 htons / ntohs 在转什么 |
| 客户端隐式 bind | 明白为什么客户端不用自己绑端口,OS 在首次发送时就办好了 |
| InetAddr 与 inet_pton / inet_ntop | 把地址转换和强转收进一个类,避开 inet_ntoa 静态缓冲区的坑 |
| std::function 与 lambda 分层 | 换业务不改网络代码,做出英汉翻译服务 |
| 线程池 + Route 广播 | 管理在线用户,把一条消息分发给所有人 |
| 客户端收发双线程 | 让「等输入」和「等消息」互不阻塞 |
前置知识: C++ 基础语法与 lambda、Linux 系统编程里的线程与互斥、计算机网络基础里的 IP / 端口 / 网络字节序。
文章目录
- 【Linux网络编程】UDP套接字编程:系统编程组件、基础回显服务、InetAddr与地址转换、翻译服务与线程池聊天室
- 正文:
- 系统编程的一些实现
- [基础echo message](#基础echo message)
- 简单的翻译功能
-
- 一、Udpserver.hpp_翻译版
-
- inet_aton、inet_pton和inet_ntoa和inet_ntop的安全性和区别
- [1. 先按方向分组](#1. 先按方向分组)
- [2. 逐个看用法与坑](#2. 逐个看用法与坑)
- [3. 安全性对比小结](#3. 安全性对比小结)
- [4. 落到本文的 `InetAddr` 类](#4. 落到本文的
InetAddr类)
- 二、Udpserver.cpp_翻译版
- 三、Udpclient.cpp_翻译版
- 简单的聊天室功能
正文:
系统编程的一些实现
在实现网络通信功能之前,先把系统编程里面实现的一些东西拿出来作为前置了解,此处不多做讲解,想要了解如何实现的可以看【Linux系统编程】的文章。
Log.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <filesystem>
#include <sstream>
#include <fstream>
#include <memory>
#include <unistd.h>
#include <ctime>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &massage) = 0;
};
class ConsoleLogStrategy : public LogStrategy
{
public:
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 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;
};
///////////////////////////////////////////////////////////
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LeveltoStr(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;
}
class Logger
{
public:
Logger()
{
EnableConsoleStrategy();
}
void EnableFileStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LogLevel &level, std::string src_name, int line_number, Logger &logger)
: _level(level), _curr_time(GetTimeStamp()), _pid(getpid()), _src_name(src_name), _line_number(line_number), _logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "]"
<< "[" << LeveltoStr(level) << "]"
<< "[" << _pid << "]"
<< "[" << _src_name << "]"
<< "[" << _line_number << "]"
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _curr_time;
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.EnableConsoleStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileStrategy()
}
Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#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;
}
pthread_mutex_t *GetMutex()
{
return &_mutex;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex) : _mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex &_mutex; // 传的是引用 → 守卫操作的就是同一把锁
};
}
Thread.hpp
cpp
#pragma once
#include <pthread.h>
#include <iostream>
#include <string>
#include <functional>
namespace ThreadModule
{
static uint32_t number = 1;
class Thread
{
using func_t = std::function<void()>;
private:
void EnableRunning()
{
_isRunning = true;
}
void EnableDetach()
{
_isDetached = true;
}
static void *Routine(void *args) // 需要执行的操作是利用传入的this指针使用传入的函数
{
Thread *self = static_cast<Thread *>(args);
self->EnableRunning();
if (self->_isDetached)
self->Detach();
pthread_setname_np(self->_tid, self->_name.c_str());
self->_func();
return nullptr; // 不传递任何信息,本意指的是join捕获的类似退出码
}
public:
Thread(func_t func) // 传入函数
: _tid(0),
_isRunning(false),
_isDetached(false),
res(nullptr),
_func(func)
{
_name = "Thread-" + std::to_string(number++);
}
std::string Getname()
{
return _name;
}
bool Start()
{
if (_isRunning)
return false;
int n = pthread_create(&_tid, nullptr, Routine, (void *)this);
if (n == 0)
return true;
else
return false;
}
void Detach()
{
if (_isDetached)
return;
if (_isRunning)
pthread_detach(_tid);
EnableDetach();
}
bool Stop()
{
if (_isRunning)
{
int n = pthread_cancel(_tid);
if (n == 0)
{
_isRunning = false;
return true;
}
else
return false;
}
else
return false;
}
void Join()
{
if(_isDetached)
return;
int n = pthread_join(_tid, &res);
if(n == 0)
std::cout << "join回收成功" << std::endl;//
else
std::cout << "join失败" << std::endl;
}
int Id()
{
return _tid;
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
bool _isRunning;
bool _isDetached;
void *res;
func_t _func;
};
}
Cond.hpp
cpp
#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.GetMutex());
(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;
};
}
ThreadPool.hpp
cpp
#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 ThreadModule;
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;
}
基础echo message
一、Udpserver.hpp_基础echo版
该服务端想实现一个收到客户端消息并回显的功能
头文件以及命名空间使用
cpp
// 四个socket的经典头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
///////////////////////////
#include <iostream>
#include <string>
#include "Log.hpp"// 我们要在使用过程中用日志打印调试信息
using namespace LogModule;
Udpserver类
cpp
class UdpServer
{
// 先来解释下成员变量
private:
// 在实现回显功能的过程要进行socket,为了所有客户端都能找到服务端,需要ip和port
// 当然一个服务端可以绑定多个网卡,其实就是多个进程,那这种情况怎么办呢?我们下面来改进
// _sockfd作为返回值也要放在成员变量里面
uint16_t _port;
std::string _ip; // 点分十进制
size_t _sockfd;
// 判断是否在运行
bool _running;
public:
UdpServer(std::string ip, uint16_t port)
: _ip(ip), _port(port)
{
}
void Init()
{
// 1.打开套接字(网络文件)
size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// AF_INET 和 SOCK_DGRAM就可以确定是 udp 协议了
// 网络 面向数据报
if (sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket fail";
exit(1);
}
_sockfd = sockfd;
LOG(LogLevel::INFO) << "socket success";
// 2.bind
// 填写结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 将结构体清零,也可使用 memset
local.sin_port = htons(_port); // 转成网络字节序
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
// inet_addr 用来让c字符串型参数变成整型
// 手动实现:先分离字符串,然后直接把 4 个字节按"192、168、1、1"的顺序存进结构体------因为 IP 的网络序本来就是"高位字节在低地址",直接按顺序摆字节就是网络序。具体实现看下面代码块
local.sin_family = AF_INET;
// 确定地址族
// bind
// int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
// 绑定问题->ip问题
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind fail";
exit(2);
}
LOG(LogLevel::INFO) << "bind success";
}
void Strat()
{
// 收发消息
_running = true;
while (_running)
{
// 收消息
char buffer[1024];
struct sockaddr_in peer; // 用来接收客户端的socket信息
socklen_t len = sizeof(peer);
int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 因为这里最后一个参数是要即作为输入型参数,又作为输出型参数的,传地址
if (s < 0)
{
uint16_t peer_port = ntohs(peer.sin_port); // 转换到本地字节序
std::string peer_ip = inet_ntoa(peer.sin_addr); // 转换成字符串,方便查看
buffer[s] = 0;
// 这里可以做封装分层来实现其他功能,后续再改动
LOG(LogLevel::INFO) << "[" << peer_ip << ":" << peer_port << "]" << buffer;
// 发消息
std::string echo_string = "server echo@";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
};
- 这是一段用来手动实现本地ip转网络的实现代码。
cpp
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>
// ========== 核心思想:按顺序存字节的结构体 ==========
// 注意字段顺序:a 是 IP 的最高位字节(192),d 是最低位字节(1)
struct ip_bytes
{
uint8_t a; // 第1段: 192 (网络序中位于最低地址/最高位)
uint8_t b; // 第2段: 168
uint8_t c; // 第3段: 1
uint8_t d; // 第4段: 1
};
// ========== 方法:分离 -> 存入结构体 -> char* 强转读出 ==========
uint32_t manual_inet_addr(const char *ip_str)
{
unsigned int a, b, c, d;
if (sscanf(ip_str, "%u.%u.%u.%u", &a, &b, &c, &d) != 4)
return INADDR_NONE;
if (a > 255 || b > 255 || c > 255 || d > 255)
return INADDR_NONE;
// 把分离出的四段按顺序放进结构体
struct ip_bytes ip;
ip.a = a;
ip.b = b;
ip.c = c;
ip.d = d;
// 关键一步:把结构体强转成 uint32_t*
// 结构体在内存中就是连续的4字节:a b c d
// 而 sin_addr 需要的正是内存中按 a b c d 排列的字节!
uint32_t result;
memcpy(&result, &ip, sizeof(result)); // 等价于 *(uint32_t*)&ip,但更规范
return result;
}
int main(void)
{
const char *ip = "192.168.1.1";
uint32_t mine = manual_inet_addr(ip);
uint32_t sys = inet_addr(ip);
printf("IP: %s\n", ip);
printf("手动(结构体+强转): 0x%08X\n", mine);
printf("系统 inet_addr : 0x%08X\n", sys);
printf("一致: %s\n\n", mine == sys ? "是 ✅" : "否 ❌");
// ---------- 用 char* 验证内存布局 ----------
printf("强转 uint32_t 后的内存 (低地址 -> 高地址):\n ");
char *p = (char *)&mine;
for (int i = 0; i < 4; i++)
printf("%02X ", p[i]);
// 小端机器上会打印: 01 01 A8 C0 ------ 因为整数本身按小端存
printf("\n\n结构体 ip_bytes 的内存 (低地址 -> 高地址):\n ");
struct ip_bytes ip2;
sscanf(ip, "%u.%u.%u.%u", &ip2.a, &ip2.b, &ip2.c, &ip2.d);
char *q = (char *)&ip2;
for (int i = 0; i < 4; i++)
printf("%02X ", q[i]);
// 打印: C0 A8 01 01 ------ 恰好就是网络序的字节排列!
// ---------- 直接填进 sockaddr_in ----------
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8080);
memcpy(&local.sin_addr.s_addr, &ip2, 4); // 4字节直接搬进去
printf("\n\nsin_addr 内存: ");
char *s = (char *)&local.sin_addr;
for (int i = 0; i < 4; i++)
printf("%02X ", s[i]);
printf("\n");
printf("inet_ntoa 还原: %s\n", inet_ntoa(local.sin_addr)); // 192.168.1.1
return 0;
}
- 运行结果
bash
IP: 192.168.1.1
手动(结构体+强转): 0xC0A80101
系统 inet_addr : 0xC0A80101
一致: 是 ✅
强转 uint32_t 后的内存 (低地址 -> 高地址):
01 01 A8 C0
结构体 ip_bytes 的内存 (低地址 -> 高地址):
C0 A8 01 01 ← 这就是网络序!
sin_addr 内存: C0 A8 01 01
inet_ntoa 还原: 192.168.1.1
注意到上面有一条:
local.sin_addr.s_addr = inet_addr(_ip.c_str());//TODO
是的,这里实际上有些问题:
一台服务器上有可能会有许多网卡,这就可能导致一台机器的IP地址不止一个。
所以当我们服务端只绑定一个固定IP地址的时候,我们就不能利用其他IP地址来接收信息,或者进行网络通信。
- 那解决方法是什么呢?
我们不显示的传IP地址:
c
local.sin_addr.s_addr = INADDR_ANY;
INADDR_ANY实际上是0,表示"监听本机的所有网络接口(网卡)上的 IP 地址"
如此一来,其他代码改变为:
cpp
#pragma once
// 这个头文件的内容是实现UPD套接字的使用
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include "Log.hpp"
using namespace LogModule;
class UdpServer
{
public:
UdpServer(uint16_t port)
: _port(port)
{
}
void Init()
{
// 1.打开套接字(网络文件)
size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket fail";
exit(1);
}
_sockfd = sockfd;
LOG(LogLevel::INFO) << "socket success";
// 2.bind
// 填写结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_port = htons(_port);
// local.sin_addr.s_addr = inet_addr(_ip.c_str());//TODO
local.sin_addr.s_addr = INADDR_ANY;
local.sin_family = AF_INET;
// bind
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
// 绑定问题->ip问题
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind fail";
exit(2);
}
LOG(LogLevel::INFO) << "bind success";
}
void Strat()
{
// 收发消息
_running = true;
while(_running)
{
// 收消息
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if(s > 0)
{
uint16_t peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0;
// LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]" << buffer;
// 发消息
std::string echo_string = "server echo@";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
uint16_t _port;
// std::string _ip; // 点分十进制
size_t _sockfd;
bool _running;
};
二、Udpserver.cpp_基础echo版
由于取消了必须显式绑定ip,所以main只需要传两个参数就够了
cpp
#include <iostream>
#include <cstring>
#include <memory>
#include "Udpserver.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;
return 1;
}
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(std::stoi(argv[1]));
usvr->Init();
usvr->Strat();
return 0;
}
三、Udpclient.cpp_基础echo版
cpp
#include <iostream>
#include <string>
#include <cstring> // memset的头
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
// ./Udpclient ip port
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]);
// 打开socket
size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == 0)
{
std::cout << "socket error" << std::endl;
return 2;
}
std::cout << "socket success" << std::endl;
// 填写 sockaddr_in 结构体
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(server_port);
// bind
// 说明一点:bind是bind自己的socket
// 那么这里需要bind吗?
// 必然是需要的,但是不需要显式bing
// 首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
// (client每次都是率先发消息的,所以只需要确保自己的socket信息能够发出即可,并不在乎是哪个ip和port → 客户端的端口"是几"根本不重要)
while(true)
{
std::string input;
std::cout << "Please Enter# ";
getline(std::cin, input);
// 发消息
int n = sendto(sockfd, input.c_str(), input.size(), 0, (sockaddr *)&server, sizeof(server));
(void)n;
// 收消息
struct sockaddr_in peer;
char buffer[1024];
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
简单在这里说明一下c++的输入函数:
| 方法 | 形式 | 读空格 | 读换行 | 缓冲区 |
|---|---|---|---|---|
>> 提取运算符 |
cin >> x |
❌ 停在空白 | ❌ | 安全 |
std::getline |
getline(cin, str) |
✅ | ✅(丢弃) | 自动管理 ⭐ |
cin.getline |
cin.getline(buf, n) |
✅ | ✅(丢弃) | 固定缓冲区 |
cin.get |
cin.get() / cin.get(buf,n) |
✅ | ❌(保留) | 可变 |
cin.read |
cin.read(buf, n) |
✅ 原始 | ❌ | 二进制读取 |
cin.ignore |
辅助方法 | --- | --- | 清空缓冲区 |
简单的翻译功能
接下来我们对上面这部分代码做一下封装分层和功能调整
一、Udpserver.hpp_翻译版
这部分的代码没有很大的调整,只是添加了function函数对象的封装。在外部传入不同的任务,服务器即可执行不同功能。
cpp
#pragma once
// 这个头文件的内容是实现UPD套接字的使用
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Inetaddr.hpp"
#include "Log.hpp"
using namespace LogModule;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _port(port), _func(func)
{
}
void Init()
{
// 1.打开套接字(网络文件)
size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket fail";
exit(1);
}
_sockfd = sockfd;
LOG(LogLevel::INFO) << "socket success";
// 2.bind
// 填写结构体
InetAddr local(_port);
// bind
int n = bind(_sockfd, CONV(local), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind fail";
exit(2);
}
LOG(LogLevel::INFO) << "bind success";
}
void Strat()
{
// 收发消息
_running = true;
while (_running)
{
// 收消息
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
uint16_t peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0;
// 添加函数对象让功能和网络服务器发送分开
InetAddr client(peer);
std::string result = _func(buffer, client);
// 发消息
sendto(_sockfd, result.c_str(), result.size(), 0, CONV(client), client.NetAddrLen());
}
}
}
~UdpServer()
{
}
private:
uint16_t _port;
size_t _sockfd;
bool _running;
func_t _func;
};
值得注意的是,这里引入了一个新的类:InetAddr
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类
#define CONV(addr) ((struct sockaddr*)&addr)
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
// 网络转本地
_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
//_ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
// inet_ntoa()所有调用共享同一个静态缓冲区,后一次调用会直接覆盖前一次的结果,有更好的选择
char buffer[128];
inet_ntop(_addr.sin_family, &_addr.sin_addr.s_addr, buffer, sizeof(buffer));
_ip = buffer;
}
InetAddr(const std::string& ip, uint16_t port) : _ip(ip), _port(port)
{
// 本地转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(_addr.sin_family, ip.c_str(), &_addr.sin_addr.s_addr);
}
InetAddr(uint16_t port) : _port(port)
{
// 本地转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
_addr.sin_addr.s_addr = INADDR_ANY;
}
uint16_t Port() { return _port; }
std::string Ip() { return _ip; }
const struct sockaddr_in &NetAddr() { return _addr; }
const struct sockaddr *NetAddrPtr()
{
return CONV(_addr);
}
socklen_t NetAddrLen()
{
return sizeof(_addr);
}
bool operator==(const InetAddr &addr)
{
return addr._ip == _ip && addr._port == _port;
}
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
这个类是整个翻译版服务端的「地址门面」:它把 struct sockaddr_in 这个又长又难用的结构体包起来,对外只暴露 Ip()、Port()、NetAddr()、NetAddrLen() 等语义清晰的接口,让上层代码不必再关心字节序转换、sockaddr 强转这些细节。
具体来说,它承担了三件事:
- 网络转本地 :从
recvfrom拿到的sockaddr_in里,端口是网络字节序、IP 是 4 字节整数。构造函数里用ntohs把端口转回本地字节序,用inet_ntop把 IP 转成点分十进制字符串,这样日志里打印出来的就是人能看懂的192.168.1.1:8080。 - 本地转网络 :服务端
bind之前需要自己构造地址,于是提供了InetAddr(uint16_t port)这个只传端口的构造,内部把sin_addr.s_addr设为INADDR_ANY,表示监听本机所有网卡;另一个InetAddr(ip, port)则用于需要指定具体 IP 的场景。 - 统一类型转换 :
bind、sendto、recvfrom这些系统调用要的都是struct sockaddr*,而实际填的是sockaddr_in。类里用CONV宏和NetAddrPtr()把这一步强转封装掉,调用处写CONV(local)即可,既简洁又不容易写错。
正因为有了这层封装,UdpServer::Init() 里原本一大段「填结构体、转字节序、强转指针」的样板代码,才能被压缩成 InetAddr local(_port); 加一行 bind 这么清爽。
这里必须要提及一下
inet_aton、inet_pton、inet_ntoa、inet_ntop这四兄弟------它们都是 IP 地址在「点分十进制字符串」和「网络字节序整数」之间转换的函数,但分属两代 API,安全性和用法差别很大。InetAddr类里之所以注释掉inet_ntoa而改用inet_ntop,正是因为前者返回的是函数内部的静态缓冲区,多次调用会互相覆盖,在多线程或连续打印多个地址时会踩坑。下面就把这四者的区别和安全性彻底讲清楚.
inet_aton、inet_pton和inet_ntoa和inet_ntop的安全性和区别
1. 先按方向分组
| 方向 | 老 API(IPv4 专用) | 新 API(IPv4/IPv6 通用) |
|---|---|---|
| 字符串 → 网络字节序整数 | inet_aton |
inet_pton |
| 网络字节序整数 → 字符串 | inet_ntoa |
inet_ntop |
inet_aton/inet_ntoa:来自早期的arpa/inet.h,只支持 IPv4,且inet_ntoa返回的是静态缓冲区。inet_pton/inet_ntop:带p(presentation,表示形式)和n(numeric,数值形式),同时支持AF_INET和AF_INET6,由调用者提供缓冲区。
2. 逐个看用法与坑
inet_aton:字符串 → struct in_addr
cpp
int inet_aton(const char *cp, struct in_addr *inp);
- 成功返回非 0,失败返回 0。
- 它把结果写进你传入的
in_addr,不涉及静态缓冲区,本身是线程安全的。 - 但它只认 IPv4,且对非法输入(如
"999.1.1.1")的判定比较宽松,历史上还支持"1.2.3"、"0x7f.1"这类简写,容易埋坑。
inet_ntoa:struct in_addr → 字符串(有坑!)
cpp
char *inet_ntoa(struct in_addr in);
- 返回值指向函数内部的一块静态缓冲区,所有调用共享同一块内存。
- 这意味着:非线程安全,且连续两次调用会互相覆盖。比如:
cpp
struct in_addr a, b;
// ... 分别赋值 ...
printf("%s %s\n", inet_ntoa(a), inet_ntoa(b)); // 两个参数可能打印出同一个地址!
因为参数求值顺序不确定,两次 inet_ntoa 可能都写进同一块缓冲区,后一次覆盖前一次。这正是文章里 InetAddr 类注释掉 inet_ntoa、改用 inet_ntop 的原因。
inet_pton:字符串 → 数值(推荐)
cpp
int inet_pton(int af, const char *src, void *dst);
af传AF_INET或AF_INET6,dst由调用者提供(IPv4 传struct in_addr*,IPv6 传struct in6_addr*)。- 返回
1表示成功,0表示src不是合法地址,-1表示af不支持(并设置errno)。 - 严格校验格式,不认简写,线程安全。
inet_ntop:数值 → 字符串(推荐)
cpp
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 缓冲区
dst和大小size由调用者提供,因此线程安全,也不会互相覆盖。 - 缓冲区大小建议用宏:IPv4 用
INET_ADDRSTRLEN(16),IPv6 用INET6_ADDRSTRLEN(46)。 - 成功返回
dst,失败返回NULL并设置errno。
3. 安全性对比小结
| 对比项 | inet_aton / inet_ntoa |
inet_pton / inet_ntop |
|---|---|---|
| 线程安全 | inet_ntoa 不安全 (静态缓冲区);inet_aton 安全 |
都安全 |
| 缓冲区归属 | inet_ntoa 由库内部管理 |
由调用者提供,可控 |
| 协议支持 | 仅 IPv4 | IPv4 + IPv6 |
| 输入校验 | 宽松,接受简写 | 严格 |
| 推荐程度 | 仅维护老代码时使用 | 新代码首选 |
4. 落到本文的 InetAddr 类
cpp
// 不推荐:静态缓冲区,多次调用会互相覆盖
// _ip = inet_ntoa(_addr.sin_addr);
// 推荐:自己提供缓冲区,线程安全
char buffer[INET_ADDRSTRLEN];
inet_ntop(_addr.sin_family, &_addr.sin_addr.s_addr, buffer, sizeof(buffer));
_ip = buffer;
同理,字符串转数值时用 inet_pton 替代 inet_addr / inet_aton:
cpp
inet_pton(AF_INET, ip.c_str(), &_addr.sin_addr.s_addr);
一句话记忆:带
p的是新一代、线程安全、支持 IPv6,新代码一律用它;inet_ntoa的静态缓冲区是经典陷阱,能不用就不用。区别
二、Udpserver.cpp_翻译版
由于在上方添加了function函数对象作为构造函数的参数,所以这里无可避免地要添加一个函数:
cpp
std::unique_ptr<UdpServer>
usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client)
{ return dict.Translate(word, client); });
函数采用lambda表达式的方式传入,因为要调用Dict,这里传入dict。
在此之前我们要解释Dict是什么和创建Dict对象并配置文件。
cpp
// 翻译单词、录入词典文件
#pragma once
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include "Inetaddr.hpp"
const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";
class Dict
{
public:
Dict(const std::string &path = defaultdict) : _pathfile(path)
{
}
bool LoadDict()
{
std::ifstream in(_pathfile);
if(!in.is_open())
{
LOG(LogLevel::FATAL) << "配置失败...";
return false;
}
std::string line;
while(getline(in, line))
{
int pos = line.find(sep);
if(pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "格式错误:" << line;
continue;
}
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
if(chinese.empty() || english.empty())
{
LOG(LogLevel::WARNING) << "格式错误:" << line;
continue;
}
_dict.insert(std::make_pair(english, chinese));
LOG(LogLevel::DEBUG) << "加载成功: " << line;
}
LOG(LogLevel::INFO) << "配置完成!";
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 << "不存在";
std::string result = word + "不存在";
return result;
}
LOG(LogLevel::INFO) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << iter->second;
return iter->second;
}
~Dict()
{
}
private:
std::string _pathfile;
std::unordered_map<std::string, std::string> _dict;
};
这里的主要函数有两个,一个是LoadDict,一个是Translate。
LoadDict是利用文件操作加载加载TXT文件内的英汉互译到哈希表中,方便查询。
Translate是利用哈希表做键值对的查询,并输出日志。
cpp
#include <iostream>
#include <cstring>
#include <memory>
#include "Inetaddr.hpp"
#include "Udpserver.hpp"
#include "Dict.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;
return 1;
}
int port = std::stoi(argv[1]);
Dict dict;
dict.LoadDict();
std::unique_ptr<UdpServer>
usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &client)
{ return dict.Translate(word, client); });
usvr->Init();
usvr->Strat();
return 0;
}
三、Udpclient.cpp_翻译版
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
// ./Udpclient ip port
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]);
// 打开socket
size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "socket error" << std::endl;
return 2;
}
std::cout << "socket success" << std::endl;
// 填写 sockaddr_in 结构体
InetAddr server(server_ip, server_port);
while(true)
{
std::string input;
std::cout << "Please Enter# ";
getline(std::cin, input);
// 发消息
int n = sendto(sockfd, input.c_str(), input.size(), 0, CONV(server), server.NetAddrLen());
(void)n;
// 收消息
struct sockaddr_in peer;
char buffer[1024];
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
简单的聊天室功能
下面继续对UDP的代码做升级和功能调整
上面都是单线程去处理回显消息或者是翻译的任务,接下来我们加入线程池来加快服务端的速度
一、Udpserver.hpp_聊天室版
Udpserver.hpp与之前相比没有变化,可以忽略。
cpp
#pragma once
// 这个头文件的内容是实现UPD套接字的使用
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Inetaddr.hpp"
#include "Log.hpp"
using namespace LogModule;
using func_t = std::function<void(const size_t &, const std::string &, InetAddr &)>;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _port(port), _func(func)
{
}
void Init()
{
// 1.打开套接字(网络文件)
size_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket fail";
exit(1);
}
_sockfd = sockfd;
LOG(LogLevel::DEBUG) << "socket success";
// 2.bind
// 填写结构体
InetAddr local(_port);
// bind
int n = bind(_sockfd, CONV(local), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind fail";
exit(2);
}
LOG(LogLevel::DEBUG) << "bind success";
}
void Strat()
{
// 收发消息
_running = true;
while (_running)
{
// 收消息
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
uint16_t peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0;
// 添加函数对象让功能和网络服务器发送分开
InetAddr client(peer);
_func(_sockfd, buffer, client);
}
}
}
~UdpServer()
{
}
private:
uint16_t _port;
size_t _sockfd;
bool _running;
func_t _func;
};
二、Udpserver.cpp_聊天室版
.cpp之中变化的点是路由类的加入:
Doute的功能是对于在线用户的管理和服务端收到的消息进行分发,这里是简略实现的,想要更加严谨的版本可以手动升级。
cpp
#include <iostream>
#include <string>
#include <list>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Mutex.hpp"
#include "Inetaddr.hpp"
using namespace MutexModule;
class Route
{
private:
// 判断是不是新用户
bool IsNewUser(InetAddr &peer)
{
for (auto u : _users)
{
if (peer == u)
return false;
}
return true;
}
// 新增用户
void AddUser(InetAddr &peer)
{
LOG(LogLevel::INFO) << "新增了一个用户:" << peer.StringAddr();
_users.push_back(peer);
}
// 广播消息
void BroadCast(const size_t &socket, const std::string &message, InetAddr &peer)
{
std::string send_message = peer.StringAddr() + " @ " + message;
for (auto u : _users)
{
sendto(socket, send_message.c_str(), send_message.size(), 0, CONV(u), sizeof(u));
}
}
// 判断并退出
void Quit(const std::string &message, InetAddr &peer)
{
if (message == "QUIT")
{
for (auto iter = _users.begin(); iter != _users.end(); iter++)
{
if (peer == *iter)
{
LOG(LogLevel::INFO) << "离线了一个用户:" << peer.StringAddr();
_users.erase(iter);
break;
}
}
}
}
public:
Route()
{
}
void MessageRoute(const size_t &socket, const std::string &message, InetAddr &peer)
{
// 访问的是共享资源(_users)
LockGuard lockguard(_mutex);
// 判断是不是新用户,是则加入
if (IsNewUser(peer))
AddUser(peer);
// 将消息发送给所有人
BroadCast(socket, message, peer);
// 消息是QUIT就退出
Quit(message, peer);
}
~Route()
{
}
private:
std::list<InetAddr> _users;
Mutex _mutex;
};
cpp
#include <iostream>
#include <cstring>
#include <memory>
#include <functional>
#include "Udpserver.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
using task_t = std::function<void()>;
using namespace ThreadPoolModule;
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;
return 1;
}
int port = std::stoi(argv[1]);
Route route;
auto tp = ThreadPool<task_t>::GetInstance();
std::unique_ptr<UdpServer>
usvr = std::make_unique<UdpServer>(port, [&tp, &route](const size_t &socket, const std::string &message, InetAddr &client)
{
task_t t= std::bind(&Route::MessageRoute,&route,socket, message, client);
tp -> Enqueue(t);
});
usvr->Init();
usvr->Strat();
return 0;
}
三、Udpclient.cpp_聊天室版
这这个模块中比较重要的一点是由于多用户的加入,收消息和发消息必须实现非阻的分别进行,于是这里用两个线程来实现。
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "Inetaddr.hpp"
using namespace ThreadModule;
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
pthread_t id;
void Send()
{
InetAddr server(server_ip, server_port);
while (true)
{
std::string input;
std::cout << "Please Enter# ";
getline(std::cin, input);
// 发消息
int n = sendto(sockfd, input.c_str(), input.size(), 0, CONV(server), server.NetAddrLen());
(void)n;
if (input == "QUIT")
{
pthread_cancel(id);
break;
}
}
}
void Recv()
{
// 收消息
while (true)
{
struct sockaddr_in peer;
char buffer[1024];
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = 0;
std::cerr << buffer << std::endl;
}
}
}
int main(int argc, char *argv[])
{
// ./Udpclient ip port
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]);
// 打开socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "socket error" << std::endl;
return 2;
}
std::cout << "socket success" << std::endl;
// 客户端改用多线程解决阻塞时只能处理一个用户的bug
Thread sender(Send);
Thread recver(Recv);
sender.Start();
recver.Start();
id = recver.Id();
sender.Join();
recver.Join();
return 0;
}
- 本节完...
📝 作者:say-fall | 编辑:say-fall | 🌟 原创不易,如果对你有帮助,记得 👍 点赞 + ⭐ 收藏 哦!