【Linux】基于Udp的简易服务端和客户端

目录

UDP的概念

简单的UDP回声服务器

服务端

类成员变量

构造函数

成员函数

服务端的启动

函数原型

函数流程

客户端

包含的头文件

函数原型

main函数流程

可以传处理方法的简单udp服务器

服务端

类定义

成员变量

构造函数

成员函数

服务端的启动

程序结构和功能

客户端

程序结构和功能

程序特点

简单的udp聊天室服务器

服务端

类定义和成员变量

构造函数和析构函数

成员函数

线程安全和并发控制

总结

服务端的启动

包含的头文件

全局变量和函数

main函数

客户端

包含的头文件

Usage 函数

ThreadData 类

RecverRoutine 函数

SenderRoutine 函数

main 函数

依赖类

错误类型

日志类

防止拷贝类

线程池

线程类

网络信息类

守护锁类


UDP的概念

UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议,正式规范为RFC 768。

  1. 无连接性:UDP是无连接的,这意味着在发送数据之前不需要建立连接。这一特点使得UDP能够减少开销和发送数据之前的时延,从而提高了传输效率。

  2. 不可靠性:UDP不提供可靠的数据传输服务。它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份。换句话说,UDP使用尽最大努力交付,但不保证数据能够可靠到达目的地。

  3. 面向报文:UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序需要负责处理可能出现的数据包乱序或丢失的情况。

  4. 支持多种交互通信:UDP支持一对一、一对多、多对一和多对多的交互通信方式,这使得它在某些特定场景下非常灵活和有用。

  5. 首部开销小:UDP的首部只有8个字节,相比TCP的20个字节的首部要短,这进一步减少了传输开销。

  6. 适用场景:由于UDP的上述特点,它特别适用于需要快速传输少量数据的场景,如实时应用程序(视频会议、在线游戏、语音聊天等),以及对数据传输速度要求较高、对数据完整性要求较低的场景。此外,UDP还常用于一些控制信息的传输,如路由协议、DNS协议等。

UDP以其高效、灵活和轻量级的特性,在特定场景下发挥着重要作用。然而,由于其不可靠性,使用UDP时需要谨慎处理数据丢失和乱序的问题。

简单的UDP回声服务器

服务端

这个UdpServer类是一个简单的UDP回声服务器,能够接收客户端发送的数据,并将其原样返回。

类成员变量

  • _prot: 存储UDP服务器要监听的端口号,默认为DefaultPort(8888)。

  • _sockfd: 存储套接字文件描述符,用于网络通信。默认值为DefaultSockfd(-1),表示尚未创建套接字。

构造函数

  • UdpServer(uint16_t port = DefaultPort): 构造函数允许用户指定一个端口号,如果不提供,则使用默认端口DefaultPort

成员函数

  1. Init():

    • 创建一个UDP套接字。

    • 如果套接字创建失败,记录错误信息并退出程序。

    • 绑定套接字到指定的IP地址(在这里是INADDR_ANY,表示接受任何IP地址的连接)和端口号。

    • 如果绑定失败,记录错误信息并退出程序。

  2. Start():

    • 这是一个无限循环,服务器将持续运行,等待并处理客户端的请求。

    • 使用recvfrom函数从套接字接收数据,并保存到buffer中。

    • 如果接收到数据(n > 0),服务器会将接收到的数据打印到标准输出,并将相同的数据发送回客户端(即实现了一个简单的回声服务器)。

  3. 析构函数 (~UdpServer()):

    • 目前析构函数是空的,但通常在这里应该关闭套接字和释放相关资源。在这个简单的例子中,由于程序在出现错误时会直接退出,所以没有在析构函数中处理资源释放。

#pragma once

#include "ErrInfo.hpp"
#include "Log.hpp"
#include "NoCopy.hpp"
#include <arpa/inet.h>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <netinet/in.h>
#include <queue>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <vector>

const static std::string DefaultIp = "0.0.0.0";
const static int DefaultSockfd = -1;
const static uint16_t DefaultPort = 8888;
const static int DefaultSize = 1024;
class UdpServer : public NoCopy
{
public:
  UdpServer(uint16_t prot = DefaultPort)
      : _prot(prot),
        _sockfd(DefaultSockfd)
  {
  }

  void Init()
  {
    // 创建套接字
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
      lg.LogMessage(Fatal, "Creat socket faile! errno:%d:%s\n", errno, strerror(errno));
      exit(Socket_Err);
    }

    lg.LogMessage(Info, "Creat socket success! socket: %d\n", _sockfd);
    // 绑定网络信息

    struct sockaddr_in local;
    bzero(&local, sizeof(local));

    local.sin_family = AF_INET;
    local.sin_port = htons(_prot);
    local.sin_addr.s_addr = INADDR_ANY;

    // 将结构体绑定到内核
    int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
    if (n != 0)
    {
      lg.LogMessage(Fatal, "bine err,%d ... %s\n", errno, strerror(errno));
      exit(Bind_Err);
    }
  }
  void Start()
  {
    char buffer[DefaultSize];

    while (true)
    {
      struct sockaddr_in peer; // 远端
      socklen_t len = sizeof(peer);
      ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
      if (n > 0)
      {
        buffer[n] = 0;
        std::cout << "client say# " << buffer << std::endl;
        sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
      }
    }
  }
  ~UdpServer()
  {
  }

private:
  uint16_t _prot;
  int _sockfd;
};

服务端的启动

函数原型

int main(int argc, char *argv[])

  • argc: 命令行参数的数量。

  • argv: 命令行参数的数组,其中argv[0]通常是程序的名称。

函数流程

  1. 检查命令行参数数量:

    • 如果命令行参数数量不等于2(程序名称和一个端口号),则打印使用方法并返回错误代码Usage_Err

  2. 解析端口号:

    • 使用std::stoi(argv[1])将命令行中的第二个参数(即argv[1])从字符串转换为整数,这个整数表示UDP服务器要监听的端口号。

  3. 创建UdpServer对象:

    • 使用C++11中的std::unique_ptr智能指针来管理UdpServer对象的生命周期。这样做的好处是,当unique_ptr超出作用域或被销毁时,它会自动释放所指向的对象,从而防止内存泄漏。

    • std::make_unique<UdpServer>(port)用于创建一个UdpServer对象的实例,并将其端口号设置为从命令行参数解析得到的值。

  4. 初始化服务器:

    • 调用usvr->Init()来初始化UDP服务器,包括创建套接字和绑定到指定的端口。

  5. 启动服务器:

    • 调用usvr->Start()来启动UDP服务器。这个函数会进入一个无限循环,等待并处理来自客户端的请求。

  6. 程序结束:

    • 函数返回0,表示程序正常结束。但实际上,由于usvr->Start()是一个无限循环,所以程序通常不会到达这一步,除非在Start函数内部发生了某种错误或异常导致循环退出。

#include "UdpServer.hpp"
#include "ErrInfo.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}

// ./udp_server 127.0.0.1 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->Init();
    usvr->Start();

    return 0;
}

客户端

这是一个C++程序,它实现了一个简单的UDP客户端。以下是对这个程序的详细介绍:

包含的头文件

程序首先包含了一系列头文件,这些头文件提供了创建套接字、处理网络数据和输入/输出等功能的必要接口。

函数原型

  • void Usage(const std::string &process): 打印如何使用这个程序的说明。

  • int main(int argc, char *argv[]): 程序的入口点。

main函数流程

  1. 检查命令行参数数量:

    • 如果参数数量不等于3(程序名称、服务器IP和服务器端口),则打印使用方法并返回错误代码1。

  2. 解析服务器IP和端口:

    • 从命令行参数中获取服务器IP和端口,端口被转换为整数。

  3. 创建套接字:

    • 使用socket函数创建一个UDP套接字。如果创建失败,则打印错误信息并返回错误代码2。

  4. 初始化服务器地址结构:

    • 使用sockaddr_in结构来表示服务器的地址。这个结构被初始化为0,然后设置其家族为AF_INET(IPv4),端口号转换为网络字节序,并使用inet_addr函数将服务器IP地址转换为网络字节序的整数形式。

  5. 主循环:

    • 程序进入一个无限循环,等待用户输入要发送的数据。
      • 提示用户输入数据,并使用getline函数从标准输入读取数据。

      • 使用sendto函数将数据发送到服务器。如果发送成功,程序将继续执行;否则,跳出循环。

      • 如果数据成功发送,程序将尝试使用recvfrom函数从服务器接收响应。如果接收到数据,程序将打印服务器的响应;否则,跳出循环。

  6. 关闭套接字:

    • 在退出循环之前,使用close函数关闭套接字。

  7. 程序结束:

    • 返回0,表示程序正常结束。

#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }
    std::cout << "create socket success: " << sock << std::endl;

 
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        // 我们要发的数据
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);
        // 我们要发给谁呀?server
        ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if(n > 0)
        {
            char buffer[1024];
            //收消息
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t m = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len); // 一般建议都是要填的.
            if(m > 0)
            {
                buffer[m] = 0;
                std::cout << "server echo# " << buffer << std::endl;
            }
            else
                break;
        }
        else
            break;
    }

    close(sock);
    return 0;
}

可以传处理方法的简单udp服务器

服务端

这个UdpServer类提供了一个简单的UDP服务器框架,用户可以通过提供自定义的消息处理函数来扩展其功能。

类定义

UdpServer类继承自NoCopy类,这意味着UdpServer对象不能被复制,这通常是为了防止资源管理的复杂性或潜在的bug。

成员变量

  • _port: 服务器的端口号,默认为8888。

  • _socket: 套接字文件描述符,默认为-1,表示尚未创建。

  • _OnMessage: 一个函数对象,用于处理接收到的消息。

构造函数

构造函数接受一个函数对象OnMessage,用于处理接收到的UDP消息,以及可选的端口号和套接字文件描述符。

成员函数

  1. Init(): 初始化函数,用于创建套接字并将其绑定到指定的端口。

    • 首先,使用socket函数创建一个UDP套接字。

    • 如果套接字创建失败,将记录错误日志并退出程序,返回Socket_Err错误代码。

    • 使用memset函数清空sockaddr_in结构体,并设置其端口、地址族和IP地址(INADDR_ANY表示接受任何IPv4地址的连接)。

    • 使用bind函数将套接字绑定到指定的端口和地址。

    • 如果绑定失败,将记录错误日志并退出程序,返回Bind_Err错误代码。

  2. Start(): 启动服务器,开始接收和处理UDP消息。

    • 进入一个无限循环,等待接收UDP消息。

    • 使用recvfrom函数接收来自客户端的UDP消息,并将其存储在buffer中。

    • 如果接收到消息,将调用_OnMessage函数对象处理该消息,并将响应发送回客户端。

    • 注意,这里使用了InetAddr类(可能是自定义的)来打印或处理客户端的地址信息。

#pragma once

#include "ErrInfo.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
#include "NoCopy.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include <cstring>
#include <functional>
#include <iostream>

#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

using func_t = std::function<std::string(std::string)>; // 定义了一个函数类型

static const uint16_t DefaultPort = 8888;
const static int DefaultFd = -1;
const static int DefaultSize = 1024;


class UdpServer : public NoCopy
{
public:
  UdpServer(func_t OnMessage,const uint16_t &port = DefaultPort, const int &socket = DefaultFd)
      : _port(port),
        _socket(socket),
        _OnMessage(OnMessage)
  {
  }

  void Init()
  {
    // 创建套接字
    _socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (_socket < 0)
    {
      lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
      exit(Socket_Err);
    }

    lg.LogMessage(Info, "socket success, sockfd: %d\n", _socket);
    // 填充服务端信息

    sockaddr_in local;
    memset(&local, 0, sizeof(local));

    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;
    local.sin_family = AF_INET;

    // 将填充好的结构体绑定到操作系统中
    socklen_t len = sizeof(local);
    int ret = bind(_socket, (sockaddr *)&local, len);
    if (ret != 0)
    {
      lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
      exit(Bind_Err);
    }
  }
  void Start()
  {
    while (true)
    {
      char buffer[DefaultSize];
      struct sockaddr_in peer; // 远端
      socklen_t len = sizeof(peer);

       //这里不能使用strlen
      ssize_t ret = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len);
     

      if (ret > 0)
      {
        InetAddr addr(peer);
        buffer[ret] = 0;

        // 处理消息
        std::string response = _OnMessage(buffer);

        // std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;
        sendto(_socket, response.c_str(), response.size(), 0, (struct sockaddr *)&peer, len);
      }
    }
  }

private:
  uint16_t _port;
  int _socket;
  func_t _OnMessage;
};

服务端的启动

这个程序是一个简单的UDP服务器示例,展示了如何处理来自客户端的消息,并执行一些基本的操作。

程序结构和功能

  1. 包含头文件:程序首先包含了多个头文件,包括自定义的"ErrInfo.hpp"、"UdpServer.hpp"以及其他标准库头文件,这些头文件提供了网络通信、字符串处理、标准输入输出等功能。

  2. Usage函数:定义了一个Usage函数,当命令行参数不足时,会打印出正确的使用方法。

  3. 黑名单词汇:定义了一个black_words的字符串向量,里面包含了一些可能用于执行危险操作的命令词汇。

  4. OnMessageDefault函数:这是一个默认的消息处理函数,它接收一个字符串作为请求,然后返回一个带有戏谑性附加语的请求字符串。

  5. SafeCheck函数:这个函数接收一个命令字符串,并检查它是否包含黑名单中的任何词汇。如果包含,函数返回false,表示该命令可能是不安全的;否则返回true

  6. ExecuteCommand函数:这个函数接收一个命令字符串,首先通过SafeCheck函数检查其安全性。如果命令不安全,返回一个警告字符串。如果安全,它尝试执行该命令,并捕获命令的输出,然后返回这个输出。如果执行过程中出现错误,也会返回相应的错误信息。

  7. main函数

    • 参数检查:程序首先检查命令行参数的数量。如果参数数量不正确(不是2个,包括程序名和端口号),则调用Usage函数并退出程序。

    • 创建UDP服务器:使用端口号创建一个UdpServer对象。这个服务器将使用OnMessageDefault函数作为默认的消息处理函数,但代码中也有一行被注释掉的代码,显示可以使用ExecuteCommand函数作为消息处理函数(这可能会带来安全风险)。

    • 服务器初始化和启动:调用Init方法初始化服务器,然后调用Start方法启动服务器。

#include "ErrInfo.hpp"
#include "UdpServer.hpp"
#include <cstdio>
#include <memory>
#include <vector>

void Usage(std::string proc)
{
  std::cout << "Usage : \n\t" << proc << " local_port\n"
            << std::endl;
}

std::vector<std::string> black_words = {
    "rm",
    "unlink",
    "cp",
    "mv",
    "chmod",
    "exit",
    "reboot",
    "halt",
    "shutdown",
    "top",
    "kill",
    "dd",
    "vim",
    "vi",
    "nano",
    "man"};

std::string OnMessageDefault(std::string request)
{
  return request + "[haha, got you!!]";
}

bool SafeCheck(std::string command)
{
  for (auto &k : black_words)
  {
    std::size_t pos = command.find(k);
    if (pos != std::string::npos)
      return false;
  }

  return true;
}

// ls -a -l/ rm / tocuh
std::string ExecuteCommand(std::string command)
{
  if (!SafeCheck(command))
    return "bad man!!";

  std::cout << "get a message: " << command << std::endl;
  FILE *fp = popen(command.c_str(), "r");
  if (fp == nullptr)
  {
    return "execute error, reason is unknown";
  }

  std::string response;
  char buffer[1024];
  while (true)
  {
    char *s = fgets(buffer, sizeof(buffer), fp);
    if (!s)
      break;
    else
      response += buffer;
  }
  pclose(fp);
  return response.empty() ? "success" : response;
}

// ./udp_server 8888
int main(int argc, char *argv[])
{
  if (argc != 2)
  {
    Usage(argv[0]);
    return Usage_Err;
  }

  // std::string ip = argv[1];
  uint16_t port = std::stoi(argv[1]);
  std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(OnMessageDefault, port);
  // std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ExecuteCommand, port);
  usvr->Init();
  usvr->Start();

  return 0;
}

客户端

这是一个简单的UDP客户端程序,用C++编写。以下是对这个程序的详细介绍:

程序结构和功能

  1. 包含头文件:程序首先包含了多个头文件,这些头文件提供了网络通信、字符串处理、标准输入输出等功能。

  2. Usage函数:定义了一个Usage函数,当命令行参数不足时,会打印出正确的使用方法,并退出程序。

  3. main函数

    • 参数检查:程序首先检查命令行参数的数量。如果参数数量不正确(不是3个,包括程序名、服务器IP和端口),则调用Usage函数并退出。

    • 创建套接字:使用socket函数创建一个UDP套接字。如果创建失败,则打印错误信息并退出。

    • 服务器信息设置:使用sockaddr_in结构体来存储服务器的地址信息,包括IP地址和端口号。IP地址通过inet_addr函数从字符串转换为网络字节序的格式。

    • 主循环:进入一个无限循环,用于发送和接收数据。
      • 发送数据:从标准输入读取用户输入的数据,并使用sendto函数发送到服务器。发送成功后,等待接收服务器的响应。

      • 接收数据:使用recvfrom函数接收来自服务器的响应,并打印到标准输出。如果接收失败或接收到的数据量为0,则退出循环。

    • 关闭套接字:在退出循环后,使用close函数关闭套接字。

程序特点

  1. UDP通信:该程序使用UDP协议进行通信,UDP是无连接的,不保证数据的可靠传输,但传输速度快,适用于对实时性要求较高或数据量较小的场景。

  2. 命令行参数:程序通过命令行参数获取服务器的IP地址和端口号,提高了程序的灵活性。

  3. 错误处理:在关键步骤(如套接字创建、数据发送和接收)中进行了错误检查,并在出现错误时打印错误信息并退出程序。

  4. 用户交互:程序通过标准输入接收用户输入的数据,并将服务器的响应打印到标准输出,实现了基本的用户交互功能。

  5. 资源释放:在程序结束时关闭了套接字,避免了资源泄漏。

#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <unistd.h>

void Usage(const std::string &process)
{
  std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    Usage(argv[0]);
    return 1;
  }

  std::string serverip = argv[1];
  uint16_t serverport = std::stoi(argv[2]);

  // 1. 创建socket
  int sock = socket(AF_INET, SOCK_DGRAM, 0);
  if (sock < 0)
  {
    std::cerr << "socket error: " << strerror(errno) << std::endl;
    return 2;
  }
  std::cout << "create socket success: " << sock << std::endl;

  // 2. client要不要进行bind? 一定要bind的!!但是,不需要显示bind,client会在首次发送数据的时候会自动进行bind
  // 为什么?server端的端口号,一定是众所周知,不可改变的,client 需要 port,bind随机端口.
  // 为什么?client会非常多.
  // client 需要bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号
  // 2.1 填充一下server信息
  struct sockaddr_in server;
  memset(&server, 0, sizeof(server));
  server.sin_family = AF_INET;
  server.sin_port = htons(serverport);
  server.sin_addr.s_addr = inet_addr(serverip.c_str());

  while (true)
  {
    // 我们要发的数据
    std::string inbuffer;
    std::cout << "Please Enter# ";
    std::getline(std::cin, inbuffer);
    // 我们要发给谁呀?server
    ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
    if (n > 0)
    {
      char buffer[1024];
      // 收消息
      struct sockaddr_in temp;
      socklen_t len = sizeof(temp);
      ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len); // 一般建议都是要填的.
      if (m > 0)
      {
        buffer[m] = 0;
        std::cout << "server echo# " << buffer << std::endl;
      }
      else
        break;
    }
    else
      break;
  }

  close(sock);
  return 0;
}

简单的udp聊天室服务器

服务端

类定义和成员变量

  • UdpServer类继承自nocopy类,意味着这个类的对象不能被复制,这通常是为了避免意外的对象复制可能引发的问题。

  • 类中定义了几个私有成员变量:
    • _port:服务器监听的端口号,默认为8888。

    • _sockfd:套接字文件描述符,用于网络通信。

    • _online_user:一个InetAddr类型的向量,存储当前在线的用户地址信息。

    • _user_mutex:互斥锁,用于保护_online_user在多线程环境下的安全访问。

构造函数和析构函数

  • 构造函数UdpServer(uint16_t port = defaultport):初始化服务器的端口号,并设置默认的套接字文件描述符和初始化互斥锁。

  • 析构函数~UdpServer():销毁互斥锁。

成员函数

  • Init():初始化函数,用于创建套接字并将其绑定到指定的端口上。如果套接字创建或绑定失败,程序将记录错误信息并退出。成功创建套接字后,会启动线程池。

  • AddOnlineUser(InetAddr addr):将一个在线用户的地址添加到_online_user列表中。这个函数是线程安全的,因为它在修改_online_user之前会先锁定互斥锁。

  • Route(int sock, const std::string &message):将消息路由到所有在线的用户。这个函数也是线程安全的,同样使用了互斥锁。

  • Start():启动服务器的主循环,接收来自客户端的消息。当接收到消息时,会将发送者的地址添加到在线用户列表中,并将消息广播给所有在线用户。这个消息广播的任务被添加到线程池中异步执行。

线程安全和并发控制

  • 类中使用了互斥锁(pthread_mutex_t)来保护在线用户列表_online_user在多线程环境下的安全访问。当需要修改这个列表时,会先锁定互斥锁,确保同一时间只有一个线程可以修改这个列表。

  • 使用线程池(ThreadPool)来异步处理消息广播的任务,提高服务器的并发处理能力。

总结

这个UdpServer类是一个简单的UDP服务器实现,具有基本的并发控制和线程安全特性。它能够接收来自客户端的消息,并将消息广播给所有在线的用户。这个类使用了线程池来提高服务器的并发处理能力,并使用互斥锁来保护共享资源的安全访问。

#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <pthread.h>
// #include <mutex>
// #include <condition_variable>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
using task_t = std::function<void()>;



// 聚焦在IO上
class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t port = defaultport) : _port(port), _sockfd(defaultfd)
    {
        pthread_mutex_init(&_user_mutex, nullptr);
    }
    void Init()
    {
        // 1. 创建socket,就是创建了文件细节
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
            exit(Socket_Err);
        }

        lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);

        // 2. 绑定,指定网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // memset
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // 0

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字节IP 2. 变成网络序列

        // 结构体填完,设置到内核中了吗??没有
        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n != 0)
        {
            lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
            exit(Bind_Err);
        }

        ThreadPool<task_t>::GetInstance()->Start();
    }

    void AddOnlineUser(InetAddr addr)
    {
        LockGuard lockguard(&_user_mutex);
        for (auto &user : _online_user)
        {
            if (addr == user)
                return;
        }
        _online_user.push_back(addr);
        lg.LogMessage(Debug, "%s:%d is add to onlineuser list...\n", addr.Ip().c_str(), addr.Port());
    }
    void Route(int sock, const std::string &message)
    {
        LockGuard lockguard(&_user_mutex);
        for (auto &user : _online_user)
        {
            sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&user.GetAddr(), sizeof(user.GetAddr()));
            lg.LogMessage(Debug, "server send message to %s:%d, message: %s\n", user.Ip().c_str(), user.Port(), message.c_str());
        }
    }
    void Start()
    {
        // 服务器永远不退出
        char buffer[defaultsize];
        for (;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 不能乱写
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);
                AddOnlineUser(addr);
                buffer[n] = 0;
                std::string message = "[";
                message += addr.Ip();
                message += ":";
                message += std::to_string(addr.Port());
                message += "]# ";
                message += buffer;
                task_t task = std::bind(&UdpServer::Route, this, _sockfd, message);
                ThreadPool<task_t>::GetInstance()->Push(task);
            }
        }
    }
    ~UdpServer()
    {
        pthread_mutex_destroy(&_user_mutex);
    }

private:
    uint16_t _port;
    int _sockfd;

    std::vector<InetAddr> _online_user; // 会被多个线程同时访问的
    pthread_mutex_t _user_mutex;
};

服务端的启动

这是一个简单的UDP服务器示例,用于演示基本的网络通信和命令执行功能。

包含的头文件

程序首先包含了一些必要的头文件,这些头文件提供了程序所需的各种功能,如标准输入输出、内存管理、向量容器以及自定义的头文件(可能是为了网络通信和日志记录等)。

全局变量和函数

  • black_words:这是一个包含“危险”命令的字符串向量。这些命令被认为是可能对系统造成危害的,因此程序会检查用户输入的命令是否包含这些“黑名单”词汇。

  • OnMessageDefault:这是一个默认的消息处理函数,它简单地将接收到的消息加上一个固定的字符串后返回。

  • SafeCheck:这个函数检查一个命令字符串是否包含black_words中的任何词汇。如果包含,则返回false,表示该命令不安全。

  • ExecuteCommand:这个函数尝试执行一个传入的命令字符串,并返回命令的输出。在执行命令之前,它会先通过SafeCheck函数检查命令的安全性。

main函数

  • 程序首先检查命令行参数的数量。如果参数数量不正确(即不是2个,包括程序自身的名称和一个端口号),则显示用法信息并退出。

  • 然后,程序将第二个命令行参数(即端口号)转换为整数,并创建一个UdpServer对象。注意,这里使用了std::unique_ptr来管理UdpServer对象的生命周期,这是一种智能指针,可以在对象不再需要时自动删除它。

  • 接下来,程序调用UdpServer对象的Init方法来初始化服务器(例如,创建套接字并绑定到指定的端口)。

  • 最后,程序调用UdpServer对象的Start方法来启动服务器的主循环,等待并处理传入的UDP数据包。

#include "Comm.hpp"
#include "UdpServer.hpp"
#include <cstdio>
#include <memory>
#include <vector>

void Usage(std::string proc)
{
  std::cout << "Usage : \n\t" << proc << " local_port\n"
            << std::endl;
}

std::vector<std::string> black_words = {
    "rm",
    "unlink",
    "cp",
    "mv",
    "chmod",
    "exit",
    "reboot",
    "halt",
    "shutdown",
    "top",
    "kill",
    "dd",
    "vim",
    "vi",
    "nano",
    "man"};

std::string OnMessageDefault(std::string request)
{
  return request + "[haha, got you!!]";
}

bool SafeCheck(std::string command)
{
  for (auto &k : black_words)
  {
    std::size_t pos = command.find(k);
    if (pos != std::string::npos)
      return false;
  }

  return true;
}

// ls -a -l/ rm / tocuh
std::string ExecuteCommand(std::string command)
{
  if (!SafeCheck(command))
    return "bad man!!";

  std::cout << "get a message: " << command << std::endl;
  FILE *fp = popen(command.c_str(), "r");
  if (fp == nullptr)
  {
    return "execute error, reason is unknown";
  }

  std::string response;
  char buffer[1024];
  while (true)
  {
    char *s = fgets(buffer, sizeof(buffer), fp);
    if (!s)
      break;
    else
      response += buffer;
  }
  pclose(fp);
  return response.empty() ? "success" : response;
}

// ./udp_server 8888
int main(int argc, char *argv[])
{
  if (argc != 2)
  {
    Usage(argv[0]);
    return Usage_Err;
  }
  std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
  usvr->Init();
  usvr->Start();

  return 0;
}

客户端

实现了一个简单的UDP客户端,可以同时进行数据的发送和接收

包含的头文件

程序首先包含了一系列必要的头文件,这些头文件为网络通信、线程操作、输入输出等提供了所需的功能。

Usage 函数

如果用户提供的命令行参数数量不正确,则此函数会打印出正确的使用方法。

ThreadData

这个类封装了线程所需的数据,包括一个套接字文件描述符 _sockfd 和一个 InetAddr 对象 _serveraddr,该对象包含服务器的地址信息。

RecverRoutine 函数

这个函数是接收线程的主体函数。它在一个无限循环中不断地从套接字接收数据,并将数据打印到标准错误输出。如果接收失败,则跳出循环。

SenderRoutine 函数

这个函数是发送线程的主体函数。它也在一个无限循环中运行,提示用户输入要发送的数据,并将数据发送到服务器。如果发送失败,则打印错误消息。

main 函数

  1. 参数检查:首先检查命令行参数的数量。如果参数数量不正确,则打印使用方法并退出程序。

  2. 创建套接字:使用 socket 函数创建一个UDP套接字。

  3. 设置服务器地址:使用用户提供的IP地址和端口号设置服务器的地址结构。

  4. 创建并启动线程
    • 创建一个 ThreadData 对象,包含套接字和服务器地址信息。

    • 创建两个线程:一个用于接收数据(recver),另一个用于发送数据(sender)。

    • 启动这两个线程。

  5. 等待线程结束:使用 Join 方法等待两个线程都执行完毕。

  6. 关闭套接字:在所有操作完成后,关闭套接字。

  #include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"

void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}

class ThreadData
{
public:
    ThreadData(int sock, struct sockaddr_in &server) : _sockfd(sock), _serveraddr(server)
    {
    }
    ~ThreadData()
    {
    }

public:
    int _sockfd;
    InetAddr _serveraddr;
};

void RecverRoutine(ThreadData &td)
{
    char buffer[4096];
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t n = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len); // 一般建议都是要填的.
        if (n > 0)
        {
            buffer[n] = 0;
            std::cerr << buffer << std::endl; // 方便一会查看效果
        }
        else
            break;
    }
}

// 该线程只负责发消息
void SenderRoutine(ThreadData &td)
{
    while (true)
    {
        // 我们要发的数据
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);

        auto server = td._serveraddr.GetAddr();
        // 我们要发给谁呀?server
        ssize_t n = sendto(td._sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if (n <= 0)
            std::cout << "send error" << std::endl;
    }
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    // udp是全双工的。既可以读,也可以写,可以同时读写,不会多线程读写的问题
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 2;
    }
    std::cout << "create socket success: " << sock << std::endl;

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    ThreadData td(sock, server);
    Thread<ThreadData> recver("recver", RecverRoutine, td);
    Thread<ThreadData> sender("sender", SenderRoutine, td);
    recver.Start();
    sender.Start();

    recver.Join();
    sender.Join();

    close(sock);
    return 0;
}

依赖类

错误类型

#pragma once

#include<iostream>

enum{
  Usage_Err = 1,
  Socket_Err,
  Bind_Err
};

日志类

#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

enum
{
  Debug = 0,
  Info,
  Warning,
  Error,
  Fatal
};

enum
{
  Screen = 10,
  OneFile,
  ClassFile
};

std::string LevelToString(int level)
{
  switch (level)
  {
  case Debug:
    return "Debug";
  case Info:
    return "Info";
  case Warning:
    return "Warning";
  case Error:
    return "Error";
  case Fatal:
    return "Fatal";
  default:
    return "Unknown";
  }
}

const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";

class Log
{
public:
  Log() : style(defaultstyle), filename(default_filename)
  {
    mkdir(logdir.c_str(), 0775);
  }
  void Enable(int sty) //
  {
    style = sty;
  }
  std::string TimeStampExLocalTime()
  {
    time_t currtime = time(nullptr);
    struct tm *curr = localtime(&currtime);
    char time_buffer[128];
    snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
             curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
             curr->tm_hour, curr->tm_min, curr->tm_sec);
    return time_buffer;
  }
  void WriteLogToOneFile(const std::string &logname, const std::string &message)
  {
    umask(0);
    int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
    if (fd < 0)
      return;
    write(fd, message.c_str(), message.size());
    close(fd);
  }
  void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
  {
    std::string logname = logdir;
    logname += "/";
    logname += filename;
    logname += levelstr;
    WriteLogToOneFile(logname, message);
  }

  void WriteLog(const std::string &levelstr, const std::string &message)
  {
    switch (style)
    {
    case Screen:
      std::cout << message;
      break;
    case OneFile:
      WriteLogToClassFile("all", message);
      break;
    case ClassFile:
      WriteLogToClassFile(levelstr, message);
      break;
    default:
      break;
    }
  }
  void LogMessage(int level, const char *format, ...) // 类C的一个日志接口
  {
    char leftbuffer[1024];
    std::string levelstr = LevelToString(level);
    std::string currtime = TimeStampExLocalTime();
    std::string idstr = std::to_string(getpid());

    char rightbuffer[1024];
    va_list args; // char *, void *
    va_start(args, format);
    // args 指向了可变参数部分
    vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
    va_end(args); // args = nullptr;
    snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
             levelstr.c_str(), currtime.c_str(), idstr.c_str());

    std::string loginfo = leftbuffer;
    loginfo += rightbuffer;
    WriteLog(levelstr, loginfo);
  }

  ~Log() {}

private:
  int style;
  std::string filename;
};

Log lg;

class Conf
{
public:
  Conf()
  {
    lg.Enable(ClassFile);
  }
  ~Conf()
  {
  }
};

Conf conf;

防止拷贝类

#pragma once

#include<iostream>
class NoCopy
{
  public:
  NoCopy()
  {}
  NoCopy(const NoCopy&) = delete;
  NoCopy& operator=(const NoCopy&) = delete;
  ~NoCopy()
  {}
};

线程池

#pragma once

#include <iostream>
#include <queue>
#include <vector>
#include <pthread.h>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"

static const int defaultnum = 10;

class ThreadData
{
public:
    ThreadData(const std::string &name) : threadname(name)
    {
    }
    ~ThreadData()
    {
    }

public:
    std::string threadname;
};

template <class T>
class ThreadPool
{
private:
    ThreadPool(int thread_num = defaultnum) : _thread_num(thread_num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 构建指定个数的线程
        for (int i = 0; i < _thread_num; i++)
        {
            // 待优化
            std::string threadname = "thread-";
            threadname += std::to_string(i + 1);

            ThreadData td(threadname);

            // Thread<ThreadData> t(threadname,
            //                      std::bind(&ThreadPool<T>::ThreadRun, this, std::placeholders::_1), td);
            // _threads.push_back(t);
            _threads.emplace_back(threadname,
                                  std::bind(&ThreadPool<T>::ThreadRun, this,
                                            std::placeholders::_1),
                                  td);
            lg.LogMessage(Info, "%s is created...\n", threadname.c_str());
        }
    }
    ThreadPool(const ThreadPool<T> &tp) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T>) = delete;

public:
    // 有线程安全问题的
    static ThreadPool<T> *GetInstance()
    {
        if (instance == nullptr)
        {
            LockGuard lockguard(&sig_lock);
            if (instance == nullptr)
            {
                lg.LogMessage(Info, "创建单例成功...\n");
                instance = new ThreadPool<T>();
            }
        }

        return instance;
    }
    bool Start()
    {
        // 启动
        for (auto &thread : _threads)
        {
            thread.Start();
            lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str());
        }

        return true;
    }
    void ThreadWait(const ThreadData &td)
    {
        lg.LogMessage(Debug, "no task, %s is sleeping...\n", td.threadname.c_str());
        pthread_cond_wait(&_cond, &_mutex);
    }
    void ThreadWakeup()
    {
        pthread_cond_signal(&_cond);
    }
    void ThreadRun(ThreadData &td)
    {
        while (true)
        {
            // checkSelf()
            // checkSelf();
            // 取任务
            T t;
            {
                LockGuard lockguard(&_mutex);
                while (_q.empty())
                {
                    ThreadWait(td);
                    lg.LogMessage(Debug, "thread %s is wakeup\n", td.threadname.c_str());
                }
                t = _q.front();
                _q.pop();
            }
            // 处理任务
            t();
            // lg.LogMessage(Debug, "%s handler task %s done, result is : %s\n",
            //               td.threadname, t.PrintTask().c_str(), t.PrintResult().c_str());
        }
    }
    void Push(T &in)
    {
        LockGuard lockguard(&_mutex);
        _q.push(in);
        ThreadWakeup();
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // for debug
    void Wait()
    {
        for (auto &thread : _threads)
        {
            thread.Join();
        }
    }

private:
    std::queue<T> _q;
    std::vector<Thread<ThreadData>> _threads;
    int _thread_num;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static ThreadPool<T> *instance;
    static pthread_mutex_t sig_lock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;

线程类

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>

// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T&)>;

template<class T>
class Thread
{
public:
    Thread(const std::string &threadname, func_t<T> func, T &data)
    :_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
    {}

    static void *ThreadRoutine(void *args) // 类内方法,
    {
        // (void)args; // 仅仅是为了防止编译器有告警
        Thread *ts = static_cast<Thread *>(args);

        ts->_func(ts->_data);

        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }
    bool Join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }
    std::string ThreadName()
    {
        return _threadname;
    }
    bool IsRunning()
    {
        return _isrunning;
    }
    ~Thread()
    {}
private:
    pthread_t _tid;
    std::string _threadname;
    bool _isrunning;
    func_t<T> _func;
    T _data;
};

网络信息类

#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr):_addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    std::string Ip() {return _ip;}
    uint16_t Port() {return _port;};
    std::string PrintDebug()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port);  // "127.0.0.1:4444"
        return info;
    }
    const  struct sockaddr_in& GetAddr()
    {
        return _addr;
    }
    bool operator == (const InetAddr&addr)
    {
        //other code
        return this->_ip == addr._ip && this->_port == addr._port;
    }
    ~InetAddr(){}
private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

守护锁类

#pragma once

#include <pthread.h>

// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock):_lock(lock)
    {}
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex()
    {}

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock): _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.Unlock();
    }
private:
    Mutex _mutex;
};

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-30 08:28:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-30 08:28:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-30 08:28:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-30 08:28:02       20 阅读

热门阅读

  1. 给vue配置路径别名@

    2024-04-30 08:28:02       13 阅读
  2. npm 安装vite

    2024-04-30 08:28:02       34 阅读
  3. Spring Boot项目中集成Logback作为日志框架-笔记

    2024-04-30 08:28:02       32 阅读
  4. 探索PyTorch:开源深度学习框架的魅力

    2024-04-30 08:28:02       13 阅读
  5. 深度学习面试总结(上岸版~)

    2024-04-30 08:28:02       15 阅读
  6. Python深度学习实践:使用TensorFlow构建图像分类器

    2024-04-30 08:28:02       13 阅读
  7. CSS 如何在长方形中间加一条横线

    2024-04-30 08:28:02       22 阅读