
阅读导航
引言
在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。本文将延续上文内容,重点讨论简单的UDP网络程序模拟实现。通过本文的学习,读者将能够深入了解UDP协议的实际应用,并掌握如何编写简单的UDP网络程序。让我们一起深入探讨UDP网络程序的实现细节,为网络编程的学习之旅添上一份精彩的实践经验。
一、UDP协议
UDP(User Datagram Protocol)是一种无连接的、轻量级的网络传输协议,它提供了快速、简单的数据传输服务。下面是一个简单的UDP程序实现示例,包括一个UDP服务器和一个UDP客户端。详介绍可以看上一篇文章:UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同
二、UDP网络程序模拟实现
1. 预备代码
⭕makefile文件
.PHONY:all
all:udpserver udpclient
udpserver:Main.cc
g++ -o$@ $^ -std=c++11
udpclient:UdpClient.cc
g++ -o$@ $^ -lpthread-std=c++11
.PHONY:clean
clean:
rm-f udpserver udpclient
这段代码是一个简单的 Makefile 文件,用于编译 UDP 服务器(udpserver)和 UDP 客户端(udpclient)的程序。在这个 Makefile 中定义了两个规则:
- all:表示默认的目标,依赖于 udpserver 和 udpclient 目标,即执行 make 命令时会编译 udpserver 和 udpclient。
- clean:用于清理生成的可执行文件 udpserver 和 udpclient。
在 Makefile 中使用了一些特殊的关键字和变量:
- .PHONY:声明 all 和 clean 是伪目标,不是真正的文件名。
- $@:表示目标文件名。
- $^:表示所有依赖文件列表。
- -std=c++11:指定 C++ 的编译标准为 C++11。
- -lpthread:链接 pthread 库,用于多线程支持。
⭕打印日志文件
#pragmaonce#include<iostream>#include<time.h>#include<stdarg.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<stdlib.h>#defineSIZE1024#defineInfo0#defineDebug1#defineWarning2#defineError3#defineFatal4#defineScreen1#defineOnefile2#defineClassfile3#defineLogFile"log.txt"classLog{public:Log(){
printMethod = Screen;// 默认输出方式为屏幕打印
path ="./log/";// 默认日志文件存放路径}voidEnable(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";}}voidprintLog(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;}}voidprintOneFile(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);}voidprintClassFile(int level,const std::string &logtxt){
std::string filename = LogFile;
filename +=".";
filename +=levelToString(level);// 构建分类文件名,例如"log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);// 将日志信息追加写入分类文件}~Log(){}voidoperator()(int level,constchar*format,...){
time_t t =time(nullptr);structtm*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);printLog(level, logtxt);// 打印日志信息}private:int printMethod;// 日志输出方式
std::string path;// 日志文件存放路径};
该代码实现了一个简单的日志记录类(Log),其中包括设置日志输出方式(屏幕、单个文件、分类文件)和打印日志信息的功能。
Log类是一个用于记录日志的类。Enable函数用于设置日志输出方式,可以选择屏幕打印、单个文件或分类文件。printLog函数根据设置的日志输出方式,将日志信息打印到屏幕、追加写入单个文件或分类文件。printOneFile函数用于将日志信息追加写入单个文件。printClassFile函数用于将日志信息追加写入分类文件。levelToString函数将日志级别转换为对应的字符串表示。operator()函数是重载的函数调用运算符,用于打印日志信息。path是日志文件存放路径,默认为"./log/"。printMethod是日志输出方式,默认为屏幕打印。SIZE定义了缓冲区大小。Info、Debug、Warning、Error、Fatal是日志级别的定义。Screen、Onefile、Classfile是日志输出方式的定义。LogFile是单个文件名的定义。
⭕打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符
#include<iostream>#include<string>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>// 定义要打开的终端设备文件路径
std::string terminal ="/dev/pts/6";// 打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符intOpenTerminal(){// 使用open函数以只写方式打开终端设备文件int fd =open(terminal.c_str(), O_WRONLY);if(fd <0){// 如果打开终端设备文件失败,则输出错误信息到标准错误输出
std::cerr <<"open terminal error"<< std::endl;return1;// 返回错误代码}// 将终端设备文件的文件描述符复制给标准错误输出的文件描述符// 这样标准错误输出就会重定向到指定的终端设备上dup2(fd,2);// 如果需要在此处输出信息到标准错误输出,可以使用printf等函数// 关闭文件描述符// close(fd);return0;// 返回成功代码}
这段代码的作用是打开一个终端设备文件 “/dev/pts/6”,将其作为标准错误输出(stderr)的目标文件描述符,实现将错误信息输出到指定的终端设备上。
terminal变量存储了要打开的终端设备文件路径 “/dev/pts/6”。OpenTerminal函数尝试打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符。 - 首先使用open函数打开终端设备文件,以只写方式(O_WRONLY)。- 如果成功打开终端设备文件,则将其文件描述符复制给标准错误输出的文件描述符(2),即dup2(fd, 2),这样标准错误输出就会重定向到该终端设备上。- 如果打开终端设备文件失败,则输出错误信息到标准错误输出,并返回错误代码 1。- 最后函数返回0表示成功。
2. UDP 服务器端实现(UdpServer.hpp)
#pragmaonce#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<unordered_map>#include"Log.hpp"// 使用Log类记录日志信息
Log lg;enum{
SOCKET_ERR =1,
BIND_ERR
};uint16_t defaultport =8080;
std::string defaultip ="0.0.0.0";constint size =1024;classUdpServer{public:UdpServer(constuint16_t& port = defaultport,const std::string& ip = defaultip):sockfd_(0),port_(port),ip_(ip),isrunning_(false){}voidInit(){// 1. 创建UDP socket
sockfd_ =socket(AF_INET, SOCK_DGRAM,0);// PF_INETif(sockfd_ <0){lg(Fatal,"socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info,"socket create success, sockfd: %d", sockfd_);// 2. 绑定socketstructsockaddr_in local;bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port =htons(port_);// 端口号需要转换为网络字节序
local.sin_addr.s_addr =inet_addr(ip_.c_str());// 将IP地址转换为网络字节序if(bind(sockfd_,(conststructsockaddr*)&local,sizeof(local))<0){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));}voidCheckUser(conststructsockaddr_in& client,const std::string clientip,uint16_t clientport){// 检查用户是否已经存在在线用户列表中auto iter = online_user_.find(clientip);if(iter == online_user_.end()){
online_user_.insert({clientip, client});
std::cout <<"["<< clientip <<":"<< clientport <<"] add to online user."<< std::endl;}}voidBroadcast(const std::string& info,const std::string clientip,uint16_t clientport){// 广播消息给所有在线用户for(constauto& user : online_user_){
std::string message ="[";
message += clientip;
message +=":";
message += std::to_string(clientport);
message +="]# ";
message += info;
socklen_t len =sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(),0,(structsockaddr*)(&user.second), len);}}voidRun(){
isrunning_ =true;char inbuffer[size];while(isrunning_){structsockaddr_in client;
socklen_t len =sizeof(client);// 接收客户端发送的消息
ssize_t n =recvfrom(sockfd_, inbuffer,sizeof(inbuffer)-1,0,(structsockaddr*)&client,&len);if(n <0){lg(Warning,"recvfrom error, errno: %d, err string: %s", errno,strerror(errno));continue;}// 获取客户端的IP地址和端口号uint16_t clientport =ntohs(client.sin_port);
std::string clientip =inet_ntoa(client.sin_addr);// 检查用户是否已经存在在线用户列表中CheckUser(client, clientip, clientport);
std::string info = inbuffer;// 将接收到的消息广播给所有在线用户Broadcast(info, clientip, clientport);}}~UdpServer(){if(sockfd_ >0)close(sockfd_);}private:int sockfd_;// 网络文件描述符
std::string ip_;// 服务器IP地址uint16_t port_;// 服务器端口号bool isrunning_;// 服务器运行状态
std::unordered_map<std::string,structsockaddr_in> online_user_;// 在线用户列表};
Log.hpp是用于记录日志信息的头文件。lg是一个Log类的对象,用于输出日志信息。enum定义了两个错误类型:SOCKET_ERR和BIND_ERR,分别表示 socket 创建错误和绑定错误。defaultport和defaultip分别设置默认的端口号和 IP 地址。size定义接收缓冲区的大小为 1024 字节。UdpServer类封装了一个 UDP 服务器。- 构造函数
UdpServer接受端口号和 IP 地址作为参数,并初始化成员变量。 Init函数用于初始化 UDP 服务器,其中: - 创建 UDP socket,并检查创建是否成功。- 绑定 socket 到指定的 IP 地址和端口号,并检查绑定是否成功。CheckUser函数用于检查用户是否已经存在在线用户列表中,如果不存在则将其添加到列表中。Broadcast函数用于向所有在线用户广播消息,其中: - 消息格式为[发送者IP:发送者端口号]# 消息内容。- 使用sendto函数发送消息给每个在线用户。Run函数是 UDP 服务器的主循环,其中: - 循环接收客户端发送的消息,并将其广播给所有在线用户。- 对每个客户端,获取其 IP 地址和端口号,并进行用户检查和消息广播。~UdpServer析构函数关闭网络文件描述符。sockfd_是网络文件描述符,用于创建和管理网络连接。ip_是服务器的 IP 地址。port_是服务器的端口号。isrunning_表示服务器的运行状态,用于控制循环退出。online_user_是一个无序映射,用于保存在线用户的 IP 地址和对应的sockaddr_in结构体。
3. UDP 客户端实现(main函数)
#include<iostream>#include<cstdlib>#include<unistd.h>#include<strings.h>#include<string.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<pthread.h>#include"Terminal.hpp"usingnamespace std;// 函数声明:打印程序的使用方法voidUsage(std::string proc);// 结构体:用于传递线程参数structThreadData{structsockaddr_in server;// 服务器地址结构体int sockfd;// socket 文件描述符
std::string serverip;// 服务器 IP 地址};// 线程函数:接收消息void*recv_message(void*args);// 线程函数:发送消息void*send_message(void*args);// 主函数intmain(int argc,char*argv[]){if(argc !=3){Usage(argv[0]);// 打印使用方法exit(0);}// 解析命令行参数
std::string serverip = argv[1];// 服务器 IP 地址uint16_t serverport = std::stoi(argv[2]);// 服务器端口号// 初始化 ThreadData 结构体structThreadData td;bzero(&td.server,sizeof(td.server));// 清零服务器地址结构体
td.server.sin_family = AF_INET;// 设置地址族为 IPv4
td.server.sin_port =htons(serverport);// 设置端口号(转换为网络字节序)
td.server.sin_addr.s_addr =inet_addr(serverip.c_str());// 设置服务器 IP 地址(转换为网络字节序)// 创建 UDP socket
td.sockfd =socket(AF_INET, SOCK_DGRAM,0);if(td.sockfd <0){
cout <<"socket error"<< endl;return1;}
td.serverip = serverip;// 存储服务器 IP 地址
pthread_t recvr, sender;// 定义接收消息和发送消息的线程pthread_create(&recvr,nullptr, recv_message,&td);// 创建接收消息线程pthread_create(&sender,nullptr, send_message,&td);// 创建发送消息线程// 等待接收消息和发送消息的线程退出pthread_join(recvr,nullptr);pthread_join(sender,nullptr);close(td.sockfd);// 关闭 socketreturn0;}// 函数实现:打印程序的使用方法voidUsage(std::string proc){
std::cout <<"\n\rUsage: "<< proc <<" serverip serverport\n"<< std::endl;}// 线程函数实现:接收消息void*recv_message(void*args){
ThreadData *td =static_cast<ThreadData *>(args);// 强制类型转换为 ThreadData 结构体指针char buffer[1024];// 接收消息的缓冲区while(true){memset(buffer,0,sizeof(buffer));// 清空缓冲区structsockaddr_in temp;
socklen_t len =sizeof(temp);
ssize_t s =recvfrom(td->sockfd, buffer,1023,0,(structsockaddr*)&temp,&len);// 接收消息if(s >0){
buffer[s]=0;
cerr << buffer << endl;// 输出接收到的消息}}}// 线程函数实现:发送消息void*send_message(void*args){
ThreadData *td =static_cast<ThreadData *>(args);// 强制类型转换为 ThreadData 结构体指针
string message;// 存储用户输入的消息
socklen_t len =sizeof(td->server);// 服务器地址的长度// 发送欢迎消息
std::string welcome = td->serverip +" comming...";sendto(td->sockfd, welcome.c_str(), welcome.size(),0,(structsockaddr*)&(td->server), len);while(true){
cout <<"Please Enter@ ";getline(cin, message);// 获取用户输入的消息sendto(td->sockfd, message.c_str(), message.size(),0,(structsockaddr*)&(td->server), len);// 发送消息给服务器}}
温馨提示
感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!
再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
版权归原作者 Yawesh 所有, 如有侵权,请联系我们删除。