
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统与网络知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在完成 Linux 网络基础知识的学习之后,接下来就需要真正进入网络编程实战 .相比只理解 IP、端口、协议和网络分层,自己动手编写一个能够收发数据的网络程序,才能更直观地理解数据究竟是如何从客户端发送到Linux主机,并由服务端接收和处理的.UDP 是网络编程中非常重要的一种传输方式.它采用无连接、面向数据报 的通信模型,不需要像TCP一样提前建立连接,通信过程更加简单,传输开销也更小,因此常被应用在实时通信、状态上报、局域网通信、音视频传输等场景中.对于刚开始学习 Socket 编程的读者来说,UDP 也是理解"IP + 端口 + 套接字 + 数据收发"这一整套网络通信流程的很好切入点.不过,仅仅写出 UDP 服务端和客户端代码还不够.在实际学习和调试过程中,我们还需要掌握一些常用的 Linux 网络命令,用来查看网卡信息、确认 IP 地址、检查端口监听状态、测试网络连通性以及分析数据包.例如通过相关命令确认服务是否启动成功,再使用客户端从其他主机访问 Linux 服务端,才能真正完成从程序编写、网络配置、通信测试到结果验证 的完整闭环.本文将围绕 UDP Socket 编程实战 展开,从常用 Linux 网络命令入手,逐步实现 UDP 服务端与客户端程序,并结合 IP 地址、端口号、
socket、bind、sendto、recvfrom等核心接口,详细梳理 UDP 数据收发流程.随后通过客户端访问 Linux 主机进行实际通信验证,进一步分析网络配置、端口监听、连通性检测以及常见问题排查方法.通过本文的学习,你不仅能够完成一个基本的UDP网络通信程序,还能建立起较完整的Linux网络实战思路,为后续深入学习 TCP Socket 编程、客户端/服务器模型、并发服务器、网络协议分析以及高性能网络编程 打下扎实基础.

目录
- 一、UDP⽹络编程
-
- [1.1 V1版本--UDP Echo Server:完成客户端与服务端的基本通信闭环](#1.1 V1版本–UDP Echo Server:完成客户端与服务端的基本通信闭环)
- [1.2 V2版本--DictServer:引入词典业务模块,实现单词查询](#1.2 V2版本–DictServer:引入词典业务模块,实现单词查询)
- [1.3 V2版本--DictServer封装版:网络层与业务层解耦](#1.3 V2版本–DictServer封装版:网络层与业务层解耦)
- [1.4 V3版本--简单聊天室:基于UDP+线程池实现多用户广播通信](#1.4 V3版本–简单聊天室:基于UDP+线程池实现多用户广播通信)
- 二、补充相关参考内容
-
- 2.1地址转换函数
- 2.2关于inet_ntoa
- [2.3remove_if 样例](#2.3remove_if 样例)
- 三、⽹络命令
- 四、验证UDP---Windows作为客户端访问Linux
一、UDP⽹络编程
UDP 网络编程的核心可以概括成一句话:通过 Socket 直接发送和接收一个个独立的数据报,不需要先建立连接.
1.UDP的特点
UDP(User Datagram Protocol,用户数据报协议)属于传输层协议,和TCP相比:
| UDP | TCP |
|---|---|
| 无连接 | 面向连接 |
| 不保证送达 | 可靠传输 |
| 不保证顺序 | 保证有序 |
| 可能重复、丢包 | 自动重传 |
| 开销小、速度快 | 开销相对较大 |
| 保留消息边界 | 字节流 |
典型应用包括:DNS、音视频直播、实时游戏、语音通话、局域网广播等.
2.UDP编程流程
UDP 服务端通常是:
text
socket()
↓
bind()
↓
recvfrom()
↓
sendto()
↓
close()
客户端通常是:
text
socket()
↓
sendto()
↓
recvfrom()
↓
close()
注意:UDP 一般不需要 listen() 和 accept().
3.Linux C语言UDP服务端
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];
// 1. 创建 UDP socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 设置服务端地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 3. 绑定端口
bind(sockfd,
(struct sockaddr *)&server_addr,
sizeof(server_addr));
printf("UDP server listening on port 8888...\n");
while (1) {
// 4. 接收数据
int n = recvfrom(
sockfd,
buffer,
sizeof(buffer) - 1,
0,
(struct sockaddr *)&client_addr,
&client_len
);
buffer[n] = '\0';
printf("收到:%s\n", buffer);
// 5. 回复客户端
sendto(
sockfd,
buffer,
n,
0,
(struct sockaddr *)&client_addr,
client_len
);
}
close(sockfd);
return 0;
}
4.UDP客户端
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[1024];
// 1. 创建 UDP socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(
AF_INET,
"127.0.0.1",
&server_addr.sin_addr
);
char *msg = "Hello UDP";
// 3. 发送数据
sendto(
sockfd,
msg,
strlen(msg),
0,
(struct sockaddr *)&server_addr,
sizeof(server_addr)
);
// 4. 接收回复
socklen_t len = sizeof(server_addr);
int n = recvfrom(
sockfd,
buffer,
sizeof(buffer) - 1,
0,
(struct sockaddr *)&server_addr,
&len
);
buffer[n] = '\0';
printf("服务器回复:%s\n", buffer);
close(sockfd);
return 0;
}
编译:
bash
gcc server.c -o server
gcc client.c -o client
先启动:
bash
./server
再运行:
bash
./client
客户端发送:
text
Hello UDP
服务端接收后再原样返回.
5.最需要掌握的函数
c
socket(AF_INET, SOCK_DGRAM, 0);
创建 UDP Socket.
c
bind()
把 Socket 和本机 IP + 端口号绑定,一般服务端需要.
c
sendto()
向指定 IP 和端口发送一个 UDP 数据报.
c
recvfrom()
接收UDP数据报,同时可以获得发送方的 IP 和端口.
其中最关键的是理解:
text
TCP:一个连接 → 连续字节流
UDP:
客户端A ──数据报1──→ 服务端
客户端B ──数据报2──→ 服务端
客户端A ──数据报3──→ 服务端
每个 UDP 数据报彼此独立。
1.1 V1版本--UDP Echo Server:完成客户端与服务端的基本通信闭环




cpp
//Makefile
.PHONY:all
all:client_udp server_udp
server_udp:EchoServerMain.cc
g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cc
g++ -o $@ $^ -std=c++17 -static
.PHONY:clean
clean:
rm -f client_udp server_udp
cpp
//EchoServer.hpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static int default_port = 8888;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(uint16_t port = default_port)
: _port(port),
_sockfd(default_fd)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步: 填充网络信息, 有没有IP和端口信息设置到内核中??设置到你刚刚打开的网络socket对应的文件内部?
struct sockaddr_in local; // struct sockaddr_in 数据类型,local 用户栈上的!!!,并没有设置到内核
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // h->n
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
local.sin_addr.s_addr = INADDR_ANY; // 最佳实践:任意IP地址bind
// 第三步:bind socket 信息
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
}
void Start()
{
// 传递的是字符串,echo server
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 用户发来的数据
// 2. 用户的socket信息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
// ?
// 用户的IP + Port信息,是recvfrom从网络中获取到的数据,网络序列(大端)-> 转换成为主机序列
// peer.sin_port; // peer client port
// peer.sin_addr; // peer client ip
uint16_t client_port = ntohs(peer.sin_port);
std::string client_ip = inet_ntoa(peer.sin_addr); // 4字节IP-> ntoh -> 字符串
std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]# ";
// 用户发来的数据
inbuffer[n] = 0;
LOG(LogLevel::DEBUG) << client_address << inbuffer;
std::string echo_string = "server echo# ";
echo_string += inbuffer;
// h to n
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
uint16_t _port; // 用户设置好的,server port必须是固定的!
};
#endif
cpp
//EchoServerMain.cc
#include "EchoServer.hpp"
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
// ./server_udp port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);
usvr->Init();
usvr->Start();
return 0;
}
cpp
//Logger.hpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"
namespace NS_LOG_MODULE
{
enum class LogLevel
{
INFO,
WARNING,
ERROR,
FATAL,
DEBUG
};
std::string LogLevel2Message(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKNOWN";
}
}
// 1. 时间戳 2. 日期+时间
std::string GetCurrentTime()
{
struct timeval current_time;
int n = gettimeofday(¤t_time, nullptr);
(void)n;
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec);
return timestr;
}
// 输出角度 -- 刷新策略
// 1. 显示器打印
// 2. 文件写入
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 控制台日志刷新策略, 日志将来要向显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl; // ??
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
// 文件策略
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
: _logpath(path),
_logfilename(name)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_mutex);
if (!_logpath.empty() && _logpath.back() != '/')
{
_logpath += "/";
}
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << "failed" << std::endl;
return;
}
out << message << "\n";
out.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
// 交给大家
// const std::string defaultfilename = "log.info";
// const std::string defaultfilename = "log.warning";
// const std::string defaultfilename = "log.fatal";
// const std::string defaultfilename = "log.error";
// const std::string defaultfilename = "log.debug";
// 文件策略&&分日志等级来进行保存
// class FileLogLevelStrategy : public LogStrategy
// {
// public:
// private:
// };
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略,进行刷新
class Logger
{
// 日志的生成:
// 构建日志字符串
public:
Logger()
{
UseConsoleStrategy();
}
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 内部类, 标识一条完整的日志信息
// 一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
// LogMessage RAII风格的方式,进行刷新
class LogMessage
{
public:
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _level(level),
_curr_time(GetCurrentTime()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 先构建出来左半部分
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2Message(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage对象,方便下次继续进行<<
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo; // 一条完整的日志信息
// 一个引用,引用外部的Logger类对象
Logger &_logger; // 方便我们后续进行策略式刷新
};
// 这里已经不是内部类了
// 故意采用拷贝LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
#endif
cpp
//Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Ptr()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard // RAII风格代码
{
public:
LockGuard(Mutex &lock):_lockref(lock)
{
_lockref.Lock();
}
~LockGuard()
{
_lockref.Unlock();
}
private:
Mutex &_lockref;
};
cpp
//EchoClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t";
std::cout << proc << " server_ip server_port" << std::endl;
}
// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(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;
exit(2);
}
// 2. 构建server端socket信息
// client 需要有自己的IP和Port信息吗? 需要的!
// 需要显示的bind自己的ip和端口吗?不要显示bind!!!
// 1. 为什么不让client显示bind?client bind port 出现冲突!client port只需要具有唯一性即可,具体是几,不重要。
// 2. 如何设置自己的IP和端口呢?client 一般会采用随机端口的方式!由OS自主选择!
// udp client 首次发送数据的时候,OS底层会隐式自动帮你进行获取随机端口,然后bind + Port + IP
struct sockaddr_in server;
memset(&server, 0, sizeof(0));
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 message;
// 1. 获取用户输入
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// 2. clinet 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n > 0)
{
// recvfrom
char inbuffer[1024] = {0};
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
}
}
return 0;
}
这部分代码本质上是在完成 UDP 网络编程的第一个完整版本:Echo Server(回显服务器) .它已经不只是演示 socket(),而是把 服务端封装、客户端通信、日志系统、互斥锁 RAII、程序入口 串成了一个完整的小项目.
1.Echo Server 要解决什么问题
Echo Server 就是回显服务器.
整个通信过程非常简单:
text
Client Server
输入 hello
│
│ sendto("hello")
├──────────────────────────────►│
│ │ recvfrom()
│ │
│ │ "server echo# " + "hello"
│ │
│ sendto(response) │
│◄──────────────────────────────┤
│
recvfrom()
│
输出:
server echo# hello
也就是说:客户端发送什么,服务器收到以后加工一下,再发送回来.
虽然业务很简单,但它把 UDP 编程最重要的流程全部覆盖了:
text
socket
↓
bind
↓
recvfrom
↓
sendto
这也是后面 DictServer、聊天室等程序的基础.
2.整个项目的文件结构
当前工程主要包含:
text
EchoClient.cc
EchoServer.hpp
EchoServerMain.cc
Logger.hpp
Mutex.hpp
Makefile
可以把它们理解成:
| 文件 | 作用 |
|---|---|
EchoServer.hpp |
UDP 服务端核心逻辑 |
EchoServerMain.cc |
服务端程序入口 |
EchoClient.cc |
UDP 客户端 |
Logger.hpp |
日志系统 |
Mutex.hpp |
互斥锁及 RAII 封装 |
Makefile |
编译整个工程 |
其中真正完成网络通信的是:
text
EchoClient.cc
│
│ UDP
▼
EchoServer.hpp
而:
text
Logger.hpp
Mutex.hpp
属于项目的基础设施代码.
3.服务端封装 ------ UdpServer
EchoServer.hpp 中最核心的是:
cpp
class UdpServer
{
public:
UdpServer(uint16_t port = default_port)
: _port(port),
_sockfd(default_fd)
{}
private:
int _sockfd;
uint16_t _port;
};
这里把 UDP 服务端抽象成了一个 UdpServer 类.
对象里面保存两个最重要的数据:
text
_sockfd
表示服务器创建出来的 socket 文件描述符.
text
_port
表示服务器绑定的端口号.
于是:
cpp
UdpServer server(8888);
就可以理解成:创建一个准备在 8888 端口工作的 UDP 服务器对象.
4.Init ------ 创建 Socket 并绑定端口
服务端初始化的核心逻辑是:
cpp
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
这里三个参数分别表示:
text
AF_INET
↓
IPv4
SOCK_DGRAM
↓
UDP
0
↓
使用默认协议
所以:
cpp
socket(AF_INET, SOCK_DGRAM, 0);
实际上就是:创建一个 IPv4 UDP socket.
接下来构造服务器自己的地址:
cpp
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;
这里的 sockaddr_in 可以理解为:
text
一个描述 IPv4 网络地址的数据结构
┌─────────────────────────┐
│ IP协议类型 AF_INET │
├─────────────────────────┤
│ Port │
├─────────────────────────┤
│ IP Address │
└─────────────────────────┘
其中:
cpp
local.sin_port = htons(_port);
是因为端口号需要使用网络字节序.
所以:
text
host to network short
htons
而:
cpp
local.sin_addr.s_addr = INADDR_ANY;
非常重要.
表示:不限定某一个本机 IP,只要是发送到本机该端口的数据,都可以接收.
类似于:
text
0.0.0.0:8888
最后:
cpp
bind(_sockfd,
(struct sockaddr *)&local,
sizeof(local));
完成:
text
socket 文件描述符
+
本机 IP
+
端口号
之间的绑定.
因此服务端初始化阶段可以总结成:
text
socket()
│
▼
创建 UDP socket
│
▼
构造 sockaddr_in
│
▼
bind()
│
▼
服务器正式绑定端口
5.Start ------ 服务端循环接收客户端请求
服务器进入:
cpp
while(true)
以后,就不断等待 UDP 数据.
核心代码:
cpp
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(
_sockfd,
inbuffer,
sizeof(inbuffer) - 1,
0,
(struct sockaddr *)&peer,
&len
);
这里最值得理解的是:
recvfrom() 不仅收到数据,还能知道是谁发来的
它得到两部分信息:
text
recvfrom()
① 数据
↓
inbuffer
② 对方地址
↓
peer
也就是说服务器收到:
text
hello
的同时,还知道:
text
客户端 IP
客户端 Port
所以代码接下来:
cpp
uint16_t client_port = ntohs(peer.sin_port);
取得客户端端口.
这里:
text
ntohs
network to host short
负责:
text
网络字节序
↓
主机字节序
再通过:
cpp
std::string client_ip = inet_ntoa(peer.sin_addr);
得到客户端 IP.
最终拼接:
cpp
std::string client_address =
"[" + client_ip + ":" +
std::to_string(client_port) + "]# ";
日志就可以打印类似:
text
[192.168.2.3:54321]# hello
6.Echo ------ 把客户端消息返回
服务器收到:
text
hello
后:
cpp
inbuffer[n] = 0;
把收到的数据变成合法的 C 风格字符串.
然后:
cpp
std::string echo_string = "server echo# ";
echo_string += inbuffer;
得到:
text
server echo# hello
随后:
cpp
sendto(
_sockfd,
echo_string.c_str(),
echo_string.size(),
0,
(struct sockaddr *)&peer,
len
);
把数据发送给刚才那个客户端.
注意这里特别体现了 UDP 的特点.
服务端没有:
cpp
accept()
也没有保存:
text
client socket
而是:
text
recvfrom()
↓
得到客户端 IP + Port
↓
sendto()
↓
直接根据 IP + Port 发回去
这就是 UDP 的典型通信方式.
7.服务端入口 ------ EchoServerMain.cc
EchoServerMain.cc 的任务不是实现网络通信,而是:负责启动服务器。
首先要求运行程序时传入一个端口:
cpp
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
所以启动方式类似:
bash
./server_udp 8888
然后:
cpp
uint16_t server_port = std::stoi(argv[1]);
得到:
text
8888
接下来:
cpp
std::unique_ptr<UdpServer> usvr =
std::make_unique<UdpServer>(server_port);
创建服务器对象.
然后:
cpp
usvr->Init();
usvr->Start();
整个服务端程序逻辑实际上非常清楚:
text
main()
↓
读取 port
↓
创建 UdpServer
↓
Init()
│
├── socket()
│
└── bind()
↓
Start()
│
├── recvfrom()
├── 处理数据
└── sendto()
8.UDP Client ------ 客户端主动发送请求
EchoClient.cc 就是客户端.
运行形式:
bash
./client_udp server_ip server_port
比如:
bash
./client_udp 127.0.0.1 8888
首先取得:
cpp
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
然后客户端同样要创建 socket:
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
因此要注意:
text
Server 需要 socket
Client 也需要 socket
socket 本质上就是:操作系统提供给用户进行网络通信的接口.
9.客户端构造服务器地址
客户端需要知道:我要把数据发给谁?
所以构造:
cpp
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());
这里实际上描述的是:
text
目标服务器
IP:server_ip
Port:server_port
例如:
text
127.0.0.1:8888
10.为什么客户端没有手动 bind?
代码里的注释提到了一个非常重要的问题:
text
client 需要 bind 吗?
实际上:UDP 客户端通常不需要主动调用 bind。
因为第一次执行:
cpp
sendto()
时,如果客户端还没有绑定地址,操作系统会自动给它:
text
选择本机 IP
+
分配临时端口
例如:
text
客户端
192.168.1.10:52731
其中:
text
52731
可能就是操作系统自动选择的临时端口.
所以执行:
cpp
sendto(...)
之后,可以近似理解为操作系统已经帮你完成了:
text
socket
↓
自动选择 IP
↓
自动选择临时 Port
↓
隐式 bind
↓
sendto
11.客户端发送与接收
客户端首先:
cpp
std::getline(std::cin, message);
让用户输入:
text
hello
然后:
cpp
sendto(
sockfd,
message.c_str(),
message.size(),
0,
(struct sockaddr *)&server,
sizeof(server)
);
发送到:
text
server_ip:server_port
随后:
cpp
recvfrom(
sockfd,
inbuffer,
sizeof(inbuffer) - 1,
0,
(struct sockaddr *)&temp,
&len
);
等待服务器回复.
服务器发送:
text
server echo# hello
客户端最终:
cpp
std::cout << inbuffer << std::endl;
打印:
text
server echo# hello
这样一个完整的 UDP 请求---响应过程就完成了.
12.Logger.hpp ------ 日志系统
这里并没有直接大量使用:
cpp
std::cout
std::cerr
而是封装了一套自己的日志系统:
cpp
LOG(LogLevel::INFO)
LOG(LogLevel::DEBUG)
LOG(LogLevel::ERROR)
LOG(LogLevel::FATAL)
例如:
cpp
LOG(LogLevel::INFO)
<< "create socket success, sockfd: "
<< _sockfd;
这样做的优势是可以统一输出:
text
日志等级
时间
文件名
行号
具体消息
例如以后可以形成:
text
[INFO][2026-10-05 15:00:00][EchoServer.hpp:49]
create socket success, sockfd: 3
而且你的 Logger.hpp 还使用了策略模式.
大致关系是:
text
Log
│
▼
LogStrategy
/ \
/ \
▼ ▼
Console File
Strategy Strategy
因此日志以后可以选择:
text
输出到控制台
或者:
text
输出到日志文件
这部分和网络本身没有直接关系,但属于比较好的工程化封装.
13.Mutex.hpp ------ 对 pthread_mutex 的 RAII 封装
我的:
cpp
class Mutex
实际上是对:
cpp
pthread_mutex_t
的一层 C++ 封装.
提供:
cpp
Lock()
Unlock()
对应:
cpp
pthread_mutex_lock()
pthread_mutex_unlock()
更加重要的是:
cpp
class LockGuard
它采用了 RAII 思想.
构造:
cpp
LockGuard(Mutex &lock)
{
_lockref.Lock();
}
自动加锁.
析构:
cpp
~LockGuard()
{
_lockref.Unlock();
}
自动解锁.
因此可以写:
cpp
{
LockGuard guard(mutex);
// 临界区
}
离开作用域之后:
text
guard 析构
↓
自动 Unlock
这样可以防止:
text
return
异常
忘记调用 Unlock
导致锁无法释放.
当前 Echo Server 本身还是单线程程序,因此这里的互斥锁主要是为日志系统以及后续多线程扩展做准备.
运行测试结果如下:

完整过程就是:
bash
终端1:Server 终端2:Client
./server_udp 8888 ./client_udp 127.0.0.1 8888
│ │
│ │
│ Please Enter# hello
│ │
│◄──────── sendto("hello") ────────────┤
│ │
recvfrom() │
│ │
打印: │
[127.0.0.1:xxxxx]# hello │
│ │
├──── sendto("server echo# hello") ───►│
│ │
│ recvfrom()
│ │
│ 输出:
│ server echo# hello
1.2 V2版本--DictServer:引入词典业务模块,实现单词查询

实现⼀个简单的英译汉的⽹络字典
bash
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
前面的代码不变,增加了Dict.hpp部分的代码,以及在EchoServer.hpp文件增加"Dict.hpp"头文件即可
cpp
//Dict.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
static const std::string default_dict = "./dict.txt";
static const std::string sep = ": ";
using namespace NS_LOG_MODULE;
class Dict
{
public:
Dict(const std::string &dict_path = default_dict) : _dict_path(dict_path)
{
LoadDict();
}
~Dict()
{
}
void LoadDict()
{
std::ifstream in(_dict_path);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << " open " << _dict_path << " error";
exit(1);
}
std::string line;
while (std::getline(in, line))
{
LOG(LogLevel::FATAL) << "load: " << line << " success";
// apple: 苹果
auto pos = line.find(sep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "Format: " << line << " error";
continue;
}
std::string k = line.substr(0, pos); // [)
std::string v = line.substr(pos + sep.size());
_dict.insert(std::make_pair(k, v));
}
in.close();
LOG(LogLevel::INFO) << "load done....";
}
std::string Translate(std::string word)
{
auto iter = _dict.find(word);
if(iter != _dict.end())
{
return iter->second;
}
else
{
return "None";
}
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
1.3 V2版本--DictServer封装版:网络层与业务层解耦

cpp
//DictServer.hpp
#pragma once
#include <string>
#include "UdpServer.hpp"
#include "Dict.hpp"
class DictServer
{
public:
DictServer(
uint16_t port,
const std::string &dict_path = default_dict)
: _dict(dict_path),
_server(
port,
[this](const std::string &word)
{
return Translate(word);
})
{
}
~DictServer()
{
}
public:
void Init()
{
_server.Init();
}
void Start()
{
_server.Start();
}
private:
// DictServer 真正的业务处理函数
std::string Translate(const std::string &word)
{
LOG(LogLevel::DEBUG)
<< "translate request: "
<< word;
std::string result =
_dict.Translate(word);
LOG(LogLevel::DEBUG)
<< "translate result: "
<< word
<< " -> "
<< result;
return result;
}
private:
// 业务模块
Dict _dict;
// 网络模块
UdpServer _server;
};
cpp
//DictServerMain.cc
#include <iostream>
#include <string>
#include <memory>
#include <cstdlib>
#include "DictServer.hpp"
static void Usage(const std::string &process)
{
std::cerr
<< "Usage:\n\t"
<< process
<< " port [dict_path]"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
return 1;
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t port =
static_cast<uint16_t>(
std::stoi(argv[1]));
std::string dict_path = default_dict;
if (argc == 3)
{
dict_path = argv[2];
}
std::unique_ptr<DictServer> server =
std::make_unique<DictServer>(
port,
dict_path);
server->Init();
server->Start();
return 0;
}
1.4 V3版本--简单聊天室:基于UDP+线程池实现多用户广播通信




cpp
//Makefile
.PHONY:all
all:client_chat server_chat
server_chat:ChatServerMain.cc
g++ -o $@ $^ -std=c++17
client_chat:ChatClient.cc
g++ -o $@ $^ -std=c++17 -static
.PHONY:clean
clean:
rm -f client_chat server_chat
cpp
//ChatClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
using namespace NS_THREAD_MODULE;
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t";
std::cout << proc << " server_ip server_port" << std::endl;
}
static void Online(InetAddr &serveraddr)
{
std::cout << "Please Set Your Nick Name# ";
std::getline(std::cin, nickname);
std::string online_message = nickname + " online!";
ssize_t n = sendto(sockfd, online_message.c_str(), online_message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
void RecvMessage()
{
while (true)
{
// recvfrom
char inbuffer[1024] = {0};
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
inbuffer[m] = 0;
std::cerr << inbuffer << std::endl; // 2
}
}
}
void SendMessage()
{
InetAddr serveraddr(server_port, server_ip);
Online(serveraddr);
while (true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter# "; // 1
std::getline(std::cin, message);
message = nickname + "# " + message;
// 2. clinet 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
}
// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(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;
exit(2);
}
Thread recver(RecvMessage);
Thread sender(SendMessage);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
return 0;
}
cpp
//ChatServerMain.cc
#include "ThreadPool.hpp" // 执行者,执行处理动作的人
#include "Route.hpp" // 任务
#include "UdpServer.hpp" // 获取事件
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
// ./server_udp port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
// 线程池模块
auto thread_pool = ThreadPool<task_t>::Instance();
// 路由模块
Route r;
// 网络模块
UdpServer usvr(server_port);
usvr.Init();
usvr.RegisterService(
[&r](const InetAddr &addr){
r.CheckUser(addr);
},
[&r, thread_pool](int sockfd, std::string msg){
auto t = std::bind(&Route::Broadcast, &r, sockfd, msg);
thread_pool->Enqueue(t);
// thread_pool->Enqueue([&r, &sockfd, &msg](){
// r.Broadcast(sockfd, msg);
// });
}
);
usvr.Start();
return 0;
}
cpp
//Cond.hpp
#ifndef __COND_HPP
#define __COND_HPP
#include <pthread.h>
#include "Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex &mutex)
{
int n = pthread_cond_wait(&_cond, mutex.Ptr());
(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;
};
#endif
cpp
//InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 对客户端进行先描述
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
{
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
{
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port); // h->n
_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
_len = sizeof(_address);
}
bool operator == (const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
InetAddr()
{}
struct sockaddr_in *GetNetAddress()
{
return &_address;
}
socklen_t Len()
{
return _len;
}
~InetAddr()
{}
private:
// net address
struct sockaddr_in _address;
socklen_t _len;
// host address
std::string _ip;
uint16_t _port;
};
cpp
//Logger.hpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"
namespace NS_LOG_MODULE
{
enum class LogLevel
{
INFO,
WARNING,
ERROR,
FATAL,
DEBUG
};
std::string LogLevel2Message(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKNOWN";
}
}
// 1. 时间戳 2. 日期+时间
std::string GetCurrentTime()
{
struct timeval current_time;
int n = gettimeofday(¤t_time, nullptr);
(void)n;
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec);
return timestr;
}
// 输出角度 -- 刷新策略
// 1. 显示器打印
// 2. 文件写入
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 控制台日志刷新策略, 日志将来要向显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl; // ??
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
// 文件策略
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
: _logpath(path),
_logfilename(name)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_mutex);
if (!_logpath.empty() && _logpath.back() != '/')
{
_logpath += "/";
}
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << "failed" << std::endl;
return;
}
out << message << "\n";
out.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
// 交给大家
// const std::string defaultfilename = "log.info";
// const std::string defaultfilename = "log.warning";
// const std::string defaultfilename = "log.fatal";
// const std::string defaultfilename = "log.error";
// const std::string defaultfilename = "log.debug";
// 文件策略&&分日志等级来进行保存
// class FileLogLevelStrategy : public LogStrategy
// {
// public:
// private:
// };
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略,进行刷新
class Logger
{
// 日志的生成:
// 构建日志字符串
public:
Logger()
{
UseConsoleStrategy();
}
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 内部类, 标识一条完整的日志信息
// 一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
// LogMessage RAII风格的方式,进行刷新
class LogMessage
{
public:
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _level(level),
_curr_time(GetCurrentTime()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 先构建出来左半部分
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2Message(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage对象,方便下次继续进行<<
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo; // 一条完整的日志信息
// 一个引用,引用外部的Logger类对象
Logger &_logger; // 方便我们后续进行策略式刷新
};
// 这里已经不是内部类了
// 故意采用拷贝LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
#endif
cpp
//Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Ptr()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard // RAII风格代码
{
public:
LockGuard(Mutex &lock):_lockref(lock)
{
_lockref.Lock();
}
~LockGuard()
{
_lockref.Unlock();
}
private:
Mutex &_lockref;
};
cpp
//Route.hpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include "Mutex.hpp"
#include "UserManager.hpp"
class Route
{
public:
Route():_uma(std::make_unique<UserManager>())
{}
void CheckUser(const InetAddr &addr)
{
LockGuard lockguard(_lock);
_uma->AddUser(addr);
}
void OfflineUser(const InetAddr &addr)
{
LockGuard lockguard(_lock);
_uma->DelUser(addr);
}
void Broadcast(int sockfd, std::string message)
{
LockGuard lockguard(_lock);
auto &users = _uma->Users();
for(auto &user : users)
{
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)user.GetNetAddress(), user.Len());
}
}
// void Sendto(int sockfd, std::string message, InetAddr & who)
// {
// }
~Route()
{}
private:
std::unique_ptr<UserManager> _uma;
// std::queue<std::string> _q;
Mutex _lock;
};
cpp
//Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
namespace NS_THREAD_MODULE
{
static int gnumber = 1;
using callback_t = std::function<void()>;
enum class TSTATUS
{
THREAD_NEW,
THREAD_RUNNING,
THREAD_STOP
};
std::string Status2String(TSTATUS s)
{
switch (s)
{
case TSTATUS::THREAD_NEW:
return "THREAD_NEW";
case TSTATUS::THREAD_RUNNING:
return "THREAD_RUNNING";
case TSTATUS::THREAD_STOP:
return "THREAD_STOP";
default:
return "UNKNOWN";
}
}
std::string IsJoined(bool joinable)
{
return joinable ? "true" : "false";
}
class Thread
{
private:
void ToRunning()
{
_status = TSTATUS::THREAD_RUNNING;
}
void ToStop()
{
_status = TSTATUS::THREAD_STOP;
}
static void *ThreadRoutine(void *args)
{
Thread *self = static_cast<Thread *>(args);
pthread_setname_np(self->_tid, self->_name.c_str());
self->_cb();
self->ToStop();
return nullptr;
}
public:
Thread(callback_t cb)
: _tid(-1), _status(TSTATUS::THREAD_NEW), _joinable(true), _cb(cb), _result(nullptr)
{
_name = "Slaver-" + std::to_string(gnumber++);
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
if (n != 0)
return false;
ToRunning();
return true;
}
void Join()
{
if (_joinable)
{
int n = pthread_join(_tid, &_result);
if (n != 0)
{
std::cerr << "join error: " << n << std::endl;
return;
}
(void)_result;
_status = TSTATUS::THREAD_STOP;
}
else
{
std::cerr << "error, thread join status: " << IsJoined(_joinable) << std::endl;
}
}
// 暂停
// void Stop() // restart()
// {
// // 让线程暂停
// }
void Die()
{
if (_status == TSTATUS::THREAD_RUNNING)
{
pthread_cancel(_tid);
_status = TSTATUS::THREAD_STOP;
}
}
void Detach()
{
if (_status == TSTATUS::THREAD_RUNNING && _joinable)
{
pthread_detach(_tid);
_joinable = false;
}
else
{
std::cerr << "detach " << _name << " failed" << std::endl;
}
}
void PrintInfo()
{
std::cout << "thread name : " << _name << std::endl;
std::cout << "thread _tid : " << _tid << std::endl;
std::cout << "thread _status : " << Status2String(_status) << std::endl;
std::cout << "thread _joinable : " << IsJoined(_joinable) << std::endl;
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
TSTATUS _status;
bool _joinable;
// 线程要有自己的任务处理,即回调函数
callback_t _cb;
// 线程退出信息
void *_result;
};
}
cpp
//ThreadPool.hpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include "Logger.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
namespace NS_THREAD_POOL_MODULE
{
using namespace NS_LOG_MODULE;
using namespace NS_THREAD_MODULE;
const int defaultnum = 5;
// void Test()
// {
// char name[128];
// pthread_getname_np(pthread_self(), name, sizeof(name));
// while (true)
// {
// LOG(LogLevel::DEBUG) << "我是一个线程,我要进行运行:" << name;
// sleep(1);
// }
// }
// 线程池要不要对多个线程进行管理呢??
// 先描述,在组织!
template <typename T>
class ThreadPool
{
private:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T task;
{
// 保护临界区
LockGuard lockguard(_mutex);
// 检测任务。不休眠:1. 队列不为空 2. 线程池退出 -> 队列为空 && 线程池不退出
while (_tasks.empty() && _isrunning)
{
// 没有任务, 休眠
_slaver_sleep_count++;
_cond.Wait(_mutex);
_slaver_sleep_count--;
}
// 线程池退出了-> while 就要break -> 不能
// 1. 线程池退出 && _tasks empty
if (!_isrunning && _tasks.empty())
{
_mutex.Unlock();
break;
}
// 有任务, 取任务,本质:把任务由公共变成私有
// T -> task*
task = _tasks.front();
_tasks.pop();
}
// 处理任务, 约定
// 处理任务需要再临界区内部处理吗?不需要
LOG(LogLevel::INFO) << name << "处理任务:";
task();
}
// 线程退出
LOG(LogLevel::INFO) << name << " quit...";
}
ThreadPool(int slaver_num = defaultnum) : _isrunning(false), _slaver_sleep_count(0), _slaver_num(slaver_num)
{
// ThreadPool对象已经存在了
for (int idx = 0; idx < _slaver_num; idx++)
{
// auto f = std::bind(&ThreadPool::HandlerTask, this);
// // auto f = [this](){
// // this->HandlerTask();
// // };
// _slavers.emplace_back(f);
_slavers.emplace_back([this]()
{ this->HandlerTask(); });
}
}
// 赋值 拷贝构造禁止
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
ThreadPool(const ThreadPool<T> &) = delete;
public:
// 如果多线程获取这个单例呢.加锁
// 多线程安全了,但是效率比较低,双if判断
static ThreadPool<T> *Instance()
{
if(nullptr == _instance) // 双if判断
{
// 多线程
LockGuard lockguard(_lock);
if (nullptr == _instance)
{
// 第一次调用
_instance = new ThreadPool<T>();
_instance->Start();
LOG(LogLevel::INFO) << "第一次使用线程池,创建线程池对象";
}
}
return _instance;
}
void Start()
{
if (_isrunning)
{
LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
return;
}
_isrunning = true;
for (auto &slave : _slavers)
{
slave.Start();
}
}
void Stop()
{
// version1 -- 后续调整
// if (!_isrunning)
// {
// LOG(LogLevel::WARNING) << "Thread Pool Is Not Running";
// return;
// }
// for (auto &slave : _slavers)
// {
// slave.Die(); // 太简单粗暴了
// }
// _isrunning = false;
// version 2
// 1. _isrunning = false
// 2. 处理完成tasks所有的任务
// 线程状态: 休眠,正在处理任务 -> 让所有线程全部唤醒
// HandlerTask自动break
_mutex.Lock();
_isrunning = false;
if (_slaver_sleep_count > 0)
_cond.Broadcast();
_mutex.Unlock();
}
void Wait()
{
for (auto &slave : _slavers)
{
slave.Join();
}
}
void Enqueue(T in)
{
_mutex.Lock();
_tasks.push(in);
if (_slaver_sleep_count > 0)
_cond.Signal();
_mutex.Unlock();
}
~ThreadPool()
{
}
private:
bool _isrunning;
int _slaver_num;
std::vector<Thread> _slavers;
std::queue<T> _tasks; // 任务队列,临界资源
Mutex _mutex;
Cond _cond;
int _slaver_sleep_count;
// 添加单例模式
static ThreadPool<T> *_instance;
static Mutex _lock; // 保证单例的安全
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template <typename T>
Mutex ThreadPool<T>::_lock;
}
cpp
//UdpServer.hpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static int default_port = 8888;
using handler_addr_t = std::function<void (const InetAddr &)>;
using handler_msg_t = std::function<void (int sokcfd, std::string msg)>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(uint16_t port = default_port)
: _port(port),
_sockfd(default_fd)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
InetAddr local(_port);
// 第三步:bind socket 信息
int n = bind(_sockfd, (struct sockaddr *)(local.GetNetAddress()), local.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
}
void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg)
{
_handler_addr = handler_addr;
_handler_msg = handler_msg;
}
void Start()
{
// 传递的是字符串,echo server
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 用户发来的数据
// 2. 用户的socket信息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
inbuffer[n] = 0;
// 1. 检测新用户
InetAddr clientaddress(peer);
std::string tips = clientaddress.ToString();
std::string message = tips + inbuffer;
LOG(LogLevel::DEBUG) << message;
_handler_addr(clientaddress);
// 2. 转发消息
// nickname# message -> [ip:port]-nickname# message
_handler_msg(_sockfd, message);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
// sleep(3);
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
uint16_t _port; // 用户设置好的,server port必须是固定的!
handler_addr_t _handler_addr;
handler_msg_t _handler_msg;
};
#endif
cpp
//UserManager.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
// class User
// {
// std::string username;
// std::string userstatus;
// InetAddr address;
// ...
// }
// 增删查改
class UserManager
{
public:
UserManager(){}
void AddUser(const InetAddr &addr)
{
if(SearchUser(addr))
return;
_users.push_back(addr);
}
void DelUser(const InetAddr &addr)
{
// if(!SearchUser(addr))
// return;
for(auto iter = _users.begin(); iter != _users.end(); iter++)
{
if(*iter == addr)
{
_users.erase(iter);
break;
}
}
}
bool SearchUser(const InetAddr &addr)
{
for(auto &user : _users)
{
if(user == addr)
{
return true;
}
}
return false;
}
bool ModUser(const InetAddr &addr)
{
// 简单一点
DelUser(addr);
AddUser(addr);
return true;
}
std::vector<InetAddr> &Users()
{
return _users;
}
~UserManager(){}
private:
// "ip:port" -> InetAddr
std::vector<InetAddr> _users;
};
这套 V3 简单聊天室 ,已经不是单纯的 UDP 收发示例,而是一个比较完整的"小型服务器工程"了.它把前面 V1/V2 学到的 UDP、封装、回调 继续往前推进,又加入了 用户管理、消息广播、线程池、互斥锁、条件变量和客户端双线程.
先抓住它的整体运行链:
text
ChatClient
│
│ sendto()
▼
UdpServer
│
│ recvfrom()
▼
InetAddr 封装客户端地址
│
├──────────────► Route::CheckUser()
│ │
│ ▼
│ UserManager
│ 维护在线用户
│
└──────────────► ThreadPool::Enqueue()
│
▼
工作线程
│
▼
Route::Broadcast()
│
▼
遍历所有在线用户
│
sendto()
↙ ↓ ↘
Client A Client B Client C
这里最重要的一点是:UDP 本身没有"连接"概念,所以服务器实际上是通过客户端的 IP + Port 来识别和管理用户的.
一、项目中每个文件分别干什么
| 文件 | 核心职责 |
|---|---|
ChatClient.cc |
聊天客户端,负责上线、发送、接收消息 |
ChatServerMain.cc |
服务端入口,负责组装所有模块 |
UdpServer.hpp |
UDP 网络层,负责 socket、bind、recvfrom |
InetAddr.hpp |
封装 IP + Port |
Route.hpp |
聊天业务层,负责用户检查和消息广播 |
UserManager.hpp |
维护在线用户列表 |
ThreadPool.hpp |
将广播任务交给工作线程异步执行 |
Thread.hpp |
对 pthread 线程进行 C++ 封装 |
Mutex.hpp |
互斥锁及 RAII 加锁封装 |
Cond.hpp |
条件变量,用于线程等待和唤醒 |
Logger.hpp |
日志输出 |
Makefile |
编译 server_chat 和 client_chat |
二、ChatClient.cc ------ 客户端核心
客户端承担两件事情:
text
发送消息
+
接收消息
而且这两件事情必须同时进行.
假如客户端只有一个线程:
text
getline()
↓
等待用户输入
↓
sendto()
↓
recvfrom()
那么程序等待你输入的时候,就没办法及时显示其他人的消息.
所以我的设计是:
text
ChatClient
┌───────────┐
│ UDP socket│
└─────┬─────┘
│
┌─────────┴─────────┐
▼ ▼
RecvMessage() SendMessage()
接收线程 发送线程
│ │
recvfrom() getline()
│ │
cout显示 sendto()
这是客户端代码最核心的思想.
三、Online() ------ 告诉服务器"我上线了"
函数:
cpp
static void Online(InetAddr &serveraddr)
首先:
cpp
std::cout << "Please Set Your Nick Name# ";
std::getline(std::cin, nickname);
取得昵称.
例如:
text
Alice
然后:
cpp
std::string online_message =
nickname + " online!";
形成:
text
Alice online!
再:
cpp
sendto(sockfd,
online_message.c_str(),
online_message.size(),
0,
(struct sockaddr *)serveraddr.GetNetAddress(),
serveraddr.Len());
发送到服务器.
所以:
text
Alice 客户端
Alice online!
│
│ UDP
▼
Server
不过这里有一个非常重要的细节:
服务器并没有真正解析 "online!" 关键字.
你的 UdpServer 每收到一个客户端的数据,都会:
cpp
_handler_addr(clientaddress);
所以本质上是:客户端第一次发消息时,服务器根据这个客户端的 IP + Port 将它加入在线用户列表。
"Alice online!" 更像是一条约定好的"第一条消息".
四、SendMessage() ------ 客户端发送线程
首先:
cpp
InetAddr serveraddr(server_port, server_ip);
创建服务器地址对象.
然后:
cpp
Online(serveraddr);
发送上线消息.
之后进入:
cpp
while (true)
不断读取:
cpp
std::getline(std::cin, message);
如果用户输入:
text
hello
代码:
cpp
message = nickname + "#" + message;
会形成:
text
Alice#hello
然后:
cpp
sendto(...)
发送到服务器.
所以发送线程的完整逻辑是:
text
启动
│
▼
输入 nickname
│
▼
发送上线消息
│
▼
while(true)
│
├── getline()
│
├── nickname + "#" + message
│
└── sendto()
注意:昵称并没有保存在服务器的 UserManager 中。
服务器只记录:
text
IP + Port
昵称只是客户端自己拼到消息正文里面.
五、RecvMessage() ------ 客户端接收线程
另外一个线程一直执行:
cpp
recvfrom(...)
例如:
cpp
ssize_t m = recvfrom(
sockfd,
inbuffer,
sizeof(inbuffer) - 1,
0,
(struct sockaddr *)&temp,
&len
);
如果收到数据:
cpp
inbuffer[m] = 0;
std::cerr << inbuffer << std::endl;
就直接打印.
所以这个线程完全不负责发送:
text
while(true)
│
▼
recvfrom()
│
▼
收到服务器广播
│
▼
打印到屏幕
例如 Bob 的客户端收到:
text
[127.0.0.1:52318]Alice#hello
就直接显示.
六、客户端为什么创建两个 Thread
main 中:
cpp
Thread recver(RecvMessage);
Thread sender(SendMessage);
创建:
text
recver
↓
RecvMessage()
sender
↓
SendMessage()
然后:
cpp
recver.Start();
sender.Start();
形成真正的并发:
text
线程1 线程2
RecvMessage() SendMessage()
│ │
recvfrom() getline()
│ │
打印消息 sendto()
│ │
recvfrom() getline()
│ │
... ...
最后:
cpp
recver.Join();
sender.Join();
等待两个线程.
由于两个函数基本都是无限循环,所以正常情况下 Join() 不会返回.
七、ChatServerMain.cc ------ 整个服务器的组装者
这个文件本身几乎不处理具体网络数据.
它承担的是:把各个模块组装起来.
这一点和前面的 DictServer 封装版思想是一样的.
首先:
cpp
uint16_t server_port = std::stoi(argv[1]);
取得:
text
./server_chat 8888
↓
8888
然后:
cpp
ENABLE_CONSOLE_LOG_STRATEGY();
开启日志.
接下来:
cpp
auto thread_pool =
ThreadPool<task_t>::Instance();
取得线程池单例.
注意你的 Instance() 内部在第一次创建线程池后调用:
cpp
instance->Start();
所以第一次取得线程池对象时,工作线程已经被启动了.
然后:
cpp
Route r;
创建聊天业务模块.
再:
cpp
UdpServer usvr(server_port);
usvr.Init();
创建并初始化 UDP Server.
八、最关键的 RegisterService()
这里是整个 V3 架构的核心:
cpp
usvr.RegisterService(
[&r](const InetAddr &addr)
{
r.CheckUser(addr);
},
[&r, thread_pool](int sockfd, std::string msg)
{
auto t =
std::bind(&Route::Broadcast,
&r,
sockfd,
msg);
thread_pool->Enqueue(t);
}
);
实际上给 UdpServer 注册了两个回调.
第一个回调:检查用户
收到 UDP 数据以后:
cpp
r.CheckUser(addr);
也就是:
text
UdpServer
│
│ 客户端地址
▼
Route::CheckUser()
│
▼
UserManager
确保这个客户端已经进入在线用户列表.
第二个回调:处理聊天消息
没有直接:
cpp
r.Broadcast(...)
而是:
cpp
std::bind(...)
先把广播操作包装成一个任务:
text
task
=
Route::Broadcast(sockfd, msg)
然后:
cpp
thread_pool->Enqueue(t);
放进线程池.
因此主线程只需要:
text
收到消息
↓
生成广播任务
↓
扔进线程池
↓
继续 recvfrom()
不需要自己完成整个广播过程.
九、UdpServer.hpp ------ 网络层核心
这个文件和前面 V1/V2 的 UDP Server 有明显演进.
它现在只负责:
text
创建 socket
绑定端口
接收数据
封装客户端地址
调用业务回调
而不负责具体:
text
聊天室怎么广播
用户怎么保存
线程池怎么运行
这就是网络层和业务层解耦.
handler_addr_t
定义:
cpp
using handler_addr_t =
std::function<void(const InetAddr &)>;
表示一个:
text
接收 InetAddr
返回 void
的业务函数.
这里实际绑定的是:
cpp
Route::CheckUser()
handler_msg_t
定义:
cpp
using handler_msg_t =
std::function<void(int sockfd,
std::string msg)>;
表示:
text
socket + message
↓
业务处理
实际绑定的是:
text
创建 Broadcast 任务
↓
ThreadPool::Enqueue()
十、UdpServer::Init() ------ 创建服务器
首先:
cpp
_sockfd =
socket(AF_INET, SOCK_DGRAM, 0);
创建 UDP Socket.
然后:
cpp
InetAddr local(_port);
这里相当于:
text
0.0.0.0:8888
再:
cpp
bind(
_sockfd,
(struct sockaddr *)local.GetNetAddress(),
local.Len()
);
完成绑定.
所以:
text
UdpServer::Init()
socket()
↓
InetAddr local
↓
0.0.0.0:8888
↓
bind()
十一、UdpServer::Start() ------ 网络事件循环
进入:
cpp
while (true)
以后不断:
cpp
recvfrom(...)
取得两个东西:
text
数据
+
客户端 sockaddr_in
例如:
text
数据:
Alice#hello
客户端地址:
127.0.0.1:45879
然后:
cpp
InetAddr clientaddress(peer);
把原始:
cpp
sockaddr_in
封装为:
cpp
InetAddr
接下来:
cpp
std::string tips =
clientaddress.ToString();
得到:
text
[127.0.0.1:45879]
再:
cpp
std::string message =
tips + inbuffer;
最终可能形成:
text
[127.0.0.1:45879]Alice#hello
然后:
cpp
_handler_addr(clientaddress);
确保用户存在.
最后:
cpp
_handler_msg(_sockfd, message);
把真正消息交给业务层.
整个网络层:
text
recvfrom
│
▼
sockaddr_in
│
▼
InetAddr
│
├──► handler_addr
│
└──► handler_msg
十二、InetAddr.hpp ------ 网络地址封装
这是一个非常值得学习的小封装.
以前你需要到处操作:
cpp
struct sockaddr_in
然后:
cpp
htons()
ntohs()
inet_addr()
inet_ntoa()
现在统一放到:
cpp
class InetAddr
里面.
从客户端地址构造
cpp
InetAddr(const struct sockaddr_in &address)
把内核返回的:
text
sockaddr_in
转换为:
text
_ip
_port
_address
_len
其中:
cpp
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);
所以业务层以后不需要直接处理字节序.
根据 IP + Port 构造
cpp
InetAddr(uint16_t port,
const std::string &ip = "0.0.0.0")
内部完成:
cpp
_address.sin_family = AF_INET;
_address.sin_port = htons(_port);
_address.sin_addr.s_addr =
inet_addr(_ip.c_str());
例如:
cpp
InetAddr addr(8888, "127.0.0.1");
代表:
text
127.0.0.1:8888
operator==
cpp
return
_ip == addr._ip &&
_port == addr._port;
这对聊天室非常重要.
因为 UDP 没有连接对象,所以:
text
IP + Port
就是一个客户端的"身份".
比如:
text
127.0.0.1:50001
和:
text
127.0.0.1:50002
虽然 IP 一样,也是两个不同客户端.
ToString()
将:
text
IP + Port
转换为:
text
[127.0.0.1:50001]
方便日志和聊天消息展示.
GetNetAddress() 和 Len()
这两个主要是给:
cpp
bind()
sendto()
recvfrom()
这些原生 socket API 使用.
所以 InetAddr 本质就是:C++ 业务代码和底层 sockaddr_in 之间的一层适配器。
十三、UserManager.hpp ------ 在线用户管理
服务器怎么知道应该广播给谁?
就是靠:
cpp
std::vector<InetAddr> _users;
保存所有客户端地址.
所以所谓"在线用户"实际上是:
text
_users
[127.0.0.1:50001]
[127.0.0.1:50002]
[192.168.1.5:52001]
...
这里没有存昵称.
SearchUser()
遍历:
cpp
for(auto &user : _users)
通过:
cpp
user == addr
判断:
text
IP 相同
+
Port 相同
就认为是同一个客户端.
AddUser()
首先:
cpp
if(SearchUser(addr))
return;
防止重复添加.
否则:
cpp
_users.push_back(addr);
所以即使 UdpServer 每次收到消息都执行:
cpp
Route::CheckUser()
也不会不断产生重复用户。
DelUser()
遍历在线用户:
cpp
for(...)
找到之后:
cpp
_users.erase(iter);
用于用户下线.
Users()
cpp
std::vector<InetAddr> &Users()
直接把在线用户表交给 Route.
于是 Route 可以:
text
获取在线用户
↓
逐个 sendto
十四、Route.hpp ------ 聊天室真正的业务层
如果说:
text
UdpServer = 网络层
那么:
text
Route = 业务层
聊天室"应该怎么处理消息"全部在这里.
Route::CheckUser()
代码实际上是:
cpp
LockGuard lockguard(_lock);
_uma->AddUser(addr);
所以这个函数名字虽然叫:
text
CheckUser
实际更准确的意思是:EnsureUser:确保用户已经存在。
如果没有:
text
加入
如果已有:
text
什么都不做
Route::OfflineUser()
执行:
cpp
_uma->DelUser(addr);
负责删除用户.
不过你目前的整个客户端/服务器协议里,还没有真正调用这个接口.
这点非常重要.
UDP 没有 TCP 那种:
text
连接断开
FIN
EOF
所以服务器不知道一个 UDP 客户端什么时候真正退出.
因此当前代码虽然写了:
cpp
OfflineUser()
但是实际上还需要设计类似:
text
Alice offline!
这样的离线协议,然后服务器主动调用它.
否则客户端退出之后,地址仍可能留在:
cpp
_users
中.
十五、Route::Broadcast() ------ 聊天室的核心
这就是聊天广播功能:
cpp
auto &users = _uma->Users();
for(auto &user : users)
{
sendto(...);
}
假设当前在线:
text
Alice
Bob
Carol
Alice 发:
text
hello
服务器收到后最终:
text
Broadcast()
┌────────► Alice
│
hello ───────┼────────► Bob
│
└────────► Carol
这就是聊天室最核心的业务.
注意:
cpp
LockGuard lockguard(_lock);
会在广播期间锁住用户列表。
这样可以防止:
text
线程 A 正在遍历 users
同时
线程 B AddUser / DelUser
造成 vector 迭代器失效或数据竞争.
十六、为什么需要 Mutex.hpp
聊天室已经进入多线程环境.
例如:
text
工作线程1
正在 Broadcast
工作线程2
正在 CheckUser
工作线程3
可能正在 OfflineUser
它们都可能操作:
cpp
_users
如果没有锁:
text
线程 A 线程 B
遍历 vector push_back
│ │
└──── 同时发生 ───────┘
可能导致数据竞争
甚至 vector 扩容导致迭代器失效
所以你封装:
cpp
class Mutex
提供:
cpp
Lock();
Unlock();
底层实际上就是:
cpp
pthread_mutex_lock()
pthread_mutex_unlock()
十七、LockGuard ------ RAII 自动锁
比:
cpp
mutex.Lock();
...
mutex.Unlock();
更推荐的是:
cpp
{
LockGuard guard(mutex);
// 临界区
}
因为构造:
cpp
_lockref.Lock();
离开作用域:
cpp
~LockGuard()
{
_lockref.Unlock();
}
实现:
text
进入作用域
↓
自动 Lock
执行代码
离开作用域
↓
自动 Unlock
这就是 RAII.
最大的优势是:
text
return
异常
break
continue
都不容易导致忘记解锁.
十八、Cond.hpp ------ 条件变量
条件变量主要不是给 Route 用的,而是给:
text
ThreadPool
使用.
线程池有一个任务队列:
cpp
std::queue<T> _tasks;
如果没有任务,工作线程没有必要:
text
while(true)
不停检查
不停检查
不停检查
这样会浪费 CPU.
所以:
cpp
_cond.Wait(_mutex);
让工作线程睡眠.
流程:
text
工作线程
│
检查 tasks
│
├── 有任务 ──► 执行
│
└── 没任务
│
▼
Cond::Wait()
│
▼
睡眠
生产者提交任务:
cpp
Enqueue()
以后:
cpp
_cond.Signal();
唤醒一个工作线程.
因此:
text
ThreadPool
网络线程 Worker
│ │
Enqueue(task) │
│ │
push task │
│ │
Signal() ────────────────────►│
│
Wake Up
│
▼
执行 task
十九、pthread_cond_wait() 为什么需要 Mutex
封装:
cpp
void Wait(Mutex &mutex)
{
pthread_cond_wait(
&_cond,
mutex.Ptr()
);
}
这个函数非常特殊.
它会:
text
1. 自动释放 mutex
2. 线程进入等待
3. 收到 Signal
4. 重新获得 mutex
5. pthread_cond_wait 返回
也就是:
text
持有锁
│
▼
cond_wait
│
├── 释放锁
├── 睡眠
├── 被唤醒
└── 重新加锁
│
▼
继续执行
这正是生产者---消费者模型的标准写法.
二十、Thread.hpp ------ pthread 的 C++ 封装
这里进一步对:
cpp
pthread_t
进行了封装。
主要成员包括:
text
_name
_tid
_status
_joinable
_cb
_result
其中最重要的是:
cpp
callback_t _cb;
即:
cpp
std::function<void()>
所以创建线程时不用再直接写复杂的:
cpp
void *ThreadRoutine(void *)
而可以直接:
cpp
Thread t([]()
{
// 线程任务
});
Start()
内部:
cpp
pthread_create(
&_tid,
nullptr,
ThreadRoutine,
this
);
真正创建线程.
ThreadRoutine()
pthread 要求入口是:
cpp
void *(*)(void *)
所以这里设置一个静态函数:
cpp
static void *ThreadRoutine(void *args)
然后:
cpp
Thread *self =
static_cast<Thread *>(args);
找回当前对象.
最终:
cpp
self->_cb();
执行真正业务.
因此这个封装完成:
text
pthread 要求的 C 风格接口
│
▼
ThreadRoutine
│
▼
std::function<void()>
│
▼
普通 C++ 回调函数
Join()
底层:
cpp
pthread_join()
等待线程结束.
Detach()
底层:
cpp
pthread_detach()
将线程设置为分离状态.
Die()
使用:
cpp
pthread_cancel()
尝试取消线程.
这部分属于线程生命周期管理.
二十一、ThreadPool.hpp ------ 服务端并发处理核心
这是整个 V3 中复杂度最高的模块之一.
线程池解决的问题是:不要每收到一条聊天消息,就创建一个新线程.
如果:
text
消息1 → pthread_create
消息2 → pthread_create
消息3 → pthread_create
消息4 → pthread_create
...
频繁创建/销毁线程代价很高.
于是提前创建:
text
Worker1
Worker2
Worker3
Worker4
Worker5
然后:
text
聊天任务
聊天任务
聊天任务
│
▼
任务队列
│
├──► Worker1
├──► Worker2
└──► Worker3
这就是线程池.
二十二、线程池里的任务 T
这是模板:
cpp
template <typename T>
class ThreadPool
你服务器定义:
cpp
using task_t =
std::function<void()>;
所以:
text
T
=
std::function<void()>
一个任务实际上就是:
cpp
Route::Broadcast(...)
被 std::bind 包装后的可调用对象.
二十三、ThreadPool 构造函数
构造时根据:
cpp
_slaver_num
创建多个:
cpp
Thread
每个线程执行:
cpp
this->HandlerTask();
所以:
text
ThreadPool
│
├── Thread 1 → HandlerTask()
├── Thread 2 → HandlerTask()
├── Thread 3 → HandlerTask()
├── Thread 4 → HandlerTask()
└── Thread 5 → HandlerTask()
这里代码把 Worker 命名为:
text
slaver
概念上就是工作线程 Worker.
二十四、ThreadPool 是单例
代码:
cpp
static ThreadPool<T> *Instance()
保证整个进程中只有一个线程池.
调用:
cpp
ThreadPool<task_t>::Instance();
第一次:
text
instance == nullptr
│
▼
new ThreadPool
│
▼
Start()
之后:
text
直接返回已有 instance
所以服务器所有消息共享同一个线程池.
代码这里还用了:
cpp
static Mutex _lock;
保护单例初始化,采用了类似 double-check 的写法.
二十五、HandlerTask() ------ Worker 线程真正工作的地方
每个 Worker 一直:
cpp
while(true)
首先加锁:
cpp
LockGuard lockguard(_mutex);
然后检查:
cpp
_tasks.empty()
如果:
text
任务队列为空
+
线程池仍运行
就:
cpp
_cond.Wait(_mutex);
休眠.
有任务之后:
cpp
task = _tasks.front();
_tasks.pop();
把任务从队列取出来.
然后非常重要:执行任务是在锁外.
也就是:
text
加锁
│
取任务
│
解锁
│
▼
task()
而不是:
text
加锁
│
task()
│
解锁
这样一个 Worker 广播的时候,不会把整个任务队列锁死.
这是正确的线程池设计思想.
二十六、Enqueue() ------ 提交任务
ChatServerMain 中:
cpp
thread_pool->Enqueue(t);
最终执行:
cpp
_mutex.Lock();
_tasks.push(in);
if(_slaver_sleep_count > 0)
{
_cond.Signal();
}
_mutex.Unlock();
含义:
text
广播任务
│
▼
锁住队列
│
▼
push
│
▼
如果有 Worker 在睡觉
│
▼
Signal()
│
▼
解锁
所以线程池实现了一个标准的:
text
生产者 消费者
UdpServer Worker Threads
Enqueue(task)
│
▼
┌───────────┐
│Task Queue │
└───────────┘
│
▼
HandlerTask()
│
▼
Broadcast()
二十七、Start / Stop / Wait
Start():
cpp
_isrunning = true;
然后:
cpp
slave.Start();
启动所有 Worker.
Stop():
cpp
_isrunning = false;
如果有 Worker 正在等待:
cpp
_cond.Broadcast();
把所有线程叫醒,让它们检查:
text
线程池已经停止
+
没有剩余任务
然后退出.
Wait():
cpp
slave.Join();
等待所有 Worker 结束.
这是一个比较完整的线程池生命周期模型.
二十八、Logger.hpp ------ 日志模块
Logger 和聊天室业务没有直接关系,但服务器开发非常需要.
支持:
text
DEBUG
INFO
WARNING
ERROR
FATAL
可以打印:
text
时间
日志等级
线程/进程信息
源文件
代码行
具体内容
例如:
text
[INFO] [UdpServer.hpp] - bind socket success
Logger 还抽象了输出策略,大致是:
text
Logger
│
LogStrategy
/ \
/ \
Console File
Strategy Strategy
所以可以:
cpp
ENABLE_CONSOLE_LOG_STRATEGY();
让日志输出到控制台.
这样的意义在多线程服务器里尤其明显,因为:
text
谁收到了消息
哪个线程正在处理
什么时候发生错误
都可以通过日志定位.
二十九、整个聊天室一次消息到底怎么跑
假设有:
text
Alice
Bob
Carol
三个人在线.
Alice 输入:
text
hello
客户端首先形成:
text
Alice#hello
之后真正调用链是:
text
Alice
│
│ SendMessage()
│
│ sendto("Alice#hello")
▼
──────────────────────── UDP ────────────────────────
│
▼
UdpServer::Start()
│
│ recvfrom()
▼
得到 peer
│
▼
InetAddr(peer)
│
▼
[127.0.0.1:50001]
│
▼
message =
"[127.0.0.1:50001]Alice#hello"
│
├──────────────► Route::CheckUser()
│ │
│ ▼
│ UserManager
│ │
│ 已存在 → 不重复添加
│
▼
handler_msg()
│
▼
std::bind(Route::Broadcast)
│
▼
生成 task
│
▼
ThreadPool::Enqueue(task)
│
▼
Task Queue
│
▼
某一个 Worker 被唤醒
│
▼
HandlerTask()
│
▼
task()
│
▼
Route::Broadcast()
│
▼
UserManager::Users()
│
├──────── sendto ───────► Alice
│
├──────── sendto ───────► Bob
│
└──────── sendto ───────► Carol
│
▼
RecvMessage()
│
▼
cout
这就是 V3 的核心.

只要Alice发一条消息,Bob和Carol都能在自己的终端看到,就说明V3简单聊天室的核心功能已经运行测试成功.
二、补充相关参考内容
2.1地址转换函数
可以把这部分理解成:地址转换函数负责在"人能看懂的 IP 字符串"和"内核/socket 使用的二进制 IP 地址"之间做转换.
例如我们平时写:
text
127.0.0.1
192.168.1.10
这是点分十进制字符串 ;而 sockaddr_in 中:
cpp
struct in_addr sin_addr;
保存的是 IPv4 的 32 位二进制地址.
1.字符串 → 网络地址
cpp
inet_aton("127.0.0.1", &addr.sin_addr);
cpp
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
cpp
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
作用都是把:
text
"127.0.0.1"
转换成 struct in_addr 使用的二进制 IP.
其中更推荐:
cpp
inet_pton()
因为它同时支持 IPv4 和 IPv6.
2.网络地址 → 字符串
cpp
inet_ntoa(addr.sin_addr);
或者:
cpp
char ip[INET_ADDRSTRLEN];
inet_ntop(
AF_INET,
&addr.sin_addr,
ip,
sizeof(ip)
);
作用是把二进制 IP 转回:
text
"127.0.0.1"
更推荐:
cpp
inet_ntop()
最核心记忆
text
字符串 IP
│
│ inet_pton
▼
struct in_addr
│
│ inet_ntop
▼
字符串 IP
即:
text
inet_pton:字符串 → 网络地址
inet_ntop:网络地址 → 字符串
2.2关于inet_ntoa
inet_ntoa这个函数返回了⼀个char*,很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的结果.那么是否需要调⽤者⼿动释放呢?

man⼿册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区.这个时候不需要我们⼿动进⾏释放.那么问题来了,如果我们调⽤多次这个函数,会有什么样的效果呢?参⻅如下代码:
cpp
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
char *ptr1 = inet_ntoa(addr1.sin_addr);
char *ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2);
return 0;
}
bash
ptr1: 255.255.255.255, ptr2: 255.255.255.255
因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区,这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果.
多线程调⽤inet_ntoa代码⽰例如下:
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr1: %s\n", ptr);
}
return NULL;
}
void *Func2(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr2: %s\n", ptr);
}
return NULL;
}
int main()
{
pthread_t tid1 = 0;
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_create(&tid1, NULL, Func1, &addr1);
pthread_t tid2 = 0;
pthread_create(&tid2, NULL, Func2, &addr2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
2.3remove_if 样例
cpp
#include <iostream>
#include <list>
#include <memory>
#include <algorithm>
int main()
{
std::list<std::shared_ptr<int>> ls;
ls.push_back(std::make_shared<int>(1));
ls.push_back(std::make_shared<int>(2));
ls.push_back(std::make_shared<int>(3));
ls.push_back(std::make_shared<int>(4));
ls.push_back(std::make_shared<int>(4));
ls.push_back(std::make_shared<int>(4));
ls.push_back(std::make_shared<int>(5));
ls.push_back(std::make_shared<int>(6));
for (auto &v : ls)
{
std::cout << *v << std::endl;
}
std::cout << "aa: " << ls.size() << std::endl;
std::cout << "\n";
// int a = 3;
int a = 4;
auto pos = remove_if(
ls.begin(),
ls.end(),
[&a](const std::shared_ptr<int> &elem) -> bool
{
return a == *elem;
});
ls.erase(pos, ls.end());
std::cout << "aa: " << ls.size() << std::endl;
for (auto &v : ls)
{
std::cout << *v << std::endl;
}
return 0;
}
remove_if()并不会实际移除序列[start, end)中的元素;如果在一个容器上应用remove_if(),容器的长度并不会改变,所有的元素都还在容器里面(但是逻辑上已经无法访问).
remove_if()将所有应该移除的元素都移动到容器尾部并返回一个分界的迭代器.为了实际移除元素,你必须对容器自行调用erase()以擦除需要移除的元素.
这段代码的核心就是经典的:
cpp
auto pos = std::remove_if(...);
ls.erase(pos, ls.end());
也就是 erase-remove 惯用法.
三、⽹络命令
3.1Ping命令
ping 命令主要用来测试 目标主机是否可达、网络延迟大概多少、是否存在丢包.
基本用法
bash
ping 目标IP
例如:
bash
ping 127.0.0.1
或者:
bash
ping 192.168.1.10
也可以测试域名:
bash
ping www.baidu.com

bash
//+-c+数字等于ping多少次数
ping -c5 www.baidu.com

工作原理
ping 使用的是 ICMP 协议,不是TCP,也不是UDP.
大致过程:
text
本机
│
│ ICMP Echo Request
▼
目标主机
│
│ ICMP Echo Reply
▼
本机
所以它本质是在问:"你在线吗?"
对方回复:"我在线。"
输出怎么看
例如:
text
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.035 ms
几个重点字段:
icmp_seq=1:第几个 ICMP 包ttl=64:数据包还能经过多少跳time=0.035 ms:往返延迟
停止 ping
Linux 默认会一直 ping:
bash
ping 127.0.0.1
按:
text
Ctrl + C
停止.
指定次数
例如只发 4 个包:
bash
ping -c 4 127.0.0.1
常见测试顺序
1.测试本机协议栈
bash
ping 127.0.0.1
成功说明本机 TCP/IP 协议栈基本正常.
2.测试局域网主机
bash
ping 192.168.1.100
3.测试公网 IP
bash
ping 8.8.8.8
4.测试域名
bash
ping www.baidu.com
如果:
bash
ping 8.8.8.8
能通,但:
bash
ping www.baidu.com
不通,通常优先怀疑 DNS 解析问题.
注意
ping 不通不一定说明目标主机真的不可达,因为有些服务器或防火墙会禁止 ICMP.
所以:
text
ping 不通
≠
TCP/UDP 服务一定不可用
一句话总结
text
ping = 用 ICMP 测试网络连通性、延迟和丢包情况
3.2netstat命令
netstat 用来查看 网络连接、监听端口、路由表、网络接口统计信息.
常用命令
查看所有网络连接:
bash
netstat -an
查看 TCP:
bash
netstat -ant
查看 UDP:
bash
netstat -anu
查看监听端口:
bash
netstat -lnt
查看监听的 UDP 端口:
bash
netstat -lnu
查看对应进程:
bash
netstat -antp
例如查看 8888 端口:
bash
netstat -anp | grep 8888
常见参数
text
-a 显示所有连接和监听端口
-n 直接显示 IP 和端口号,不做域名解析
-t TCP
-u UDP
-l 只显示监听状态
-p 显示进程 PID / 程序名
-r 查看路由表
-i 查看网络接口
常见状态
TCP 常见:
text
LISTEN 监听
ESTABLISHED 已建立连接
TIME_WAIT 等待连接彻底关闭
CLOSE_WAIT 等待本地程序关闭连接
UDP 常见:
text
UNCONN
因为 UDP 是无连接协议,属于正常现象.
例如你的 UDP 服务端运行在 8888:
bash
netstat -anup | grep 8888
可能看到:
text
udp 0 0 0.0.0.0:8888 0.0.0.0:* ...
表示服务端已经绑定 8888 端口.
现在 Linux 中更推荐使用:
bash
ss
例如:
bash
ss -lntp
ss -unlp
一句话记忆:netstat = 查看当前主机的网络连接和端口状态.
3.3pidof命令
pidof 用来根据进程名查找进程 PID.
基本用法
bash
pidof 进程名
例如:
bash
pidof bash
可能输出:
text
1234 5678
表示当前有两个 bash 进程.
查找自己写的程序
例如启动:
bash
./server_chat 8888
查询:
bash
pidof server_chat
可能得到:
text
24680
这个 24680 就是进程 PID.
常用搭配
查看进程:
bash
pidof server_chat
结束进程:
bash
kill $(pidof server_chat)
强制结束:
bash
kill -9 $(pidof server_chat)
常用选项
只返回一个 PID:
bash
pidof -s bash
忽略指定 PID:
bash
pidof -o 1234 bash
和 ps 的区别
bash
pidof server_chat
直接得到 PID.
而:
bash
ps aux | grep server_chat
会显示更详细的信息.
一句话记:pidof = 根据程序名快速找到进程PID.
四、验证UDP---Windows作为客户端访问Linux
cpp
//Makefile
.PHONY:all
all:tcp_server udp_server
tcp_server:TcpServer.cc
g++ -o $@ $^ -std=c++14
udp_server:UdpServer.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -rf tcp_server udp_server
cpp
//TcpClient.cc
#include <winsock2.h>
#include <iostream>
#include <string>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = ""; // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号
int main()
{
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
std::cerr << "WSAStartup failed: " << result << std::endl;
return 1;
}
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET)
{
std::cerr << "socket failed" << std::endl;
WSACleanup();
return 1;
}
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(serverport); // 替换为服务器端口
serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址
result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
if (result == SOCKET_ERROR)
{
std::cerr << "connect failed" << std::endl;
closesocket(clientSocket);
WSACleanup();
return 1;
}
while (true)
{
std::string message;
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
if(message.empty()) continue;
send(clientSocket, message.c_str(), message.size(), 0);
char buffer[1024] = {0};
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesReceived > 0)
{
buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾
std::cout << "Received from server: " << buffer << std::endl;
}
else
{
std::cerr << "recv failed" << std::endl;
}
}
closesocket(clientSocket);
WSACleanup();
return 0;
}
cpp
//TcpServer.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
const static int default_backlog = 6;
enum
{
Usage_Err = 1,
Socket_Err,
Bind_Err,
Listen_Err
};
#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
class TcpServer
{
public:
TcpServer(uint16_t port) : _port(port), _isrunning(false)
{
}
// 都是固定套路
void Init()
{
// 1. 创建socket, file fd, 本质是文件
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
exit(0);
}
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 2. 填充本地网络信息并bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
// 2.1 bind
if (bind(_listensock, CONV(&local), sizeof(local)) != 0)
{
exit(Bind_Err);
}
// 3. 设置socket为监听状态,tcp特有的
if (listen(_listensock, default_backlog) != 0)
{
exit(Listen_Err);
}
}
void ProcessConnection(int sockfd, struct sockaddr_in &peer)
{
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
std::string prefix = clientip + ":" + std::to_string(clientport);
std::cout << "get a new connection, info is : " << prefix << std::endl;
while (true)
{
char inbuffer[1024];
ssize_t s = ::read(sockfd, inbuffer, sizeof(inbuffer)-1);
if(s > 0)
{
inbuffer[s] = 0;
std::cout << prefix << "# " << inbuffer << std::endl;
std::string echo = inbuffer;
echo += "[tcp server echo message]";
write(sockfd, echo.c_str(), echo.size());
}
else
{
std::cout << prefix << " client quit" << std::endl;
break;
}
}
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 4. 获取连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listensock, CONV(&peer), &len);
if (sockfd < 0)
{
continue;
}
ProcessConnection(sockfd, peer);
}
}
~TcpServer()
{
}
private:
uint16_t _port;
int _listensock; // TODO
bool _isrunning;
};
using namespace std;
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n"
<< std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);
tsvr->Init();
tsvr->Start();
return 0;
}
cpp
//UdpClient.cc
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = ""; // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号
int main()
{
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport); //?
server.sin_addr.s_addr = inet_addr(serverip.c_str());
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == SOCKET_ERROR)
{
std::cout << "socker error" << std::endl;
return 1;
}
std::string message;
char buffer[1024];
while (true)
{
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
if(message.empty()) continue;
sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)&server, sizeof(server));
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}
cpp
//UdpServer.cc
#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
enum
{
Usage_Err = 1,
Socket_Err,
Bind_Err
};
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport)
: _port(port), _sockfd(defaultfd)
{
}
void Init()
{
// 1. 创建socket,就是创建了文件细节
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
exit(Socket_Err);
}
// 2. 绑定,指定网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local)); // memset
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 1. 4字节IP 2. 变成网络序列
// 结构体填完,设置到内核中了吗??没有
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n != 0)
{
exit(Bind_Err);
}
}
void Start()
{
// 服务器永远不退出
char buffer[defaultsize];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 不能乱写
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
std::string prefix = clientip + ":" + std::to_string(clientport);
buffer[n] = 0;
std::cout << prefix << "# " << buffer << std::endl;
std::string echo = buffer;
echo += "[udp server echo message]";
sendto(_sockfd, echo.c_str(), echo.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
uint16_t _port;
int _sockfd;
};
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n"
<< std::endl;
}
// ./udp_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}





🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容
每日心灵鸡汤: 做真实的自己,奔赴属于你的远方!
活出真实,接纳不完美的自己!不必伪装优秀,硬撑不属于你的模样;课堂上不懂就问,不必害怕别人笑话.允许考试有失误,接纳学习里的短板,不用刻意迎合,勉强融入不适合的圈子;正视自身优缺点,不盲目贬低或高估;累了可以停下休整,不必一直勉强硬扛;踏实做好分内之事,遵从内心稳步前行;不必事事追求亮眼,平凡努力同样可贵.坦然做真实的你,自在奔赴属于你的远方.
