目录
[V1版本-Echo Server](#V1版本-Echo Server)
[V2版本-Dict Server](#V2版本-Dict Server)
[V3版本-Chat Server](#V3版本-Chat Server)
UDP网络编程
V1版本-Echo Server
我们利用上篇基础接口,写一个简单的网络通信代码。
框架:客户端向服务端发送消息,服务端处理信息,同时给客户端回显消息。
服务端UdpServer.hpp:


注意:将服务端IP绑定设置成INADDR_ANY(是一个特殊常量(值为 0.0.0.0
))的理由是,它可以设置将监听主机上的所有网络接口,也就是说无论主机有多少个网卡或动态 IP,将来客户端只需要拿着主机某一个IP和确定端口就可以进行网络通信。




代码:
// ./UdpClient server_ip server_port
int main(int agrc, char *agrv[])
{
if (agrc != 3)
{
std::cerr << "Usage: " << agrv[0] << "ip port" << std::endl;
return 1;
}
std::string server_ip = agrv[1];
uint16_t server_port = std::stoi(agrv[2]);
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
// 不需要绑定
// 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server)); // 清0
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
while (true)
{
std::string input;
std::cout<<"Please enter:";
std::getline(std::cin, input);
// 发送
sendto(sockfd, input.c_str(), input.size(),
0, (struct sockaddr *)&server, sizeof(server));
// 收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&peer, &len);
//打印
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
UdpServer.hpp:
using func_t = std::function<std::string(const std::string &)>;
const int sockdefault = -1;
// 网络通信类
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(sockdefault), _port(port), _func(func), _isrunning(false)
{
}
void Init()
{
// 1.创建套接字 本质是网络文件 返回文件描述符
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建失败
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success! socketfd:" << _sockfd;
// 2填充sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清0
// 表示使用 IPv4 协议 进行网络通信
local.sin_family = AF_INET;
// 将来需要将自己的端口号和ip发送至网络
// 需要将端口号和ip转成网络格式,再发送至网络
local.sin_port = htons(_port); // 填充端口号
// ip需要先转成4字节,再变网络格式
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 填充ip
local.sin_addr.s_addr = INADDR_ANY;
// 3.绑定socket信息,ip和端口号
// IP和端口必须是众所周知且不能轻易改变的
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(2);
}
LOG(LogLevel::INFO) << "bind success,socket:" << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1.收消息,会收到哪个ip,哪个进程发送的消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&peer, &len);
if (s > 0) // 返回实际接收的字节数
{
// 将网络端口序列转化出来
int peer_port = ntohs(peer.sin_port);
// 将4字节的网络格式ip转化出来
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0;
std::string result=_func(buffer);
// 打印
LOG(LogLevel::DEBUG) << "[" << peer_ip << ":"
<< peer_port << "]# " << buffer;
// 2.给对应的客户端发消息
sendto(_sockfd, result.c_str(), result.size(),
0, (struct sockaddr *)&peer, len);
}
}
}
private:
int _sockfd; // 套接字fd
uint16_t _port; // 端口号
bool _isrunning;
func_t _func;
};
// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
//./UdpServer port
int main(int agrc, char *agrv[])
{
if (agrc != 2)
{
std::cerr << "Usage: " << agrv[0] << "port" << std::endl;
return 1;
}
// std::string ip = agrv[1];
uint16_t port = std::stoi(agrv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(port,defaulthandler);
us->Init();
us->Start();
return 0;
}
V2版本-Dict Server
实现一个翻译功能,当客户端发送一个单词给服务端,服务端翻译,将结果返回给服务端。
核心功能:

服务端:需要实现翻译,我们可以这样,一个字典文档存放着单词和汉字的映射数据,一个字典类(里面有着哈希表),首先需要将字典文档中的映射信息加载到字典类的哈希表中,然后将字典类中的翻译接口传给服务端,将来服务端收到客户端的消息,就执行这个回调函数(翻译接口)。

字典类(Dict):


翻译功能:

代码:
Dict.hpp:
const std::string defalutdict = "./dictionary.txt";
const std::string sep = ": ";
class Dict
{
public:
Dict(const std::string &path = defalutdict)
: _dict_path(path)
{
}
bool LoadDict()
{
// 以读方式打开
std::ifstream in(_dict_path);
if (!in.is_open())
{
LOG(LogLevel::DEBUG) << "打开字典:" << _dict_path << "错误";
return false;
}
std::string line;
while (std::getline(in, line))
{
// apple: 苹果
auto pos = line.find(sep);
if (pos == std::string::npos)
{
LOG(LogLevel::ERROR) << "解析:" << line << "失败";
continue;
}
std::string english = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
if (english.empty() || chinese.empty())
{
LOG(LogLevel::ERROR) << "没有有效内容:" << line;
continue;
}
// 正常数据
_dict.insert(std::make_pair(english, chinese));
LOG(LogLevel::INFO) << "加载:" << line;
}
in.close();
return true;
}
std::string Translate(const std::string &word, InetAddr &cli)
{
auto iter = _dict.find(word);
if (iter == _dict.end())
{
LOG(LogLevel::INFO) << "进入翻译功能:" << cli.Ip() << ":"
<< cli.Port() << ":#" << word << "->" << "None";
return "None";
}
LOG(LogLevel::INFO) << "进入翻译功能:" << cli.Ip() << ":"
<< cli.Port() << ":#" << word << "->" << iter->second;
return iter->second;
}
private:
std::string _dict_path; // 路径+文件名
std::unordered_map<std::string, std::string> _dict; // 映射
};
其他代码比较冗余,不过多列举。
V3版本-Chat Server
我们要实现一个简单的聊天室。
客户端多线程,一个发,一个收,当客户端没发消息时,它能收到其他客户端的消息,发消息时,服务端收到消息,做路由服务,将路由服务插入线程池队列,由多个线程消费路由服务。

我们先讲讲地址转换函数。
地址转换函数
字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接⼝是 void *addrptr。
关于inet_ntoa:
inet_ntoa这个函数返回了⼀个char*,很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的 结果.那么是否需要调⽤者⼿动释放呢?
inet_ntoa函数,是把这个返回结果放到了静态存储区.这个时候不需要我们⼿动进⾏释放。
inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区,这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果
如果有多个线程调⽤inet_ntoa,是否会出现异常情况呢?
在APUE中,明确提出inet_ntoa不是线程安全的函数,但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁。在多线程环境下,推荐使⽤inet_ntop,这个函数由调⽤者提供⼀个缓冲区保存结果,可以规避线程 安全问题;
实现
我们可以将Ip和端口主机和网络之间的转化,提前写一个类,可以避免代码冗余情况。
InetAddr类:将ip和端口进行网络和主机之间的转化。

UdpServer.cc,UdpServer.hpp(服务端):

UdpClient.cc(客户端):

Route.hpp(路由服务):

代码:
Route.hpp:
class Route
{
private:
bool IsExist(InetAddr &peer)
{
for (auto &user : _online_user)
{
if (user == peer)
return true;
}
return false;
}
void AddUser(InetAddr &peer)
{
LOG(LogLevel::INFO) << "新添加一个新用户" << peer.StringAddr();
_online_user.push_back(peer);
}
void DeleteUser(InetAddr &peer)
{
for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
{
if (*iter == peer)
{
LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";
_online_user.erase(iter);
break;
}
}
}
public:
void MessageRoute(int sockfd, std::string &message, InetAddr &peer)
{
LockGuard lock(_mutex);
if (!IsExist(peer))
{
AddUser(peer);
}
std::string send_message = peer.StringAddr() + "#" + message;
// 给每个用户都发送一下信息
for (auto &user : _online_user)
{
sendto(sockfd, send_message.c_str(), send_message.size(), 0,
(struct sockaddr *)&user.NetAddr(), sizeof(user.NetAddr()));
}
//退出
if (message == "quit")
{
LOG(LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr();
DeleteUser(peer);
}
}
private:
Mutex _mutex;
std::vector<InetAddr> _online_user; // 在线用户
};
InetAddr.hpp:
class InetAddr
{
public:
// 两个构造函数
InetAddr(struct sockaddr_in &addr)
: _addr(addr)
{
// 网络转主机序列
_port = ntohs(_addr.sin_port);
// _ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
InetAddr(const std::string &ip, uint16_t port)
: _ip(ip), _port(port)
{
// 主机转网络序列
memset(&_addr, 0, sizeof(_addr)); // 清0
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET, ip.c_str(), &_addr.sin_addr);
}
uint16_t Port() { return _port; }
std::string Ip() { return _ip; }
bool operator==(InetAddr &add)
{
return add._ip == _ip && add._port == _port;
}
struct sockaddr_in &NetAddr()
{
return _addr;
}
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
UdpServer.hpp:
using func_t = std::function<void(int sockfd, const std::string &, InetAddr &)>;
const int sockdefault = -1;
// 网络通信类
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(sockdefault), _port(port), _func(func), _isrunning(false)
{
}
void Init()
{
// 1.创建套接字 本质是网络文件 返回文件描述符
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建失败
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success! socketfd:" << _sockfd;
//可以这样
uint16_t port = _port;
InetAddr local("0", port);
int n = bind(_sockfd, (struct sockaddr *)(&(local.NetAddr())),
sizeof(local.NetAddr()));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(2);
}
LOG(LogLevel::INFO) << "bind success,socket:" << _sockfd;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1.收消息,会收到哪个ip,哪个进程发送的消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&peer, &len);
if (s > 0) // 返回实际接收的字节数
{
// 网络转主机序列
InetAddr client(peer);
buffer[s] = 0;
_func(_sockfd, buffer, client); // 回调函数
}
}
}
private:
int _sockfd; // 套接字fd
uint16_t _port; // 端口号
bool _isrunning;
func_t _func;
};
using task_t = std::function<void()>;
//./UdpServer port
int main(int agrc, char *agrv[])
{
if (agrc != 2)
{
std::cerr << "Usage: " << agrv[0] << "port" << std::endl;
return 1;
}
uint16_t port = std::stoi(agrv[1]);
// 路由服务
Route r;
// 多线程处理
auto tp = ThreadPool<task_t>::GetInstance();
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(port,
[&tp,&r](int sockfd,const std::string& message,InetAddr& peer){
task_t t =std::bind(&Route::MessageRoute,&r,sockfd,message,peer);
tp->Enqueue(t);
});
us->Init();
us->Start();
return 0;
}
void Recv()
{
while (true)
{
// 收
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&peer, &len);
// 打印
if (m > 0)
{
buffer[m] = 0;
std::cerr << buffer << std::endl;//2
}
}
}
void Send()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server)); // 清0
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
//发
const std::string online = "inline";
sendto(sockfd, online.c_str(), online.size(), 0,
(struct sockaddr *)&server, sizeof(server));
while (true)
{
std::string input;
std::cout << "Please enter:";//1
std::getline(std::cin, input);//0
// 发送
sendto(sockfd, input.c_str(), input.size(),
0, (struct sockaddr *)&server, sizeof(server));
if (input == "quit")
{
pthread_cancel(id);
break;
}
}
}
// ./UdpClient server_ip server_port
int main(int agrc, char *agrv[])
{
if (agrc != 3)
{
std::cerr << "Usage: " << agrv[0] << "ip port" << std::endl;
return 1;
}
server_ip = agrv[1];
server_port = std::stoi(agrv[2]);
// 1.创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
// 发送和接受消息多线程
Thread recver(Recv);
Thread sender(Send);
recver.Start();
sender.Start();
// id = recver.Id();
recver.Join();
sender.Join();
return 0;
}
好了,我们下期见。