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_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)); // 清0server.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()); // 填充iplocal.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; // 套接字fduint16_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 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; // 套接字fduint16_t _port; // 端口号bool _isrunning;func_t _func;
};
UdpServer.cc:
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)); // 清0server.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:";//1std::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;
}
好了,我们下期见。