🦄个人主页:修修修也
🎏所属专栏:
⚙️操作环境:VS Code (操作系统:Ubuntu 22.04 server 64bit)
目录
搭建UDP服务器
搭建UDP服务器的主要流程任务如下:
根据流程搭建UDP服务器完整代码如下:
//服务器 #pragma once #include <iostream> #include <string> #include <strings.h> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <functional> #include "Log.hpp" // using func_t = std::function<std::string(const std::string&)>; typedef std::function<std::string(const std::string&)> func_t; Log lg; enum { SOCKET_ERR=1, BIND_ERR }; uint16_t defaultport = 8080; //端口号绑1024以上,因为[0-1023]被系统绑定了 std::string defaultip = "0.0.0.0"; //ip地址写0表示只要是我这台主机的信息我都接收 const int size = 1024; //封装服务器类 class UdpServer { public: UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false) {} void Init() { // 1. 创建udp socket // socket(域(表明你支持哪种协议IPv4?IPv6?),套接字类型(面向数据报还是字节流),协议类型(一般可填0)) 返回的是一个文件 sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET if(sockfd_ < 0)//如果创建套接字失败 { lg(Fatal, "socket create error, sockfd: %d", sockfd_); exit(SOCKET_ERR); } lg(Info, "socket create success, sockfd: %d", sockfd_); // 2. 绑定bind socket(告诉服务器我们要使用的端口号) struct sockaddr_in local; //sockaddr_in结构体里包含[1]16位协议类型family[2]16位端口号port[3]32位IP地址addr[4]8位填充字段zero bzero(&local, sizeof(local)); //把一段指定大小的内存初始化为0 local.sin_family = AF_INET; //IPv4协议 local.sin_port = htons(port_); //端口号和IP在网络里是互相传递的,所以需要保证大家发送的端口号统一是网络字节序列,这样才不会被不同机器大小端影响 local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string(字符串ip) -> uint32_t(4字节ip) 2. uint32_t(4字节ip)必须是网络序列的 //inet_addr:把字符串风格的Ip转化为网络序列四字节ip // local.sin_addr.s_addr = htonl(INADDR_ANY); //任意地址ip //bind绑定的本质是把你在用户态设定的一个套接字变量local绑定到内核里 int n = bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)); if( n < 0) //如果bind绑定失败 { lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno)); exit(BIND_ERR); } lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno)); } //func是用户传入的数据处理方法 void Run(func_t func) // 对代码进行分层 { isrunning_ = true; char inbuffer[size]; while(isrunning_)//服务器一定是一直在运行的 { //服务器运行的第一件事,从udp读取数据 struct sockaddr_in client; socklen_t len = sizeof(client); // \|/这个是用来接收对方的端口ip信息的,记录下来方便回消息能找到对方 ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len); if(n < 0)//接收信息失败,不影响后续运行,继续接收信息就行 { lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } //接收信息成功,读到数据了 //加工数据 inbuffer[n] = 0; std::string info = inbuffer; //std::string echo_string = "server echo# " + info; //把字符串数据交给func函数处理 std::string echo_string = func(info); //std::cout<<"client echo@"<<info<<std::endl; //加工好数据,返回给对方 sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len); } } ~UdpServer() { if(sockfd_>0) close(sockfd_); } private: int sockfd_; // 网路文件描述符 std::string ip_; // 任意地址bind 0 uint16_t port_; // 表明服务器进程的端口号 bool isrunning_; };
搭建UDP客户端
搭建UDP客户端的主要流程任务如下:
根据流程搭建UDP客户端完整代码如下:
#include<iostream> #include<string> #include<strings.h> #include<cstring> #include<cstdlib> #include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> using namespace std; void Usage(std::string proc) { std::cout<<"\n\rUsage: "<<proc<<" serverip serverport\n"<<std::endl; } // int main(int argc,char* argv[]) { //确定用户要给谁发数据 if(argc != 3) //运行程序的参数:一个可执行程序名和一个ip地址和一个端口号 { Usage(argv[0]); exit(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); struct sockaddr_in server; bzero(&server,sizeof(server)); server.sin_family = AF_INET; //表明自己的类型 server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str()); socklen_t len = sizeof(server); //创建套接字 int sockfd = socket(AF_INET,SOCK_DGRAM,0); if(sockfd < 0) { cout<<"socker error"<<endl; return 1; } //客户端要bind绑定,只不过不需要用户显式绑定,一般由操作系统随机选择,因为一个端口号只能被一个进程绑定,操作系统指定有利于化解冲突 //服务器端口号必须固定,因为不固定用户很难第一下稳定的找到你,客户端不用固定,因为服务端接收客户端的消息时就会有客户端的套接字 string message; char buffer[1024]; while(true) { //1.接收用户想发送的数据 cout<<"Please Enter@ "; getline(cin,message); //cout<<message<<endl; //客户端回显给用户要发送的数据 //2.发送请求数据给客户端 sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len); struct sockaddr_in temp; socklen_t len = sizeof(temp); //2.接收客户端回复的数据 ssize_t s = recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len); //表示成功接收到客户端返回的数据了,这个数据的处理方式是打印 if(s>0) { buffer[s]=0; cout<<buffer<<endl; } } close(sockfd); return 0; }
其余工程文件
主函数文件Main.cc
主函数中的代码逻辑分为两部分,第一是主函数逻辑,即创建并运行服务器对象.第二部分是提供服务器数据处理的回调函数,这里提供了三个不同功能的回调函数,分别是字符串大小写转换功能函数,聊天功能函数,简单复读机功能函数,指令执行功能函数.这些回调函数需要在Run服务器的时候当作参数传递给服务器,以便服务器调用这些函数来处理接收到的数据.完整代码如下:
#include "UdpServer.hpp" #include<iostream> #include <memory> #include <string> #include <vector> #include <cstdio> // "120.78.126.148" 点分十进制字符串风格的IP地址 void Usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl; } //简单处理,只是在数据前加一个数据 std::string Handler(const std::string &str) { std::string res = "Server get a message: "; //std::string res = "Server say: "; res += str; std::cout << res << std::endl; return res; } //聊天 std::string talker(const std::string &str) { //std::string res = "Server get a message: "; std::string res = "client say: "; res += str; std::cout << res << std::endl; std::string qus; getline(std::cin,qus); return qus; } //字符串大小写转换 std::string ToChar(const std::string &str) { std::string res; for(const auto e:str) { if(e>='A'&&e<='Z') res+=(e+32); else if(e>='a'&&e<='z') res+=(e-32); else res+=e; } std::cout << "转换后:"<< res << std::endl; return res; } //检查指令合理 bool SafeCheck(const std::string &cmd) { std::vector<std::string> key_word = { "rm", "mv", "cp", "kill", "sudo", "unlink", "uninstall", "yum", "top", "while" }; for(auto &word:key_word) { auto pos = cmd.find(word); if(pos != std::string::npos) return false; } return true; } //执行用户输入指令 std::string ExcuteCommand(const std::string &cmd) { std::cout<<"get a request cmd: "<<cmd<<std::endl; //对指令做安全检查 if(!SafeCheck(cmd)) return "Bad man"; //直接给指令,popen帮你fork子进程 FILE *fp = popen(cmd.c_str(), "r"); if(nullptr == fp)//如果popen失败了 { perror("popen"); return "error"; } std::string result; char buffer[4096]; //从fp里把指令执行结果取出来 while(true) { char *ok = fgets(buffer, sizeof(buffer), fp); if(ok == nullptr) break; result += buffer; } pclose(fp); return result; } // ./udpserver port int main(int argc, char *argv[]) { if(argc != 2) { Usage(argv[0]); exit(0); } uint16_t port = std::stoi(argv[1]); //构建一个服务器对象 std::unique_ptr<UdpServer> svr(new UdpServer(port)); //初始化服务器 svr->Init(/**/); //运行服务器 svr->Run(ExcuteCommand); return 0; }
日志打印文件Log.hpp
该文件提供了一个Log类,支持直接打印日志或分级导出日志到相应文件功能,是一个非常好用的日志小组件.完整代码如下:
#pragma once #include<iostream> #include<stdarg.h> #include<time.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile "log.txt" class Log { public: Log() { printMethod = Screen; path = "./log/"; } void Enable(int method) { printMethod = method; } std::string levelToString(int level) { switch(level) { case Info: return "Info"; case Debug: return "Debug"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "None"; } } void printLog(int level, const std::string &logtxt) { switch(printMethod) { case Screen: std::cout << logtxt << std::endl; break; case Onefile: printOnefile(LogFile,logtxt); break; case Classfile: printClassfile(level,logtxt); break; default: break; } } void printOnefile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666); if(fd < 0) return; write(fd,logtxt.c_str(),logtxt.size()); close(fd); } void printClassfile(int level, const std::string &logtxt) { std::string filename = LogFile; filename +="."; filename += levelToString(level); printOnefile(filename, logtxt); } ~Log() {} void operator()(int level,const char* format,...) { time_t t = time(nullptr); struct tm *ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d %d:%d:%d]",levelToString(level).c_str(),ctime->tm_year+1900,\ ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,\ ctime->tm_min,ctime->tm_sec); va_list s; va_start(s,format); char rightbuffer[SIZE]; vsnprintf(rightbuffer,sizeof(rightbuffer),format,s); va_end(s); char logtxt[SIZE*2]; snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer); //格式:默认部分+自定义部分 //printf("%s\n",logtxt); printLog(level,logtxt); } private: int printMethod; std::string path; };
Makefile文件
主要用于自动化编译工程文件,完整文件如下:
.PHONY:all all:udpserver udpclient udpserver:Main.cc g++ -o $@ $^ -std=c++11 udpclient:UdpClient.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f udpserver udpclient
结语
希望这篇关于 搭建一个简单的UDP通信服务器和客户端 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!
相关文章推荐