35.Socket网络编程(UDP)(下)
UdpSocket编程V2(字典)
Linux-remote: linux远程仓库https://gitee.com/its-quite-six/linux-remote/tree/master/25_9_15/2.Dictionary
细节点1
先创建实例dict,构造服务端类udpserver方法用lambda表达式,捕获dict实例,调用其成员函数,就实现了类之间的耦合。
细节点2
对于字符串的分割,用substr更好,用迭代器构造的话还要自行判断。substr第二个参数不传默认截取到结尾。
代码:
Dict.hpp
#pragma once #include <iostream> #include <fstream> #include <unordered_map> #include "Log.hpp"using namespace LogModule; const std::string defaultdict = "./dict.txt"; const std::string separator = ": ";class Dict { public:Dict(const std::string &dictpath = defaultdict):_dict_path(dictpath){}void Load(){// 只读方式打开文件std::ifstream ifs(defaultdict);if (!ifs.is_open()){LOG(LogLevel::FATAL) << "open " << defaultdict << " failure!";exit(1);}// data的内容: apple: 苹果std::string data;while (std::getline(ifs, data)){size_t pos = data.find(separator);// 没找到分割符if (pos == std::string::npos){LOG(LogLevel::WARNING) << "加载失败,内容是:" << data;continue;}std::string english = data.substr(0, pos);std::string chinese = data.substr(pos + separator.size());if (english.empty() || chinese.empty()){LOG(LogLevel::WARNING) << "加载失败,缺失中文/英文,内容是:" << data;continue;}LOG(LogLevel::INFO) << "加载成功,单词为:" << data;_dict.insert(std::make_pair(english, chinese));}ifs.close();}std::string Translate(const std::string &english){auto it = _dict.find(english);if (it == _dict.end()){LOG(LogLevel::INFO) << "没有找到单词,英文为:" << english;return "NONE";}LOG(LogLevel::INFO) << "找到了对应的单词, " << english << separator << it->second;return it->second;}~Dict(){}private:std::string _dict_path; //路径+文件名std::unordered_map<std::string, std::string> _dict; };
UdpSocket编程V3(聊天室)
分析场景1
聊天室的功能:客户端发送消息给主机,主机将消息转发给所有的在线用户
模型:服务端线程创建了sokcet,接受来自客户端的信息。
客户端不止一个(客户端信息不止一条),如何管理这些客户端信息呢?
先描述在组织,为了便于实现port和IP等转化,以及stuct sockaddr_in的封装,封装一个InetAddr的类用来描述客户端信息;定义一个Route管理类,用vector组织管理客户端信息InetAddr;
易错1:
const变量只能调用const成员函数(调用普通成员函数属于权限放大)。
易错2:
InetAddr的构造函数要把_peer清0后使用,并且_peer的sin_family字段要设置成AF_INET,不然Route用来转发时,客户端收不到。
分析场景2
udp是支持全双工的,多线程读写。
- client处理时,读线程和写线程分离,让消息读写是异步的,使得不用等写完才能读。
- 读端打印到std::cerr(fd=2),将2号重定向到另一个打开的终端上,这样就实现了读写输出到不同地方,便于查看。
易错点1:
注意:信息是指发消息的那个的 信息+数据。
消息转发,给所有人都发一条,包括自己。
ChatV1
Linux-remote: linux远程仓库https://gitee.com/its-quite-six/linux-remote/tree/master/25_9_15/3.Chat_V1
工作流程:
- 客户端给服务端发送数据
- 服务端接收数据
- 消息处理,回调
- 回调本质工作:消息转发给每一个在线的客户端
- 转发给在线客户端
结构:
- 服务端主线程执行接收客户端数据,进行消息转发的工作
- 客户端三个线程:
主线程控制整个调用逻辑;
发送线程负责发送数据给服务端;
接收线程负责接受数据,写入到2号文件描述符对应文件。
ChatV2
Linux-remote: linux远程仓库https://gitee.com/its-quite-six/linux-remote/tree/master/25_9_15/4.Chat_V2
与ChatV1唯一区别,ChatV2的服务端主线程只负责接收客户端数据。消息转发的工作交给线程池,多线程来做消息转发工作。
服务端:主线程负责通信,线程池负责处理消息转发的任务。
inet_ntoa(线程不安全)
之前用的inet_ntoa返回的char*是函数内部申请的静态存储区
多次调用inet_ntoa,结果会被覆盖,因此,inet_ntoa是线程不安全的,不建议用。
ip转化函数(inet_ntop , inet_pton 线程安全)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- af:协议类型,AF_INET
- src:网络序列的IP数字
- dst:输出型参数buffer,存放转换后的IP地址,字符串
- size:buffer容量
- 返回值:成功返回dst指针;失败返回空指针,错误码被设置
int inet_pton(int af, const char *src, void *dst);
- af:协议类型,AF_INET
- src:本地ip地址,字符串
- dst:网络ip地址,32位无符号整数
- 返回值:成功返回1;af正确但ip地址无效返回0;af不正确返回-1,错误码被设置
细节点1:
task_t为std::function<void()>类型,通过bind参数绑定可以实现(lambda同理)
细节点2:
线程池是基于生产者消费者模型写的,其中消费者处理任务是多进程并发执行的,此时,_online_users是临界资源,且有增加和删除,因此消息转发任务需要加锁。
InetAddr.hpp实现
#pragma once #include <iostream> #include <string> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include "Log.hpp"using namespace LogModule;class InetAddr { public:InetAddr(const struct sockaddr_in peer): _peer(peer){// 网络转主机_port = ntohs(peer.sin_port);char buff[64];inet_ntop(AF_INET, &_peer.sin_addr.s_addr, buff, sizeof(buff));_ip = buff;}InetAddr(const uint16_t port, const std::string &ip): _port(port), _ip(ip){// 主机转网络memset(&_peer, 0, sizeof(_peer));_peer.sin_family = AF_INET;_peer.sin_port = htons(_port);inet_pton(AF_INET, _ip.c_str(), &_peer.sin_addr.s_addr);}bool operator==(const InetAddr &inetaddr) const{return (_port == inetaddr._port) && (_ip == inetaddr._ip);}// 返回本地序列的端口号uint16_t Port() const { return _port; }// 返回本地序列的点分十进制ipstd::string Ip() const { return _ip; }// 返回网络地址对象指针const struct sockaddr_in *Peer() const { return &_peer; }// 返回ip+端口号拼接的字符串std::string AddrString() const { return _ip + ":" + std::to_string(_port); }~InetAddr(){}private:uint16_t _port; // 主机序列portstd::string _ip; // 主机ip,点分10进制struct sockaddr_in _peer; // 网络序列peer };
Route.hpp实现
#pragma once #include <string> #include <vector> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include "Log.hpp" #include "InetAddr.hpp" #include "Mutex.hpp"using namespace LogModule; using namespace MutexModule;class Route { private:bool Exist(const InetAddr &inetaddr){for (auto &user : _online_users){if (user == inetaddr)return true;}return false;}// 添加在线用户void AddUser(const InetAddr &inetaddr){_online_users.emplace_back(inetaddr);LOG(LogLevel::INFO) << "添加用户成功, " << inetaddr.AddrString();}// 删除在线用户void DeleteUser(const InetAddr &inetaddr){for (auto it = _online_users.begin(); it != _online_users.end(); it++){if (*it == inetaddr){_online_users.erase(it);break;}}LOG(LogLevel::INFO) << "删除用户成功, " << inetaddr.AddrString();}public:Route(){}// 消息转发给所有在线用户,以服务端的名义sockfdvoid MessageRoute(int sockfd, const std::string &data, const InetAddr &inetaddr){// 消息转发是多线程并行的,_online_users是临界资源,访问要加锁MutexGuard mutexguard(_mutex);// 1.判断该用户是否在线,如果不在线,就要添加if (!Exist(inetaddr))AddUser(inetaddr);// 格式:(用户ip:port:数据)std::string message = inetaddr.AddrString() + "# " + data;// 2.消息转发,给所有人都发一条for (auto &user : _online_users){ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,(const struct sockaddr *)(user.Peer()), sizeof(struct sockaddr_in));(void)n;}// 3.如果data内容为QUIT,那么就删除对应用户if (data == "QUIT")DeleteUser(inetaddr);}~Route(){}private:std::vector<InetAddr> _online_users;Mutex _mutex; };
拓展
防火墙处添加外网可访问的端口号。