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

这篇具有很好参考价值的文章主要介绍了【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

引言

在前一篇文章中,我们详细介绍了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 中定义了两个规则:

  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 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_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; // 设置地址族为 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++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现),Linux,网络,linux,udp文章来源地址https://www.toymoban.com/news/detail-844587.html

到了这里,关于【探索Linux】P.28(网络编程套接字 —— 简单的UDP网络程序模拟实现)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux网络编程】网络编程套接字二

    喜欢的点赞,收藏,关注一下把! TCP和UDP在编程接口上是非常像的,前面我们说过TCP是面向连接的,UDP我们上篇博客也写过了,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。具体怎么不一样,我们

    2024年04月15日
    浏览(52)
  • linux【网络编程】之网络套接字预备

    在【网络基础】中我们提到了IP地址,接下来了解一下网络通信中其他方面的知识 端口号是一个2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理; 一个端口号只能被一个进程占用 通信原理 (公网)IP唯一标识一台主机,这样两台

    2024年02月05日
    浏览(53)
  • 【Linux网络】网络编程套接字(TCP)

    目录 地址转换函数 字符串IP转整数IP 整数IP转字符串IP 关于inet_ntoa 简单的单执行流TCP网络程序 TCP socket API 详解及封装TCP socket  服务端创建套接字  服务端绑定  服务端监听  服务端获取连接  服务端处理请求 客户端创建套接字 客户端连接服务器 客户端发起请求 服务器测试

    2024年03月21日
    浏览(58)
  • Linux网络编程(二-套接字)

    目录 一、背景知识 1.1 端口号 1.2 网络字节序 1.3 地址转换函数  二、Socket简介 三、套接字相关的函数  3.1 socket() 3.2 bind() 3.3 connect() 3.4 listen() 3.5 accept()  3.6 read()/recv()/recvfrom() 3.7 send()/sendto()  3.8 close()  四、UPD客服/服务端实验  1.1 端口号 端口号是访问服务器的标识 ,就好像

    2024年01月22日
    浏览(72)
  • 【Linux】网络编程套接字一

    上篇博客由唐僧的例子我们知道: 在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。 思考一下: 不考虑中间的一系列步骤,两台主机我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上。 但是我们把

    2024年03月26日
    浏览(123)
  • 【Linux网络编程】网络编程套接字(TCP服务器)

    作者:爱写代码的刚子 时间:2024.4.4 前言:本篇博客主要介绍TCP及其服务器编码 只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址 但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换 字符串转in

    2024年04月14日
    浏览(63)
  • Linux网络编程——tcp套接字

    本章Gitee仓库:tcp套接字 客户端: 客户端: 关于构造和初始化,可以直接在构造的时候,将服务器初始化,那为什么还要写到 init 初始化函数里面呢? 构造尽量简单一点,不要做一些“有风险”的操作。 tcp 是面向连接的,通信之前要建立连接,服务器处于等待连接到来的

    2024年02月20日
    浏览(45)
  • 【Linux】网络基础+UDP网络套接字编程

    只做自己喜欢做的事情,不被社会和时代裹挟着前进,是一件很奢侈的事。 1. 首先计算机是人类设计出来提高生产力的工具,而人类的文明绵延至今一定离不开人类之间互相的协作,既然人类需要协作以完成更为复杂的工作和难题,所以计算机作为人类的工具自然也一定需要

    2024年02月08日
    浏览(49)
  • 【Linux】网络---->套接字编程(TCP)

    TCP的编程流程:大致可以分为五个过程,分别是准备过程、连接建立过程、获取新连接过程、消息收发过程和断开过程。 1.准备过程:服务端和客户端需要创建各自的套接字,除此之外服务端还需要绑定自己的地址信息和进行监听。注意:服务端调用listen函数后,处理监听状

    2024年02月04日
    浏览(51)
  • 【Linux网络】网络编程套接字(预备知识+UDP)

    目录 预备知识 1. 理解源IP地址和目的IP地址 2. 理解源MAC地址和目的MAC地址 3. 认识端口号  4. 理解源端口号和目的端口号 5. 端口号(port) vs 进程pid 6. 认识TCP协议和认识UDP协议 7. 网络字节序 socket编程接口  1. socket 常见API 2. sockaddr结构  简单的UDP网络程序  1. 服务端创建udp

    2024年02月19日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包