阅读导航
引言
在前一篇文章中,我们详细介绍了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 库,用于多线程支持。
⭕打印日志文件
#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
定义了缓冲区大小。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";
// 打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符
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 socket
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. 绑定socket
struct 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_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"
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; // 设置地址族为 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;
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); // 关闭 socket
return 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++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
发表评论