【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)

在这里插入图片描述

阅读导航

  • 引言
  • 一、UDP协议
  • 二、UDP网络程序模拟实现
    • 1. 预备代码
      • ⭕makefile文件
      • ⭕打印日志文件
      • ⭕打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符
    • 2. UDP 服务器端实现(UdpServer.hpp)
    • 3. UDP 客户端实现(main函数)
  • 温馨提示

引言

在前一篇文章中,我们详细介绍了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 udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient

这段代码是一个简单的 Makefile 文件,用于编译 UDP 服务器(udpserver)和 UDP 客户端(udpclient)的程序。在这个 Makefile 中定义了两个规则:

  1. all:表示默认的目标,依赖于 udpserver 和 udpclient 目标,即执行 make 命令时会编译 udpserver 和 udpclient。
  2. clean:用于清理生成的可执行文件 udpserver 和 udpclient。

在 Makefile 中使用了一些特殊的关键字和变量:

  • .PHONY:声明 all 和 clean 是伪目标,不是真正的文件名。
  • $@:表示目标文件名。
  • $^:表示所有依赖文件列表。
  • -std=c++11:指定 C++ 的编译标准为 C++11。
  • -lpthread:链接 pthread 库,用于多线程支持。

⭕打印日志文件

#pragma once#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>#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); // 构建分类文件名,例如"log.txt.Debug/Warning/Fatal"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);printLog(level, logtxt); // 打印日志信息}private:int printMethod; // 日志输出方式std::string path; // 日志文件存放路径
};

该代码实现了一个简单的日志记录类(Log),其中包括设置日志输出方式(屏幕、单个文件、分类文件)和打印日志信息的功能。

  • Log 类是一个用于记录日志的类。
  • Enable 函数用于设置日志输出方式,可以选择屏幕打印、单个文件或分类文件。
  • printLog 函数根据设置的日志输出方式,将日志信息打印到屏幕、追加写入单个文件或分类文件。
  • printOneFile 函数用于将日志信息追加写入单个文件。
  • printClassFile 函数用于将日志信息追加写入分类文件。
  • levelToString 函数将日志级别转换为对应的字符串表示。
  • operator() 函数是重载的函数调用运算符,用于打印日志信息。
  • path 是日志文件存放路径,默认为"./log/"。
  • printMethod 是日志输出方式,默认为屏幕打印。
  • SIZE 定义了缓冲区大小。
  • InfoDebugWarningErrorFatal 是日志级别的定义。
  • ScreenOnefileClassfile 是日志输出方式的定义。
  • 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";// 打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符
int OpenTerminal()
{// 使用open函数以只写方式打开终端设备文件int fd = open(terminal.c_str(), O_WRONLY);if(fd < 0){// 如果打开终端设备文件失败,则输出错误信息到标准错误输出std::cerr << "open terminal error" << std::endl;return 1; // 返回错误代码}// 将终端设备文件的文件描述符复制给标准错误输出的文件描述符// 这样标准错误输出就会重定向到指定的终端设备上dup2(fd, 2);// 如果需要在此处输出信息到标准错误输出,可以使用printf等函数// 关闭文件描述符// close(fd);return 0; // 返回成功代码
}

这段代码的作用是打开一个终端设备文件 “/dev/pts/6”,将其作为标准错误输出(stderr)的目标文件描述符,实现将错误信息输出到指定的终端设备上。

  • terminal 变量存储了要打开的终端设备文件路径 “/dev/pts/6”。
  • OpenTerminal 函数尝试打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符。
    • 首先使用 open 函数打开终端设备文件,以只写方式(O_WRONLY)。
    • 如果成功打开终端设备文件,则将其文件描述符复制给标准错误输出的文件描述符(2),即 dup2(fd, 2),这样标准错误输出就会重定向到该终端设备上。
    • 如果打开终端设备文件失败,则输出错误信息到标准错误输出,并返回错误代码 1。
    • 最后函数返回0表示成功。

2. UDP 服务器端实现(UdpServer.hpp)

#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 <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";
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 socketsockfd_ = 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. 绑定socketstruct sockaddr_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_, (const struct sockaddr*)&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));}void CheckUser(const struct sockaddr_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;}}void Broadcast(const std::string& info, const std::string clientip, uint16_t clientport) {// 广播消息给所有在线用户for (const auto& 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, (struct sockaddr*)(&user.second), len);}}void Run() {isrunning_ = true;char inbuffer[size];while (isrunning_) {struct sockaddr_in client;socklen_t len = sizeof(client);// 接收客户端发送的消息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;}// 获取客户端的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, struct sockaddr_in> online_user_; // 在线用户列表
};
  • Log.hpp 是用于记录日志信息的头文件。
  • lg 是一个 Log 类的对象,用于输出日志信息。
  • enum 定义了两个错误类型:SOCKET_ERRBIND_ERR,分别表示 socket 创建错误和绑定错误。
  • defaultportdefaultip 分别设置默认的端口号和 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"using namespace std;// 函数声明:打印程序的使用方法
void Usage(std::string proc);// 结构体:用于传递线程参数
struct ThreadData
{struct sockaddr_in server; // 服务器地址结构体int sockfd; // socket 文件描述符std::string serverip; // 服务器 IP 地址
};// 线程函数:接收消息
void *recv_message(void *args);// 线程函数:发送消息
void *send_message(void *args);// 主函数
int main(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 结构体struct ThreadData td;bzero(&td.server, sizeof(td.server)); // 清零服务器地址结构体td.server.sin_family = AF_INET; // 设置地址族为 IPv4td.server.sin_port = htons(serverport); // 设置端口号(转换为网络字节序)td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 设置服务器 IP 地址(转换为网络字节序)// 创建 UDP sockettd.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0){cout << "socket error" << endl;return 1;}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); // 关闭 socketreturn 0;
}// 函数实现:打印程序的使用方法
void Usage(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)); // 清空缓冲区struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&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, (struct sockaddr *)&(td->server), len);while (true){cout << "Please Enter@ ";getline(cin, message); // 获取用户输入的消息sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len); // 发送消息给服务器}
}

温馨提示

感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/625536.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

✪✪✪宁波应对 CBAM 欧盟碳税:挑战与机遇并存✪✪✪

&#x1f349;宁波作为中国的&#x1f6e5;️重要港口城市&#xff0c;⏲️一直以来都是国内外&#x1f6a2;贸易的重要枢纽。然而&#xff0c;&#x1f329;️随着全球气候变化&#x1f30e;的日益严重&#xff0c;&#x1f310;欧盟等国家纷纷&#x1f6eb;开始实施碳税政策&…

【静态分析】软件分析课程实验A1-活跃变量分析和迭代求解器

1 作业导览 为 Java 实现一个活跃变量分析&#xff08;Live Variable Analysis&#xff09;。实现一个通用的迭代求解器&#xff08;Iterative Solver&#xff09;&#xff0c;用于求解数据流分析问题&#xff0c;也就是本次作业中的活跃变量分析。 Live Variable Analysis 详…

视频AI野生动物保护智能监管方案,撑起智能保护伞,守护野生动物

一、背景 在当今世界&#xff0c;野生动物保护已经成为全球性的重要议题。然而&#xff0c;由于野生动物生存环境的不断恶化以及非法狩猎等活动的盛行&#xff0c;保护野生动物变得尤为迫切。为了更有效地保护野生动物&#xff0c;利用视频智能监管技术成为一种可行的方案。 …

Java Spring 框架下利用 MyBatis 实现请求 MySQL 数据库的存储过程

Java Spring 框架下利用 MyBatis 实现请求 MySQL 数据库的存储过程 环境准备与前置知识1. 创建 MySQL 存储过程2. 配置数据源3. 创建实体类4. 创建 Mapper 接口5. 创建 Mapper XML 文件6. 创建 Service 接口及Impl实现类7. 创建 Controller 类8. 测试与总结 在现代的 Web 应用开…

揭秘!网络安全“4法2例”国家安全体系背后基石

今年的四月十五日&#xff0c;承载着特殊的意义&#xff0c;它标志着我国迎来了第九个全民国家安全教育日。今年的主题活动聚焦于“总体国家安全观 创新引领10周年”&#xff0c;唤醒了我们对新时代国家安全多元维度的深刻认知。 网络安全&#xff0c;作为国家总体安全架构中的…

2024五一杯数学建模A题思路分析

文章目录 1 赛题思路2 比赛日期和时间3 组织机构4 建模常见问题类型4.1 分类问题4.2 优化问题4.3 预测问题4.4 评价问题 5 建模资料 1 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 2 比赛日期和时间 报名截止时间&#xff1a;2024…

攻防世界---misc---hit-the-core

1.下载附件&#xff0c;用记事本打开&#xff0c;在记事本中发现一串可疑的字符串&#xff0c;很像flag 2.注意观察会发现&#xff0c;它的大写字母出现的很有规律&#xff1a;除第一个字母外&#xff0c;其余的都是每隔4个字母出现大写字母 3.接着用C语言写脚本 #include <…

LINUX网卡一般性问题分析

一、网卡相关概念 网卡&#xff1a;网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。 网络模型&#xff1a;OSI网络模型、TCP/IP网络模型 LINUX网络收发流程&#xff1a; 1. 内核分配一个主内存地址段&#xff08;DMA缓冲区)&#xff0c;网卡设备可以在…

Linux中进程和计划任务

一.程序 1.什么是程序 &#xff08;1&#xff09;是一组计算机能识别和执行的指令&#xff0c;运行于电子计算机上&#xff0c;满足人们某种需求的信息化工具 &#xff08;2&#xff09;用于描述进程要完成的功能&#xff0c;是控制进程执行的指令集 二.进程 1.什么是进程…

docker网路和主机通讯问题

#注 1&#xff0c;安装docker和启动容器服务的时候如果防火墙处于开启状态&#xff0c;那么重启docker里面的容器的时候必须开启防火墙&#xff0c;否则会出现iptable错误&#xff1b; 2&#xff0c;linux开启防火墙会导致主机和docker网络之间单向通讯&#xff0c;主机可以访…

2024蓝桥杯每日一题(最大公约数)

备战2024年蓝桥杯 -- 每日一题 Python大学A组 试题一&#xff1a;公约数 试题二&#xff1a;最大公约数 试题三&#xff1a;等差数列 试题四&#xff1a;最大比例 试题五&#xff1a;Hankson的趣味题 试题一&#xff1a;公约数 【题目描述】 …

呵护童心:儿童情感测试的关怀指南

引言&#xff1a; 儿童的情感健康对于其成长和发展至关重要。情感测试作为了解和评估儿童情感状态的重要工具&#xff0c;需要在专业人士的指导下进行&#xff0c;并且需要家长的配合和关注。本文将探讨儿童情感测试的注意事项&#xff0c;以期为儿童的情感健康提供更全面的保障…