【Linux】Socket编程-UDP构建自己的C++服务器
🌈 个人主页:Zfox_
🔥 系列专栏:Linux
目录
- 一:🔥 UDP 网络编程
- 🦋 接口讲解
- 🦋 V1 版本 - echo server
- 🦋 V2 版本 - DictServer
- 🦋 V3 版本 - 简单聊天室
- 二:🔥 观察者模式
- 三:🔥 补充参考内容
- 🦋 地址转换函数
- 🦋 关于 inet_ntoa
- 四:🔥 补充网络命令
- 🦋 Ping 命令
- 🦋 netstat
- 🦋 pidof
- 五:🔥 共勉
一:🔥 UDP 网络编程
🦋 接口讲解
socket
#include <sys/types.h>
#include <sys/socket.h>// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);domain: 域 / 协议家族AF_INET IPv4 Internet protocolsAF_INET6 IPv6 Internet protocolstype: 报文类型SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.protocol: 传输层类型默认为0On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
bind
#include <sys/types.h>
#include <sys/socket.h>// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);On success, zero is returned. On error, -1 is returned, and errno is set appropriately.// 2. 填充网络信息,并bind绑定
// 2.1 没有把socket信息设置进入内核
struct sockaddr_in local;
bzero(&local, sizeof(local)); // string.h
local.sin_family = AF_INET;
local.sin_port = ::htons(_port); // 要被发送给对方,既要发送到网络中! 主机序列转换为网络序列 大小端转换 网络中都是大端 #include <arpa/inet.h>
local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. string ip -> 4bytes 2. network order #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
local.sin_addr.s_addr = INADDR_ANY;// 2.1 bind 这里设置进入内核
int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
📚 我们之前在调用socket的时候,明明已经填充了一次 AF_INET, 为什么这里还需要一次呢?
创建套接字的时候填充的 AF_INET 是给操作系统文件系统里的网络文件接口,告诉我们的操作系统我们要创建一个网络的套接字。
这里则是用来填充 sockaddr_in 网络信息,只有套接字的结构和这里的结构一样,操作系统才能绑定成功。
📚 必带四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
recvfrom
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);sockfd: 指定的套接字
buf: 存放收到的消息内容缓冲区
len: 期望收取多少个字节
flag: 收方式,默认为0,阻塞收输出型参数:
sockaddr *src_addr: 谁发来的消息(ip + port)远端
addrlen:These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.
sendto
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);sockfd: 指定的套接字
buf: 发送的字符串
len:
flag: 收方式,默认为0,阻塞发输出型参数:
sockaddr *dest_addr: 目标主机(ip + port)远端
addrlen:
🦋 V1 版本 - echo server
简单的回显服务器和客户端代码
备注: 代码中会用到 地址转换函数 . 参考接下来的章节.
UdpServer.hpp
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <string.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;using func_t = std::function<std::string(const std::string&)>;class UdpServer
{
public:UdpServer(func_t func, uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false),_func(func){}// 都是套路void InitServer(){// 1. 创建socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;// 2. 填充网络信息,并bind绑定// 2.1 没有把socket信息设置进入内核// struct sockaddr_in local;// bzero(&local, sizeof(local));// local.sin_family = AF_INET;// local.sin_port = ::htons(_port); // 要被发送给对方,既要发送到网络中! 主机序列转换为网络序列 大小端转换 网络中都是大端// // local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. strinf ip -> 4bytes 2. network order// local.sin_addr.s_addr = INADDR_ANY;// 2.1 bind 这里设置进入内核int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if(n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while(true){char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设置ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer - 1), 0, CONV(&peer), &len);if(n > 0){inbuffer[n] = 0;std::string result = _func(inbuffer); // 回调 出去还会回来::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if(_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;// uint16_t _port; // 服务器未来的端口号// std::string _ip; // 服务器所对应的IPbool _isrunning; // 服务器运行状态// 业务 回调方法func_t _func;
};#endif
InetAddr.hpp
#pragma once#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"class InetAddr
{
private:void PortNettoHost(){_port = ::ntohs(_net_addr.sin_port);}void IpNettoHost(){char ipbuffer[64];const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));(void)ip;}public:InetAddr(){}InetAddr(const struct sockaddr_in &addr): _net_addr(addr){PortNettoHost();IpNettoHost();}InetAddr(uint16_t port): _port(port), _ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}struct sockaddr *NetAddr() { return CONV(&_net_addr); }socklen_t NetAddrLen() { return sizeof(_net_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port; }~InetAddr(){}private:struct sockaddr_in _net_addr;std::string _ip;uint16_t _port;
};
Comm.hpp
#pragma once#include <iostream>#define Die(code) \do \{ \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
- Log.hpp 前面的博客已经有了, 这里就不再复制粘贴了
- 云服务器不允许直接 bind 公有 IP, 我们也不推荐编写服务器的时候, bind 明确的 IP, 推荐直接写成
INADDR_ANY
C++
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)
在网络编程中, 当一个进程需要绑定一个网络端口以进行通信时, 可以使用
INADDR_ANY作为 IP 地址参数。 这样做意味着该端口可以接受来自任何 IP 地址的连接请求, 无论是本地主机还是远程主机。 例如, 如果服务器有多个网卡(每个网卡上有不同的 IP 地址) , 使用INADDR_ANY可以省去确定数据是从服务器上具体哪个 网卡/IP 地址上面获取的。
UdpServerMain.cc
#include "UdpServer.hpp"// ./server_udp localport
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);LogModule::ENABLE_CONSOLE_LOG();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
UdpClientMain.cc
#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建 socketint sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充server信息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());// 2. clientdonewhile (true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}
- 📚 client 端要不要显示 bind 的问题
client 不需要bind吗? socket <-> socket
client 必须也有自己的ip和端口! 客户端不需要自己显示的调用bind!!而是客户端首次sendto信息的时候,由OS自动进行bind
1. 如何理解client自动随机bind端口号 一个端口号,只能被一个进程bind
2. 如何理解server要显示的bind? 服务器的端口号,必须稳定!必须是众所周知且不能轻易改变的!
🦋 V2 版本 - DictServer
实现一个简单的英译汉的网络字典
dict.txt
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
...
Dictionary.hpp
#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "Common.hpp"const std::string gpath = "./";
const std::string gdictname = "dict.txt";
const std::string gsep = ": ";using namespace LockModule;class Dictionary
{
private:bool LoadDictionary(){std::string file = _path + _filename;std::ifstream in(file.c_str());if(!in.is_open()){LOG(LogLevel::ERROR) << "open file " << file << " error";return false;}// happy: 快乐的std::string line;while(std::getline(in, line)){std::string key;std::string value;if(SplitString(line, &key, &value, gsep)){_dictionary.insert(std::make_pair(key, value));} }in.close();return true;}
public:Dictionary(const std::string &path = gpath, const std::string &filename = gdictname):_path(path),_filename(filename){LoadDictionary();}std::string Translate(const std::string &word){auto iter = _dictionary.find(word);if(iter == _dictionary.end()) return "None";return iter->second;}~Dictionary(){}
private:std::unordered_map<std::string, std::string> _dictionary;std::string _path;std::string _filename;
};
UdpServer.hpp
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <string.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;using func_t = std::function<std::string(const std::string&)>;class UdpServer
{
public:UdpServer(func_t func, uint16_t port = gdefaultport):_sockfd(gsockfd),_addr(port),_isrunning(false),_func(func){}// 都是套路void InitServer(){// 1. 创建socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;// 2. 填充网络信息,并bind绑定// 2.1 没有把socket信息设置进入内核// struct sockaddr_in local;// bzero(&local, sizeof(local));// local.sin_family = AF_INET;// local.sin_port = ::htons(_port); // 要被发送给对方,既要发送到网络中! 主机序列转换为网络序列 大小端转换 网络中都是大端// // local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. strinf ip -> 4bytes 2. network order// local.sin_addr.s_addr = INADDR_ANY;// 2.1 bind 这里设置进入内核int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if(n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while(true){char inbuffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设置ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer - 1), 0, CONV(&peer), &len);if(n > 0){inbuffer[n] = 0;std::string result = _func(inbuffer); // 回调 出去还会回来::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if(_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;// uint16_t _port; // 服务器未来的端口号// std::string _ip; // 服务器所对应的IPbool _isrunning; // 服务器运行状态// 业务 回调方法func_t _func;
};#endif
UdpServerMain.cc
#include "UdpServer.hpp"
#include "Dictionary.hpp"// ./server_udp localport
int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " localport" << std::endl;Die(USAGE_ERR);}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);LogModule::ENABLE_CONSOLE_LOG();std::shared_ptr<Dictionary> dict_sptr = std::make_shared<Dictionary>();std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string &word) { return dict_sptr->Translate(word); }, port);// func_t f = std::bind(&Dictionary::Translate, dict_sptr.get(), std::placeholders::_1);// std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(f, port);svr_uptr->InitServer();svr_uptr->Start();return 0;
}
UdpClientMain.cc
#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建 socketint sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充server信息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());// 2. clientdonewhile (true){std::cout << "Please Enter# ";std::string message;std::getline(std::cin, message);// client 不需要bind吗? socket <-> socket// client 必须也有自己的ip和端口! 客户端不需要自己显示的调用bind!!// 而是客户端首次sendto信息的时候,由OS自动进行bind// 1. 如何理解client自动随机bind端口号 一个端口号,只能被一个进程bind// 2. 如何理解server要显示的bind? 服务器的端口号,必须稳定!必须是众所周知且不能轻易改变的!int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}
🦋 V3 版本 - 简单聊天室
UdpServer.hpp
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <string.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;using adduser_t = std::function<void(InetAddr &id)>;
using remove_t = std::function<void(InetAddr &id)>;using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string &message)>;// 编程技巧 继承nocopy的类就都不会被拷贝了
class nocopy
{
public:nocopy() {}nocopy(const nocopy &) = delete;const nocopy &operator=(const nocopy &) = delete;~nocopy() {}
};class UdpServer : public nocopy
{
public:UdpServer(uint16_t port = gdefaultport): _sockfd(gsockfd), _addr(port), _isrunning(false){}// 都是套路void InitServer(){// 1. 创建socket_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket: " << strerror(errno);Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind: " << strerror(errno);Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void RegisterService(adduser_t adduser, route_t route, remove_t removeuser){_adduser = adduser;_route = route;_removeuser = removeuser;}void Start(){_isrunning = true;while (true){char inbuffer[4096];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 必须设置ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);if (n > 0){InetAddr cli(peer);inbuffer[n] = 0;std::string message;if (strcmp(inbuffer, "QUIT") == 0){// 移除观察者_removeuser(cli);message = cli.Addr() + "# " + "我走了, 你们聊!";}else{// 2. 新增用户_adduser(cli);message = cli.Addr() + "# " + inbuffer;}// task_t task = [this, message]()// {// this->_route(this->_sockfd, message);// };// 3. 构建转发任务,推送给线程池,让线程池进行转发 线程池是无参数的所以要绑定task_t task = std::bind(UdpServer::_route, _sockfd, message);ThreadPool<task_t>::getInstance()->Equeue(task);// std::string echo_string = "echo# ";// echo_string += inbuffer;// ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd;InetAddr _addr;bool _isrunning; // 服务器运行状态// 新增用户adduser_t _adduser;// 移除用户remove_t _removeuser;// 数据转发route_t _route;
};#endif
UdpClientMain.cc
#include "UdpClient.hpp"
#include "Common.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>int sockfd = -1;
struct sockaddr_in server;void ClientQuit(int signo)
{(void)signo;const std::string quit = "QUIT";int n = ::sendto(sockfd, quit.c_str(), quit.size(), 0, CONV(&server), sizeof(server));(void)n;exit(0);
}void *Recver(void *args)
{(void)args;while (true){struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);if (n > 0){buffer[n] = 0;std::cerr << buffer << std::endl;}}
}// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;Die(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建 socketsockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;Die(SOCKET_ERR);}signal(2, ClientQuit);// 1.1 填充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());pthread_t tid;::pthread_create(&tid, nullptr, Recver, nullptr);// 1.2 启动的时候 给服务器推送消息即可const std::string online = " ... 来了哈!";int n = ::sendto(sockfd, online.c_str(), online.size(), 0, CONV(&server), sizeof(server));// 2. clientdonewhile (true){fprintf(stdout, "Please Enter# ");fflush(stdout);std::string message;std::getline(std::cin, message);int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));(void)n;}return 0;
}
User.hpp
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"using namespace LogModule;
using namespace LockModule;class UserInterface
{
public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string &message) = 0;virtual bool operator==(const InetAddr &u) = 0;virtual std::string Id() = 0;
};class User : public UserInterface
{
public:User(const InetAddr &id): _id(id){}virtual void SendTo(int sockfd, const std::string &message) override{LOG(LogLevel::DEBUG) << "send message to " << _id.Addr() << " info:" << message;int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}virtual bool operator==(const InetAddr &u) override{return _id == u;}virtual std::string Id() override{return _id.Addr();}~User(){}private:InetAddr _id;
};// 对用户消息进行路由
// UserManager 把所有用户先管理起来!
// 观察者模式 : observer
class UserManager
{
public:UserManager(){}void AddUser(InetAddr &id){LockGuard lockguard(_mutex);for (auto &user : _online_user){if (*user == id) // 父类的指针调用 必须重写 父类必须也重载 继承的话也得用子类的指针访问{LOG(LogLevel::INFO) << id.Addr() << " 用户已经存在";return;}}LOG(LogLevel::INFO) << " 新增该用户: " << id.Addr();_online_user.push_back(std::make_shared<User>(id));PrintUser();}void DelUser(InetAddr &id){auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface> &user){ return *user == id;});_online_user.erase(pos, _online_user.end());PrintUser();}// 通知观察者void Router(int sockfd, const std::string &message){LockGuard lockguard(_mutex);for(auto &user : _online_user){user->SendTo(sockfd, message);}}void PrintUser(){for(auto &user : _online_user){LOG(LogLevel::DEBUG) << "在线用户-> " << user->Id();}}~UserManager(){}private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
};
UDP 协议支持全双工, 一个 sockfd, 既可以读取, 又可以写入, 对于客户端和服务端同样如此多线程客户端, 同时读取和写入测试的时候, 使用管道进行演示

二:🔥 观察者模式
观察者模式(发布订阅模式)是一种行为型设计模式,用于定义对象之间的一种一对多的依赖关系,使得一个对象状态发生变化时,所有依赖它的对象都会收到通知并自动更新。
它的目的就是将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。
📚 观察者模式的特点:
松耦合: 观察者和被观察者之间是松耦合的,便于扩展和维护。动态订阅: 可以动态添加或移除观察者,灵活性高。单向通信: 被观察者通知观察者,观察者不能反向修改被观察者的状态。
📚 一般用在什么场景?
- 事件驱动系统:在用户操作界面中,通过监听事件(如按钮点击)触发响应。
- 系统间通信:系统中某个模块发生变化时,需要通知多个依赖模块。
- 分布式系统:数据更新时通知所有订阅者,例如推送通知、实时数据同步。
📚 典型场景:
- GUI 事件处理系统(如按钮点击、窗口关闭事件)。
- 数据模型与视图同步更新(如MVC架构中的数据绑定)。·股票价格更新通知订阅者。
在本篇博客中每一个user就是一个观察者,一旦收到了消息,就通知所有注册过的观察者。
三:🔥 补充参考内容
🦋 地址转换函数
本节只介绍基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位 的 IP 地址
但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和 in_addr 表示之间转换;
- 字符串转
in_addr的函数:

in_addr转字符串的函数:

🐮 其中
inet_pton和inet_ntop不仅可以转换 IPv4 的 in_addr, 还可以转换 IPv6 的 in6_addr, 因此函数接口是 void *addrptr。
- 📚 代码示例:

🦋 关于 inet_ntoa
inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?

man 手册上说, inet_ntoa 函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
📚 运行结果如下:
因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
- 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
- 在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数;
- 但是在 centos7 上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
- 大家可以自己写程序验证一下在自己的机器上 inet_ntoa 是否会出现多线程的问题;
- 在多线程环境下, 推荐使用
inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
📚 多线程调用 inet_ntoa 代码示例如下:(大家自行去测试)
#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;
}
四:🔥 补充网络命令
🦋 Ping 命令
Ping (Packet Internet Groper),因特网包探索器,用于测试网络连接量的程序。Ping 发送一个ICMP;回声请求消息给目的地并报告是否收到所希望的 ICMP echo (ICMP回声应答)。它是用来检查网络是否通畅或者网络连接速度的命令。
- 📚 Ping 的原理:
🎯 向指定的网络地址发送一定长度的数据包,按照约定,若指定网络地址存在的话,会返回同样大小的数据包,当然,若在特定时间内没有返回,就是“超时”,会被认为指定的网络地址不存在。
🦋 netstat
✨ netstat 是一个用来查看网络状态的重要工具.
📚 语法: netstat [选项]
📚 功能: 查看网络状态
✨ 常用选项:
- n 拒绝显示别名, 能显示数字的全部转化成数字
- l 仅列出有在 Listen (监听) 的服務状态
- p 显示建立相关链接的程序名
- t (tcp) 仅显示 tcp 相关选项
- u (udp) 仅显示 udp 相关选项
- a (all) 显示所有选项, 默认不显示 LISTEN 相关
// 每个 1s 执行一次 netstat -nltp
$ watch -n 1 netstat -nltp
🦋 pidof
✨ 在查看服务器的进程 id 时非常方便.
📚 语法: pidof [进程名]
📚 功能: 通过进程名, 查看进程 id
$ ps axj | head -1 && ps ajx | grep tcp_server
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2958169 2958285 2958285 2958169 pts/2 2958285 S+ 1002 0:00 ./tcp_server 8888 $ pidof tcp_server
2958285
五:🔥 共勉
以上就是我对 【【Linux】Socket编程-UDP构建自己的C++服务器 的理解,想要完整代码可以私信博主噢!觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

相关文章:
【Linux】Socket编程-UDP构建自己的C++服务器
🌈 个人主页:Zfox_ 🔥 系列专栏:Linux 目录 一:🔥 UDP 网络编程 🦋 接口讲解🦋 V1 版本 - echo server🦋 V2 版本 - DictServer🦋 V3 版本 - 简单聊天室 二&a…...
磁盘结构、访问时间、调度算法
目录 一、什么是磁盘? 二、磁盘分类 1、从磁头分 2、通过盘面分 三、一次磁盘读/写的时间 四、磁盘调度算法 1、先来先到服务算法FCFS 2、最短寻找时间优先SSTF 3、扫描算法(SCAN) 4、LOOk算法 5、循环扫描算法(C-SCAN…...
详解归并排序
归并排序 归并排序的基本概念归并排序的详细步骤1. 分解阶段2. 合并阶段3. 归并排序的递归流程 时间复杂度分析空间复杂度分析算法步骤2-路归并排序代码分析代码讲解1. 合并两个子数组的函数 merge()2. 归并排序函数 mergeSort()3. 打印数组的函数 printArray()4. 主函数 main(…...
45.在 Vue 3 中使用 OpenLayers 鼠标点击播放视频
引言 在 Web 开发中,地图可视化和互动功能是越来越重要的应用场景。OpenLayers 是一个强大的开源 JavaScript 库,用于显示和处理地图数据,支持多种地图服务和交互功能。在这个教程中,我们将介绍如何在 Vue 3 中集成 OpenLayers&a…...
《大话Java+playWright》系列教程初级篇-初识
后续代码会整理开源-大家期待吧!!! 首先讲下为啥不用python,因为不想下载各种安装插件,太麻烦了,好多不兼容。 所以选择了java。 先来讲下什么是playwright,playwright是微软开源自动化测试工…...
05.HTTPS的实现原理-HTTPS的握手流程(TLS1.2)
05.HTTPS的实现原理-HTTPS的握手流程(TLS1.2) 简介1. TLS握手过程概述2. TLS握手过程细化3. 主密钥(对称密钥)生成过程4. 密码规范变更 简介 主要讲述了混合加密流程完成后,客户端和服务器如何共同获得相同的对称密钥…...
提示词工程
一、六何分析法快速写出准确的提示词 英文单词中文解释提问时的思考示例Why何故问题的背景,包括为什么做及目标(做成什么样)最近我们要与某品牌合作推广冲牙器,对方需要我们策划一场营销活动What何事具体是什么事写一个营销策划方…...
基于python网络爬虫的搜索引擎设计
一、毕业设计(论文)题目:基于网络爬虫的搜索引擎设计 - 基于网络爬虫的搜索引擎设计1 二、毕业设计(论文)工作自 2022-09-01 起至 2022-10-28 止 三、毕业设计(论文)内容要求: 主…...
ip-协议
文章目录 1. 网络层2. ip协议2.1 ip协议格式2.2 网段划分基本概念网段划分的两种方式为什么要网段划分?特殊的IP地址IP地址数量不足 2.3 私有IP与公网IP2.4 路由 3. IP的分片与组装为什么要分片与组装?如何分片?如何组装? 1. 网络…...
Git(11)之log显示支持中文
Git(11)之log显示支持中文 Author:Once Day Date:2024年12月21日 漫漫长路有人对你微笑过嘛… 参考文档:GIT使用log命令显示中文乱码_gitlab的log在matlab里显示中文乱码-CSDN博客 全系列文章可查看专栏: Git使用记录_Once_day的博客-CSD…...
oneflow深度学习框架使用问题总结(Windows/Linux)
目录 1.简述 2.在Windows下使用Oneflow深度学习框架(错误记录,谨慎,官方不支持,需要WSL) 2.1安装Anaconda 2.1创建虚拟环境 2.2安装Pytorch 2.3安装Pycharm 2.4 安装Oneflow 3.在Linux下使用Oneflow深度学习框…...
论文研读:AnimateDiff—通过微调SD,用图片生成动画
1.概述 AnimateDiff 设计了3个模块来微调通用的文生图Stable Diffusion预训练模型, 以较低的消耗实现图片到动画生成。 论文名:AnimateDiff: Animate Your Personalized Text-to-Image Diffusion Models without Specific Tuning 三大模块: 视频域适应…...
SQLAlchemy示例(连接数据库插入表数据)
背景需求 连接数据库,插入表中一些数据。 其用户是新建用户,所以只能插入,不能更新。 再次输入数据则使用更新数据语法,这个没调试。 #! /usr/bin/env python # -*- coding: utf-8 -*-from sqlalchemy import create_engine, …...
Springboot3国际化
国际化实现步骤 Spring Boot 3 提供了强大的国际化支持,使得应用程序可以根据用户的语言和区域偏好适配不同的语言和地区需求。 添加国际化资源文件: 国际化资源文件通常放在 src/main/resources 目录下,并按照不同的语言和地区命名…...
阿尔萨斯(JVisualVM)JVM监控工具
文章目录 前言阿尔萨斯(JVisualVM)JVM监控工具1. 阿尔萨斯的功能2. JVisualVM启动3. 使用 前言 如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。 而且听说点赞的人每天的运气都不会太差ÿ…...
框架专题:反射
1. 什么是反射? 简单来说,反射是一种程序自省的能力,即在程序运行时动态地获取其结构信息或操作其行为。这包括类、方法、属性等元信息。反射的核心在于让代码变得更加动态化,从而突破静态语言的限制。 以Java为例,反…...
【Go】context标准库
文章目录 1. 概述1.1 什么是 Context1.2 设计原理1.3 使用场景源码分析核心:Context接口4个实现6个方法TODO 和 BackgroundWithCancelcancelpropagateCancel 绑定父对象WithTimeout 和 WithDeadlineWithValue总结参考1. 概述 基于版本: go1.22.3/src/context/context.go 1.1…...
LLMs之o3:《Deliberative Alignment: Reasoning Enables Safer Language Models》翻译与解读
LLMs之o3:《Deliberative Alignment: Reasoning Enables Safer Language Models》翻译与解读 导读:2024年12月,这篇论文提出了一种名为“审慎式对齐 (Deliberative Alignment)”的新方法,旨在提高大型语言模型 (LLM) 的安全性。论…...
git设置项目远程仓库指向github的一个仓库
要将你的Git项目设置为指向GitHub上的远程仓库,你需要执行以下步骤: 创建GitHub仓库: 登录到你的GitHub账户。点击右上角的 “” 号,选择 “New repository” 创建一个新的仓库。填写仓库的名称,可以添加描述ÿ…...
实战演练JDK的模块化机制
实战演练JDK的模块化机制--楼兰 带你聊最纯粹的Java 你发任你发,我用Java8。你用的JDK到什么版本了?很多开源框架都已经开始陆续升级JDK版本了。你对于JDK8往后陆陆续续更新的这些版本有什么感觉吗? 很多人会说其实并没有太多的感觉。JDK的新版本不断推出一些不痛不痒…...
利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...
基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...
离线语音识别方案分析
随着人工智能技术的不断发展,语音识别技术也得到了广泛的应用,从智能家居到车载系统,语音识别正在改变我们与设备的交互方式。尤其是离线语音识别,由于其在没有网络连接的情况下仍然能提供稳定、准确的语音处理能力,广…...


