🌟 各位看官好,我是!****
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦!
UDP网络编程
话不多说,我们直接编写代码,我们要实现的版本如下,再从实践转到理论中来:
v1版本 - Echo server(主要熟悉接口)
v2版本 - DictServer
v2版本 - DictServer封装版
v3版本 - 简单聊天室
Echo server
服务端
bash
static const int gdefaultsockfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip)
: _port(port),
_sockfd(gdefaultsockfd),
_ip(ip),
_isrunning(false)
{}
~UdpServer(){}
private:
int _sockfd;
uint16_t _port;
std::string _ip; // 暂时,"192.168.1.1"
bool _isrunning;
};
创建套接字
bash
// 1. 创建socket fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3
绑定套接字
bash
// 我们有没有实现,把socket和file关联起来呢??没有!!!
// 2. bind
// 2.1: 填充IP和Port
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
//inet_addr: 1. 字符串转整数ip 2. 整数ip是网络序列的
local.sin_addr = inet_addr(_ip.c_str());
// 2.2 和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
实际上local.sin_addr是有错误的,为什么呢?因为 sin_addr 本身是一个结构体(in_addr),而不是一个整数值。
不能直接将一个整数值赋值给一个结构体。s_addr 才是 in_addr 结构体中真正存放网络序IP地址的32位无符号整型成员。
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
不应该有个疑惑么?
为什么不直接local.s_addr呢?非要单独设计一个结构体?
历史导致的多样性:在4.2BSD的早期实现中,in_addr 结构体可能更复杂,它可能是一个联合体(union),
允许多种方式来解释同一个IP地址。
bash
local.sin_addr.s_addr = inet_addr(_ip.c_str());
要是绑ip地址,只能收到这个ip发来的消息.但是一台主机可能会被多个ip指向.只要是给我这个主机发的,我就都要!因此成员变量的ip就不需要啊
bash
local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind
正确代码:
bash
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind
// 2.2 和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
读取数据
UDP独有:
// 读取数据报
ssize_t recv(int sockfd, void buf.len, size_t len, int flags);
ssize_t recvfrom(int sockfd, void bufrestrict .len, size_t len, int flags, struct sockaddr *_Nullable restrict src_addr, socklen_t *_Nullable restrict addrlen);
参数:
- buf:输出型参数,把数据读取到缓冲区.
- flags:设为0,阻塞读
- restrict src_addr:当我收到消息内容的时候,未来我还有给别人会消息所以,我必须知道对方是谁! --> 我必须知道对方的socket信息,即IP+Port --> 因此是一个输出型参数,把对方的socket带出来.
- addrlen:输入输出型参数,把不真实的src_addr大小带进去,把真实的src_addr大小带出来.
返回值:
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. 返回小于0表示出错.
bash
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; // 清空缓冲区
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 读取数据
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer, &len);
if(n > 0)
{
//client是谁啊??ip和端口给我!
//...
}
}
发送数据
// 发送数据报
ssize_t send(int sockfd, const void buf.len, size_t len, int flags);
ssize_t sendto(int sockfd, const void buf.len, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
dest_addr:目标地址
addrlen:目标地址长度
结论1:sockfd既可以读,又可以写,UDP支持全双工通信
不需要把\0发送给对方,因为向文件里写的时候,不需要往文件写入\0,\0是C语言规定,和网络、文件无关
bash
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
buffer[n] = 0;
LOG(LogLevel::DEBUG) << "[" << clientip
<< ":" << clientport << "]# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
(struct sockaddr*)&peer, len);
客户端
创建套接字
bash
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
绑定套接字
client要不要:显示的bind自己的ip和端口,即程序员自己是否需要绑定bind函数绑定ip和端口?不要!!!
为什么客户端不大大方方地把自己的socket和对方的socket进行bind,非要扭扭捏捏的?如果程序员进行bind,知道绑哪一个吗?client会在自己OS的帮助下,随机bind端口号,客户端之间彼此不会出现冲突.
为什么服务端要固定呢?是具体的公司,一定是只能访问某个公司的服务端.
client 要不要 隐式绑定 IP和端口?隐式绑定: 程序员不手动调用
bind(),而是由操作系统内核在特定时机自动地、隐式地为套接字分配一个可用的IP地址和端口号.特定时机:这个时机就是在你首次尝试与服务器通信 的时候。对于TCP,是调用
connect()函数时;对于UDP,是调用sendto()或connect()函数时。
所以客户端不需要绑定套接字!!!
写数据
bash
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());
while (true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line);
// 写
sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
}
读数据
bash
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
代码测试
netstat是用来查看网络状态的
netstat -u(带udp) -a(所有udp) -n(显示数字) -p(进程)

那如果一个是本地环回,一个是本地ip呢?数据发不过去.

那是否可以绑定公网IP呢?发现并不能,因为云服务器的服务端禁止用户bind公网ip(即便可以)
最佳实践:不建议服务端bind固定的IP!!!
附源码
UdpServer.hpp
bash
static const int gdefaultsockfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port)
: _port(port),
_sockfd(gdefaultsockfd),
_isrunning(false)
{}
void Init()
{
// 1. 创建socket fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3
// 2. bind
// 2.1: 填充IP和Port
// 我们有没有实现,把socket和file关联起来呢??没有!!!
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; // 任意IPbind
// 什么叫做任意IP bind? 不明确具体IP,只要是发给我对应的主机,对应的port
// 我都能收到!
local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind
//local.sin_addr = inet_addr(_ip.c_str()); //inet_addr: 1. 字符串转整数ip 2. 整数ip是网络序列的
// ?
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); //inet_addr: 1. 字符串转整数ip 2. 整数ip是网络序列的
// 2.2 和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
}
void Start()
{
// 所有的服务器都是死循环
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; // 清空缓冲区
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 读取数据
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer, &len);
if(n > 0)
{
//client是谁啊??ip和端口给我!
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
buffer[n] = 0;
LOG(LogLevel::DEBUG) << "[" << clientip
<< ":" << clientport << "]# " << buffer;
std::string echo_string = "server echo# ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
(struct sockaddr*)&peer, len);
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~UdpServer(){}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 暂时,"192.168.1.1"
bool _isrunning;
};
bash
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localport" << std::endl;
}
// ./udp_server serverport
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port =std::stoi(argv[1]);
EnableConsoleLogStrategy();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
bash
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "create socket errror" << std::endl;
return 0;
}
// [srcip, srcport] [dstip, dstport]
// client要不要:显示的bind自己的ip和端口?不要!!!
// client 要不要 隐式bind IP和端口?
// 为什么?client会在自己OS的帮助下,随机bind端口号
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());
while (true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line);
// 写
sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
DictServer

熟悉了网络相关接口后,这里写一个支持中文翻译成英文的工作.
翻译工作
dict.txt
bash
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
: 冬天
winter1:
:
我们把中英翻译放到一个txt文档里,将来写一个类翻译Dictionary类,在上层指定中英文档路径.
bash
class Dictionary
{
public:
Dictionary(const std::string &path):_path(path)
{
LOG(LogLevel::INFO) << "construct Dictionary obj";
LoadConf();
}
~Dictionary()
{}
private:
std::string _path;
std::unordered_map<std::string, std::string> _dict;
};
将 dict.txt 文档中合法的中英文对照条目载入到_dict 哈希表中。
- 判断该路径是否存在,在判断该文件是否打开;
- 按行读取,将": "作为分隔符,若读到npos说明没有找到分隔符,即该行数据是非法的,跳到下一行;
- 即使找到分隔符,若英文或中文缺一也是非法的;
- 此时该行数据一定是合法的,插入到_dict表中.
bash
static const std::string sep = ": ";
void LoadConf()
{
std::ifstream in(_path);
if(!in.is_open())
{
LOG(LogLevel::ERROR) << "open file error: " << _path;
return;
}
std::string line;
while(std::getline(in, line))
{
LOG(LogLevel::DEBUG) << "load dict message: " << line;
//dog: 狗
auto pos = line.find(sep);
if(pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "format error: " << line;
continue;
}
std::string word = line.substr(0, pos); // [)
std::string value = line.substr(pos + sep.size());
if(word.empty() || value.empty())
{
LOG(LogLevel::WARNING) << "format error, word or value is empty: " << line;
continue;
}
_dict.insert(std::make_pair(word, value));
}
in.close();
}
当客户端向服务端发送数据时(双方约定该操作对应翻译任务),服务端读取数据并提交给上层模块处理。此时系统会调用 Translate 函数,从_dict 哈希表中查找该英文对应的中文,最终将查找到的中文结果返回给客户端。
bash
std::string Translate(const std::string &word,
const std::string &whoip, uint16_t whoport)
{
(void)whoip;
(void)whoport;
auto iter = _dict.find(word);
if(iter == _dict.end())
{
return "unknown";
}
return iter->first + "->" + iter->second;
}
服务端
服务端完全不知道 接收到的数据报对应的数据类型是字符串、单词、整数还是二进制数据等等.即UDP 给了你一个 "完整的盒子",但盒子上依然没有标签 ------ 标签需要你自己提前和对方说好怎么贴、怎么看。
DictServer.hpp
bash
using callback_t = std::function<std::string \
(const std::string &word, const std::string &whoip, uint16_t whoport)>;
std::string word = buffer;
std::string result = _cb(word, clientip, clientport);
bash
std::unique_ptr<DictServer> usvr = std::make_unique<DictServer>(port,
[&dict](const std::string &word, const std::string &whoip, uint16_t whoport) ->
std::string{ return dict.Translate(word, whoip, whoport);}
);
附源码
DictServer.hpp
bash
using callback_t = std::function<std::string \
(const std::string &word, const std::string &whoip, uint16_t whoport)>;
static const int gdefaultsockfd = -1;
class DictServer
{
public:
DictServer(uint16_t port, callback_t cb)
: _port(port),
_sockfd(gdefaultsockfd),
_isrunning(false),
_cb(cb)
{}
void Init()
{
// 1. 创建socket fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3
// 2. bind
// 2.1: 填充IP和Port
// 我们有没有实现,把socket和file关联起来呢??没有!!!
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; // 任意IPbind
// 什么叫做任意IP bind? 不明确具体IP,只要是发给我对应的主机,对应的port
// 我都能收到!
local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPbind
//local.sin_addr = inet_addr(_ip.c_str()); //inet_addr: 1. 字符串转整数ip 2. 整数ip是网络序列的
// ?
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); //inet_addr: 1. 字符串转整数ip 2. 整数ip是网络序列的
// 2.2 和socketfd进行bind
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
}
void Start()
{
// 所有的服务器都是死循环
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; // 清空缓冲区
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 读取数据
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = 0;
//client是谁啊??ip和端口给我!
uint16_t clientport = ntohs(peer.sin_port);
std::string clientip = inet_ntoa(peer.sin_addr);
std::string word = buffer;
LOG(LogLevel::DEBUG) << "用户查找: " << word;
// 回调!
std::string result = _cb(word, clientip, clientport);
// buffer[n] = 0;
// LOG(LogLevel::DEBUG) << "[" << clientip
// << ":" << clientport << "]# " << buffer;
// std::string echo_string = "server echo# ";
// echo_string += buffer;
sendto(_sockfd, result.c_str(), result.size(), 0,
(struct sockaddr*)&peer, len);
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~DictServer(){}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 暂时,"192.168.1.1"
callback_t _cb;
bool _isrunning;
};
bash
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localport" << std::endl;
}
// std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
// {
// return "哈哈";
// }
// ./udp_server serverport
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
EnableConsoleLogStrategy();
uint16_t port = std::stoi(argv[1]);
Dictionary dict("./dict.txt");
std::unique_ptr<DictServer> usvr = std::make_unique<DictServer>(port,
[&dict](const std::string &word, const std::string &whoip, uint16_t whoport) -> std::string
{
return dict.Translate(word, whoip, whoport);
});
usvr->Init();
usvr->Start();
return 0;
}
bash
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "create socket errror" << std::endl;
return 0;
}
// [srcip, srcport] [dstip, dstport]
// client要不要:显示的bind自己的ip和端口?不要!!!
// client 要不要 隐式bind IP和端口?
// 为什么?client会在自己OS的帮助下,随机bind端口号
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());
while (true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line);
// 写
sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
Dictionary.hpp
bash
static const std::string sep = ": ";
class Dictionary
{
private:
void LoadConf()
{
std::ifstream in(_path);
if(!in.is_open())
{
LOG(LogLevel::ERROR) << "open file error: " << _path;
return;
}
std::string line;
while(std::getline(in, line))
{
LOG(LogLevel::DEBUG) << "load dict message: " << line;
//dog: 狗
auto pos = line.find(sep);
if(pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "format error: " << line;
continue;
}
std::string word = line.substr(0, pos); // [)
std::string value = line.substr(pos + sep.size());
if(word.empty() || value.empty())
{
LOG(LogLevel::WARNING) << "format error, word or value is empty: " << line;
continue;
}
_dict.insert(std::make_pair(word, value));
}
in.close();
}
public:
Dictionary(const std::string &path):_path(path)
{
LOG(LogLevel::INFO) << "construct Dictionary obj";
LoadConf();
}
std::string Translate(const std::string &word,
const std::string &whoip, uint16_t whoport)
{
(void)whoip;
(void)whoport;
auto iter = _dict.find(word);
if(iter == _dict.end())
{
return "unknown";
}
return iter->first + "->" + iter->second;
}
~Dictionary()
{}
private:
std::string _path;
std::unordered_map<std::string, std::string> _dict;
};
简单聊天室

该 demo 代码主要实现了群聊功能:当一名用户发送消息时,所有在线用户均能收到该消息.
当用户发送消息时,服务器读取该消息后,会将客户端的消息内容与其对应的套接字打包为一个任务,提交至线程池。线程池检测到新任务后,会唤醒相应线程从中取出任务进行处理 ------ 处理逻辑为实现群聊功能,即把该消息转发给当前所有在线用户。
前置工作
当一个客户端给服务端发消息时,服务端需不需要知道客户端的是谁?需要的,而客户端身份通常通过 IP 地址与端口号组合标识,这就不可避免涉及网络字节序(大端)与主机字节序(可能为小端)的相互转换。这种转换操作具有极强的重复性:每次解析客户端地址、绑定服务端端口、设置目标地址时都需要调用htons()/ntohs()(端口转换)、inet_pton()/inet_ntop()(IP 地址转换)等函数,不仅冗余繁琐,还容易因疏忽导致字节序错误(比如忘记转换直接使用主机序端口)。因此我们可以把它封装在InetAddr.hpp中.
bash
#define Conv(addr) ((struct sockaddr*)&addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
}
public:
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
Net2Host();
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr* Addr()
{
return Conv(_addr);
}
socklen_t Length()
{
return sizeof(_addr);
}
std::string ToString()
{
return _ip + "-" + std::to_string(_port);
}
bool operator==(const InetAddr &addr)
{
return (_ip == addr._ip && _port == addr._port);
// return (_ip == addr._ip);
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 网络风格地址 --> 网络序列之后可能还要用,所以保存_addr,方便发回去
// 主机风格地址
std::string _ip;
uint16_t _port;
};
路由功能
当服务器读到了客户端发来的message时,需要把这个消息转发给在线用户.那么该如何设计呢?为了保证层与层之间的解耦,这里引出了所谓的路由功能,它帮我们进行消息的转发,而服务端只负责IO,并不负责处理数据.
bash
using callback_t = std::function<void \
(int sockfd, std::string message, InetAddr addr)>;
_cb(_sockfd, message, clientaddr);
// 将打包好的任务push到线程池里,并交由路由进行消息的群发
std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
[&r, &tp](int sockfd, std::string message, InetAddr addr){
task_t task = std::bind(&Route::RouteMessageToAll, r.get(), sockfd, message, addr);
tp->Enqueue(task);
}
);
路由需要把消息转发给所有人?那么需不需要知道在线用户是多少呢?离线用户有哪些?一共有多少在线用户呢?需不需要进行维护呢?需要的,如何维护?先描述,再组织,因此一定存在对路由功能进行描述的类.
bash
class Route
{
private:
bool IsExists(const InetAddr &addr)
{
for (auto &user : _online_user)
{
if (user == addr)
{
return true;
}
}
return false;
}
void AddUser(const InetAddr &addr)
{
if(!IsExists(addr))
_online_user.push_back(addr);
}
void DeleteUser(const std::string &message, const InetAddr &addr)
{
// 权宜之计, 自定义协议部分
if(message == "QUIT")
{
auto iter = _online_user.begin();
for(; iter != _online_user.end(); iter++)
{
if(*iter == addr)
{
_online_user.erase(iter);
break;
}
}
}
}
void SendMessageToAll(int sockfd, std::string &message, InetAddr &addr)
{
for(auto &user : _online_user)
{
LOG(LogLevel::DEBUG) << "route [" << message << "] to : " << user.ToString();
std::string info = addr.ToString();
info += "# ";
info += message; // XXXX-PORT# 你好
sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Length());
}
}
public:
Route()
{
}
void RouteMessageToAll(int sockfd, std::string &message, InetAddr &addr)
{
AddUser(addr);
// 我们就一定或有在线用户列表
SendMessageToAll(sockfd, message, addr);
DeleteUser(message, addr);
}
// void RouteMessageToOne()
// {}
~Route()
{
}
private:
// 临界资源
// 方法1:加锁
std::vector<InetAddr> _online_user; // 在线用户
};
有一个问题:线程池内进行消息转发时,线程池从任务队列里拿任务,.一个线程在进行广播的时候,另外一个线程有没有可能也要进行广播呢?在广播的同时,有没有可能要新增用户或删除用户,即多线程并发执行RouteMessageToAI时,可能会对在线用户列表进行并发访问,即是一个临界资源啊!
bash
// 方法2:
// 锁 + 拷贝
std::vector<InetAddr> _send_list;
// 方法3:
std::queue<std::string> _message_queue;
客户端
在微信中,如果我们不进行发消息,能不能收到消息呢?可以的,因此客户端不能是串行执行的,即串行的写和读明显是不对的,应该是多线程的,一个线程专门用来写,一个新线程专门用来读.
bash
void recver()
{
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cerr << buffer << std::endl; // 1->2
}
}
}
void sender()
{
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());
while (true)
{
std::cout << "Please Enter@ "; //1
std::string line;
std::getline(std::cin, line); //0
// 写
sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
附源码
Route.hpp
bash
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
class Route
{
private:
bool IsExists(const InetAddr &addr)
{
for (auto &user : _online_user)
{
if (user == addr)
{
return true;
}
}
return false;
}
void AddUser(const InetAddr &addr)
{
if(!IsExists(addr))
_online_user.push_back(addr);
}
void DeleteUser(const std::string &message, const InetAddr &addr)
{
// 权宜之计, 自定义协议部分
if(message == "QUIT")
{
auto iter = _online_user.begin();
for(; iter != _online_user.end(); iter++)
{
if(*iter == addr)
{
_online_user.erase(iter);
break;
}
}
}
}
void SendMessageToAll(int sockfd, std::string &message, InetAddr &addr)
{
for(auto &user : _online_user)
{
LOG(LogLevel::DEBUG) << "route [" << message << "] to : " << user.ToString();
std::string info = addr.ToString();
info += "# ";
info += message; // XXXX-PORT# 你好
sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Length());
}
}
public:
Route()
{
}
void RouteMessageToAll(int sockfd, std::string &message, InetAddr &addr)
{
AddUser(addr);
// 我们就一定或有在线用户列表
SendMessageToAll(sockfd, message, addr);
DeleteUser(message, addr);
}
// void RouteMessageToOne()
// {}
~Route()
{
}
private:
// 临界资源
// 方法1:加锁
std::vector<InetAddr> _online_user; // 在线用户
// 方法2:
// // 锁 + 拷贝
// std::vector<InetAddr> _send_list;
// 方法3:
// std::queue<std::string> _message_queue;
};
ChatServer.hpp
bash
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"
using callback_t = std::function<void \
(int sockfd, std::string message, InetAddr addr)>;
static const int gdefaultsockfd = -1;
class ChatServer
{
public:
ChatServer(uint16_t port, callback_t cb)
: _port(port),
_sockfd(gdefaultsockfd),
_isrunning(false),
_cb(cb)
{}
void Init()
{
// 1. 创建socket fd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success : " << _sockfd; // 3
InetAddr local(_port);
// 2.2 和socketfd进行bind
int n = bind(_sockfd, local.Addr(), local.Length());
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success : " << _sockfd; // 3
}
void Start()
{
// 所有的服务器都是死循环
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
buffer[0] = 0; // 清空缓冲区
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 读取数据
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&peer, &len);
if(n > 0)
{
// 约定: 聊天消息
buffer[n] = 0;
// 得到对应的client是谁?
InetAddr clientaddr(peer);
LOG(LogLevel::DEBUG) << "get a client info # "
<< clientaddr.Ip() << "-" << clientaddr.Port() << ": "
<< buffer;
std::string message = buffer;
// 回调!
_cb(_sockfd, message, clientaddr);
}
}
_isrunning = false;
}
void Stop()
{
_isrunning = false;
}
~ChatServer(){}
private:
int _sockfd;
uint16_t _port;
callback_t _cb;
bool _isrunning;
};
bash
#include "ThreadPool.hpp"
#include "Route.hpp"
#include "ChatServer.hpp"
#include <iostream>
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " localport" << std::endl;
}
// // for debug
// void chat(int sockfd, std::string message, InetAddr addr)
// {
// LOG(LogLevel::DEBUG) << "sockfd: " << sockfd;
// LOG(LogLevel::DEBUG) << "message: " << message;
// LOG(LogLevel::DEBUG) << "client info: " << addr.ToString();
// sendto(sockfd, message.c_str(), message.size(), 0, addr.Addr(), addr.Length());
// }
using task_t = std::function<void()>;
// ./udp_server serverport
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
EnableConsoleLogStrategy();
uint16_t port = std::stoi(argv[1]);
// 1. 消息转发功能
std::unique_ptr<Route> r = std::make_unique<Route>();
// 2. 线程池对象
auto tp = ThreadPool<task_t>::GetInstance();
// 3. 服务器对象
std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
[&r, &tp](int sockfd, std::string message, InetAddr addr){
task_t task = std::bind(&Route::RouteMessageToAll, r.get(), sockfd, message, addr);
tp->Enqueue(task);
}
);
usvr->Init();
usvr->Start();
return 0;
}
bash
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <thread>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}
int sockfd = -1;
std::string serverip;
uint16_t serverport;
void InitClient(const std::string &serverip, uint16_t serverport)
{
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "create socket errror" << std::endl;
}
}
void recver()
{
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cerr << buffer << std::endl; // 1->2
}
}
}
void sender()
{
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());
while (true)
{
std::cout << "Please Enter@ "; //1
std::string line;
std::getline(std::cin, line); //0
// 写
sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
std::cerr << "hahhaha " << std::endl;
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
serverip = argv[1];
serverport = std::stoi(argv[2]);
InitClient(serverip, serverport);
std::thread trecv(recver);
std::thread tsend(sender);
trecv.join();
tsend.join();
return 0;
}
OS和网络的关系

网络命令
- 检测网络连通信
ping -c(n) www.baidu.com
- 查看网络服务
netstat -utapn -l(只显示处于LISTEN状态)
- 查看特定网络服务特定进程的命令
pidof 名称 -->自动提取pid
pidof 名称|xargs kill -9 (xargs的作用就是把管道当中传递过来的数据转换成后续命令的后续,拼接到后面)