socket编程UDP
目录
UDP网络编程
V1版本-Echo Server
V2版本-Dict Server
V3版本-Chat Server
地址转换函数
实现
UDP网络编程
V1版本-Echo Server
我们利用上篇基础接口,写一个简单的网络通信代码。
框架:客户端向服务端发送消息,服务端处理信息,同时给客户端回显消息。
服务端UdpServer.hpp:
注意:将服务端IP绑定设置成INADDR_ANY(是一个特殊常量(值为 0.0.0.0
))的理由是,它可以设置将监听主机上的所有网络接口,也就是说无论主机有多少个网卡或动态 IP,将来客户端只需要拿着主机某一个IP和确定端口就可以进行网络通信。
UdpServer.cc:
UdpClient.cc:
代码:
UdpClient.cc:
// ./UdpClient server_ip server_portint 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< 0) { buffer[m] = 0; std::cout << buffer << std::endl; } } return 0;}
UdpServer.hpp:
using func_t = std::function;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:\" < 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;};
UdpServer.cc:
// 仅仅是用来进行测试的std::string defaulthandler(const std::string &message){ std::string hello = \"hello, \"; hello += message; return hello;}//./UdpServer portint 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 us = std::make_unique(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 <\" <second; return iter->second; }private: std::string _dict_path; // 路径+文件名 std::unordered_map _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 _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;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:\" < 0) // 返回实际接收的字节数 { // 网络转主机序列 InetAddr client(peer); buffer[s] = 0; _func(_sockfd, buffer, client); // 回调函数 } } }private: int _sockfd; // 套接字fd uint16_t _port; // 端口号 bool _isrunning; func_t _func;};
UdpServer.cc:
using task_t = std::function;//./UdpServer portint 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::GetInstance(); Enable_Console_Log_Strategy(); std::unique_ptr us = std::make_unique(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_portint 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;}
好了,我们下期见。