『 Linux 』利用UDP套接字实现简单群聊

news2024/11/14 18:25:12

文章目录

    • 服务端通过传入命令处理实现远程命令执行
    • 使用Windows编辑UDP客户端实现Windows远程控制Linux
    • 接收套接字的其他信息
    • UDP套接字简单群聊服务端
    • UDP套接字简单群聊客户端
      • 运行测试及分离输入输出
    • 参考代码


服务端通过传入命令处理实现远程命令执行

请添加图片描述

『 Linux 』利用UDP套接字简单进行网络通信 中实现了利用UDP套接字实现简单的网络通信(参考代码[gitee - udp process]);

具体思路是实现利用UDP套接字实现一个客户端和一个服务端,客户端向服务端发送数据,服务端进行数据的拼接并返回拼接后的数据,或者是传入一个回调函数,服务端通过调用回调函数调用popen接口创建子进程执行命令;

std::string HandlerCommand(const std::string& cmd) {
  // 打开管道,执行命令
  FILE* fp = popen(cmd.c_str(), "r");
  if (!fp) {
    perror("popen");
    return "error";
  }

  std::string ret;
  char buffer[4096];

  // 循环读取命令输出
  while (true) {
    char* res = fgets(buffer, sizeof(buffer), fp);
    if (res == nullptr) break;   // 到达文件末尾,或出错
    ret += std::string(buffer);  // 将命令输出追加到返回字符串中
  }

  // 关闭管道,并获取命令执行的返回值
  int status = pclose(fp);
  if (status == -1) {
    perror("pclose");
    return "error";
  }

  // 返回命令执行结果
  return ret;
}

可以创建一个子函数使用string::find判断命令中是否存在不安全的操作,如rm,sudo等;

bool isSave(const std::string& cmd) {
  std::vector<std::string> unsaves = {"rm", "while", "sudo",
                                            "mv", "cp",    "yum"};
  int pos = 0;
  for (auto& world : unsaves) {
    pos = cmd.find(world);
    if (pos != std::string::npos) {
      return false;
    }
  }
  return true;
}

std::string HandlerCommand(const std::string& cmd) {
  // 打开管道,执行命令
  if (!isSave(cmd)) {
    return "the cmd unsave";
  }
  FILE* fp = popen(cmd.c_str(), "r");
  if (!fp) {
    perror("popen");
    return "error";
  }

  std::string ret;
  char buffer[4096];

  // 循环读取命令输出
  while (true) {
    char* res = fgets(buffer, sizeof(buffer), fp);
    if (res == nullptr) break;   // 到达文件末尾,或出错
    ret += std::string(buffer);  // 将命令输出追加到返回字符串中
  }

  // 关闭管道,并获取命令执行的返回值
  int status = pclose(fp);
  if (status == -1) {
    perror("pclose");
    return "error";
  }

  // 返回命令执行结果
  return ret;
}

测试结果为:

这里的服务端地址在测试时可以使用环回地址,即127.0.0.1127.0.0.0;

  • 环回地址

    环回地址是一个特殊的IP地址,用于主机自己与自己通信,不与任何物理网络接口相关联;

    通常用于进行CS测试,即客户端与服务端之间的测试;

    IPv4中的环回地址通常为127.0.0.0/8网段;

    IPv6中的环回地址通常为::1;

    主机名local host通常会被映射到环回地址,环回地址通常是安全的,因为数据不会离开主机;

本质上Xshell等软件就是通过类似的原理实现本地与远端服务器进行网络通信(所用协议不同);


使用Windows编辑UDP客户端实现Windows远程控制Linux

请添加图片描述

服务端已经实现,此处不考虑服务端;

#define _CRT_SECURE_NO_WARNINGS 1 // 使用VS编译器预防警告

#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
#include <string>

#pragma comment(lib, "ws2_32.lib")

enum ERR {
	SOCKETERR = 1,
	INETPTONERR
};

// 封装客户端
class UdpClient { 
public:
	UdpClient(const std::string&ip,UINT16 port)
		:sockfd_(0),ip_(ip),port_(port),len_(0) // 初始化客户端的值
	{
		memset(&local_, 0, sizeof(local_)); // 初始化struct stockaddr结构体为0

		if (WSAStartup(MAKEWORD(2, 2), &wsd_)) {} // 取消警告 
	}
	
	void Run() {
		/*
			进行套接字的运行与数据的发送接收及管理
		*/
		Init(); // 进行套接字的初始化


		std::string message; // 用于存储需要发送给服务端的字符串数据
		char buffer[1024] = { 0 }; // 用于存储服务端返回给客户端的内容
		while (true) {
			std::cout << "Please Enter@ "; // 消息提示符
			std::getline(std::cin,message); // 从键盘中接收需要发送给服务端的数据

			int sd = sendto(sockfd_, message.c_str(), message.size(), 0, (sockaddr*)&local_, len_); // 调用sendto发送给服务端
			if (sd < 0) {
				std::cout << "sendto err " << std::endl;
			}

			struct sockaddr_in tmp; // 创建 sockaddr_in 作为服务端发来数据包的其他信息(IP,端口等)
			memset(&tmp,0, sizeof(tmp)); // 初始化结构体
			socklen_t len = sizeof(tmp); // 作为输出型参数接收服务端发来数据包的大小

			SSIZE_T n = recvfrom(sockfd_, buffer, 1023, 0, (struct sockaddr*)&tmp, &len); // 使用 recvfrom 接收来自服务端的信息
			if (n > 0) {
				buffer[n] = 0;
				std::cout << buffer << std::endl; // 当做字符串进行打印
			}
		}
	}

	~UdpClient() {
		WSACleanup();
		closesocket(sockfd_); // 关闭套接字文件描述符
	}
protected:

	void Init() {
		/*
			进行套接字的初始化工作
		*/

		// 设置 sockaddr_in 结构体
		local_.sin_family = AF_INET; // 设置网络传输为IPv4协议
		local_.sin_port = htons(port_); // 设置端口号 - 需要发送给服务端需要转网络字节序
		if (inet_pton(AF_INET, ip_.c_str(), &local_.sin_addr) != 1) {
			std::cerr << "Init - sin_addr erro" << std::endl;
			exit(INETPTONERR);
		}
		len_ = sizeof(local_); // 计算 sockaddr_in 结构体大小


	   // 创建套接字
		sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字并存储套接字文件描述符
		if (sockfd_ < 0) {
			std::cerr << "soket fail" << std::endl;
			exit(ERR::SOCKETERR);
		}

	}

private:
	SOCKET sockfd_;
	std::string ip_;
	UINT16 port_;
	struct sockaddr_in local_;
	WSADATA wsd_;
	socklen_t len_;
};

int main() {
	std::string ip; 
	UINT16 port;

	// 打印提示符信息并输入IP与端口
	std::cout << "Please Enter your IP@ ";
	std::cin >> ip;
	std::cout << "Please Enter your port@ ";
	std::cin >> port;

	// 实例化一个客户端实例并传入IP与端口
	UdpClient client(ip, port);

	// 调用客户端的运行(运行中自动进行初始化)
	client.Run();

	return 0;
}

这里封装了一个Windows版本的Client客户端;

  • 头文件和预处理指令

    包含了必要的头文件,如WinSock2.h用于网络编程;

    #define _CRT_SECURE_NO_WARNINGS 1用于禁用某些Visual Studio的安全警告;

    #pragma comment(lib, "ws2_32.lib") 链接 Windows Socket 网络库;

  • 错误枚举

    enum ERR {
        SOCKETERR = 1,
        INETPTONERR
    };
    

    定义了两种错误类型,用于错误处理;

  • UdpClient

    主要的客户端类,封装了UDP客户端的功能;

    • 构造函数

      UdpClient(const std::string&ip,UINT16 port)
          :sockfd_(0),ip_(ip),port_(port),len_(0)
      

      用于初始化客户端,设置IP地址和端口号,调用了WSAStartup初始化Winsock;

    • Run方法

      void Run()
      

      客户端的主要运行方法,首先调用Init函数进行初始化,然后进入一个无限循环,不断从用户获取输入并发送到服务器,并接收服务器的响应;

    • 析构函数

      ~UdpClient()
      

      清理资源,关闭套接字文件描述符,调用WSACleanup;

    • Init函数

      void Init()
      

      初始化套接字和地址结构;

      设置sockadddr_in结构并创建UDP套接字;

      客户端不需要显式bind,当调用sendto将数据发送给服务端时将自动生成一个port端口bind当前IP;

    • 私有成员

      private:
      	SOCKET sockfd_;  // 套接字文件描述符
      	std::string ip_; // IP
      	UINT16 port_; // 端口号
      	struct sockaddr_in local_; // sockaddr_in 结构体
      	WSADATA wsd_; // WSADATA 变量
      	socklen_t len_; // sockaddr_in 结构体的大小
      

      套接字描述符,IP地址,端口号,地址族结构体等;

    • main函数

      从用户获取IP地址和端口号;

      创建UdpClient实例;

      调用client.Run()开始运行客户端;

主要流程为用户输入服务器IP和端口并创建UdpClient对象;

调用Run函数:

  • 初始化套接字(调用Init);
  • 进入循环:
    • 获取用户输入
    • 发送到服务器
    • 接收服务器响应并显示

由于协议分层,WindowsLinux不同平台可以使用同一套协议簇进行网络通信;

因为即使是不同平台下Socket API网络接口是一致的;


接收套接字的其他信息

请添加图片描述

当服务端接收到客户端所发的信息时这个数据包中存放的除了数据以外还包含着客户端的基本信息,如IP地址和端口号;

/* UdpServer.hpp */
#include "log.hpp"  // 日志头文件

#define BUF_SIZE 1024

// 定义函数类型别名
using Comfunc_t = std::function<std::string(const std::string &)>;
using Echofunc_t = std::function<std::string(const std::string &,
                                             const std::string &, uint16_t)>;

Log log_;

// 错误码枚举
enum { SOCK_CREATE_FAIL = 1, SOCK_BIND_FAIL };

class UdpServer {
 public:
  // 构造函数
  UdpServer(const uint16_t port = defaultport)
      : sockfd_(0), port_(port), isrunning_(false) {}

  ~UdpServer() {}

  // 初始化服务器
  void Init() {
    // 创建 UDP socket
    sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd_ < 0) {
      log_(FATAL, "socket create fail , the errornum : %d\n", sockfd_);
      exit(SOCK_CREATE_FAIL);
    }
    log_(INFO, "socket create sucess , sockfd : %d", sockfd_);

    // 绑定地址和端口
    struct sockaddr_in localsr;
    bzero(&localsr, sizeof(localsr));
    localsr.sin_family = AF_INET;
    localsr.sin_port = htons(port_);
    localsr.sin_addr.s_addr = INADDR_ANY;
    socklen_t locallen = sizeof(localsr);

    if (bind(sockfd_, (const struct sockaddr *)&localsr, locallen) < 0) {
      log_(FATAL, "socket bind fail, err string :%s", strerror(errno));
      exit(SOCK_BIND_FAIL);
    }
    log_(INFO, "socket bind sucess , sockfd : %d", sockfd_);
  }

  // 运行服务器
  void Run(Echofunc_t EchoHandler) {
    isrunning_ = true;
    char inbuf[BUF_SIZE] = {0};
    while (isrunning_) {
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      bzero(&client, sizeof(client));

      // 接收客户端消息
      size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
                          (struct sockaddr *)&client, &len);

      if (n < 0) {
        log_(WARNING, "recvfrom fail, err string :%s", strerror(errno));
        continue;
      }
      log_(INFO, "recvfrom sucess");

      uint16_t port = ntohs(client.sin_port); // 接收客户端数据并提取对应的IP和port将其序列化(网络字节序转主机字节序)
      std::string ip = inet_ntoa(client.sin_addr);

      inbuf[n] = 0;

      // 处理数据
      std::string info = inbuf;
      std::string echo_string = EchoHandler(inbuf, ip, port);
      std::cout << echo_string << std::endl;

      // 发送回复
      sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0,
             (const struct sockaddr *)&client, len);
    }
  }

 private:
  int sockfd_;     // 套接字文件描述符
  uint16_t port_;  // 端口号
  bool isrunning_; // 运行状态

  static const uint16_t defaultport; // 默认端口
};

// 设置默认端口
const uint16_t UdpServer::defaultport = 8080;

#endif

其中下面这段代码为接收到客户端所发的数据并提取对应的IP地址和端口号将其进行序列化,即网络字节序转主机字节序;

      uint16_t port = ntohs(client.sin_port); // 接收客户端数据并提取对应的IP和port将其序列化(网络字节序转主机字节序)
      std::string ip = inet_ntoa(client.sin_addr);

UDP套接字简单群聊服务端

请添加图片描述

/* UdpServer.hpp */
#ifndef UDPSERVER_HPP
#define UDPSERVER_HPP

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <cstring>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>

#include "log.hpp"

#define BUF_SIZE 1024

// 定义回调函数类型,用于处理接收到的消息
using Echofunc_t = std::function<std::string(const std::string &,
                                             const std::string &, uint16_t)>;

Log log_;

// 错误枚举,用于标识不同的错误类型
enum { SOCK_CREATE_FAIL = 1, SOCK_BIND_FAIL };

class UdpServer {
 public:
  // 构造函数,初始化服务器参数
  UdpServer(const uint16_t port = defaultport)
      : sockfd_(0), port_(port), isrunning_(false) {}

  // 析构函数
  ~UdpServer() {}

  // 初始化服务器,创建并绑定socket
  void Init() {
    // 创建UDP socket
    sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd_ < 0) {
      log_(FATAL, "socket create fail , the errornum : %d\n", sockfd_);
      exit(SOCK_CREATE_FAIL);
    }
    log_(INFO, "socket create sucess , sockfd : %d", sockfd_);

    // 设置服务器地址结构
    struct sockaddr_in localsr;
    bzero(&localsr, sizeof(localsr));
    localsr.sin_family = AF_INET;
    localsr.sin_port = htons(port_);
    localsr.sin_addr.s_addr = INADDR_ANY;
    socklen_t locallen = sizeof(localsr);

    // 绑定socket到指定地址和端口
    if (bind(sockfd_, (const struct sockaddr *)&localsr, locallen) < 0) {
      log_(FATAL, "socket bind fail, err string :%s", strerror(errno));
      exit(SOCK_BIND_FAIL);
    }
    log_(INFO, "socket bind sucess , sockfd : %d", sockfd_);
  }

  // 检查用户是否已存在,如果是新用户则添加到在线用户列表
  void CheckUsr(const struct sockaddr_in &client) {
    uint16_t port = ntohs(client.sin_port);
    std::string ip = inet_ntoa(client.sin_addr);

    // 检查用户是否首次登录
    auto it = online_user_.find(ip);
    if (it == online_user_.end()) {
      online_user_[ip] = client;
      std::cout << "The " << ip << " first login..." << std::endl;
    }
  }

  // 向所有在线用户广播消息
  void Broadcast(const std::string &info, const std::string &ip,
                 uint16_t port) {
    for (const auto &usr : online_user_) {
      // 构造广播消息
      std::string massage =
          "[" + ip + ":" + std::to_string(port) + " echo]# " + info;
      socklen_t len = sizeof(usr.second);
      // 发送消息给每个在线用户
      sendto(sockfd_, massage.c_str(), massage.size(), 0,
             (struct sockaddr *)(&usr.second), len);
    }
  }

  // 运行服务器,处理接收到的消息
  void Run(Echofunc_t EchoHandler) {
    isrunning_ = true;
    char inbuf[BUF_SIZE] = {0};
    while (isrunning_) {
      // 接收客户端数据
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      bzero(&client, sizeof(client));

      size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
                          (struct sockaddr *)&client, &len);

      if (n < 0) {
        log_(WARNING, "recvfrom fail, err string :%s", strerror(errno));
        continue;
      }

      // 解析客户端信息
      uint16_t port = ntohs(client.sin_port);
      std::string ip = inet_ntoa(client.sin_addr);
      inbuf[n] = 0;  // 确保字符串以null结尾

      // 检查并更新用户状态
      CheckUsr(client);
      std::string info = inbuf;

      // 广播接收到的消息
      Broadcast(info, ip, port);
    }
  }

 private:
  int sockfd_;                // socket文件描述符
  uint16_t port_;             // 服务器端口
  bool isrunning_;            // 服务器运行状态标志

  static const uint16_t defaultport;  // 默认端口
  std::unordered_map<std::string, struct sockaddr_in> online_user_;  // 在线用户列表
};

const uint16_t UdpServer::defaultport = 8080;  // 设置默认端口为8080

#endif

这段代码包含了必要的系统和标准库头文件;

  • 构造函数

      // 构造函数,初始化服务器参数
      UdpServer(const uint16_t port = defaultport)
          : sockfd_(0), port_(port), isrunning_(false) {}
    

    用于初始化服务器的基本参数,包括套接字文件描述符,端口号,运行标识符;

  • Init函数

    void Init()
    
    • 创建UDP socket

      sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
      

      创建一个UDP socket套接字,AF_INET表示使用IPv4,SOCK_DGRAM表示使用UDP协议,0表示默认协议;

      如果创建失败记录错误并退出程序;

    • 设置服务器地址结构

        // 设置服务器地址结构
          struct sockaddr_in localsr;
          bzero(&localsr, sizeof(localsr));
          localsr.sin_family = AF_INET;
          localsr.sin_port = htons(port_);
          localsr.sin_addr.s_addr = INADDR_ANY;
          socklen_t locallen = sizeof(localsr);
      

      创建sockadd_in结构体localsr并调用bzero清空结构体;

      设置地址族为IPv4,设置端口并转网络字节序,同样的设置IP地址并转网络字节序;

    • 绑定socket

       if (bind(sockfd_, (const struct sockaddr *)&localsr, locallen) < 0) {
              log_(FATAL, "socket bind fail, err string :%s", strerror(errno));
              exit(SOCK_BIND_FAIL);
          }
          log_(INFO, "socket bind sucess , sockfd : %d", sockfd_);
      

      socket绑定到指定的端口和地址,绑定成功或失败都会使用日志插件打印对应信息;

  • Run函数

    void Run(Echofunc_t EchoHandler);
    

    该函数用于运行服务端,其中EchoHandler是一个回调函数,用于处理接收到的数据;

    • 初始化

          isrunning_ = true;
          char inbuf[BUF_SIZE] = {0};
      

      将运行状态设置为true,初始化inbuf数组用于接收数据;

    • 主循环

      while (isrunning_){/* ... */}
      

      服务端会持续运行直至isrunning_被设置为false;

    • 接收数据

      // 接收客户端数据
              struct sockaddr_in client;
              socklen_t len = sizeof(client);
              bzero(&client, sizeof(client));
      
              size_t n = recvfrom(sockfd_, inbuf, sizeof(inbuf) - 1, 0,
                                  (struct sockaddr *)&client, &len);
      

      创建一个sockaddr_in结构体用来存储客户端地址信息,并调用bzro清空客户端地址结构;

      socklen_t len设置地址结构的长度;

      使用recvfrom函数接收数据,并返回接收到的字节数给size_t n;

    • 错误处理

       if (n < 0) {
                  log_(WARNING, "recvfrom fail, err string :%s", strerror(errno));
                  continue;
              }
      

      如果recvfrom返回负值说明调用失败,记录错误并进行下一次循环;

    • 处理接收到的数据

        // 解析客户端信息
              uint16_t port = ntohs(client.sin_port);
              std::string ip = inet_ntoa(client.sin_addr);
              inbuf[n] = 0;  // 确保字符串以null结尾
      

      解析客户端的信息,包括IP,端口号等等;

      如果n>0说明数据接收成功,该值即为接收的长度,将数组对应的位置赋值0(即\0)将该数组看成是字符串;

    • 检查更新用户状态

             // 检查并更新用户状态
              CheckUsr(client);
      

      调用CheckUsr函数检查用户状态并进行更新;

    • 广播消息

              std::string info = inbuf;
      
              // 广播接收到的消息
              Broadcast(info, ip, port);
      

      将接收到的消息调用Broadcast函数广播接收到的消息;

  • CheckUsr检查更新用户函数

    void CheckUsr(const struct sockaddr_in &client);
    

    参数client表示传入一个地址结构,该地址结构保存客户端的信息;

    • 解析客户端信息

      	uint16_t port = ntohs(client.sin_port);
          std::string ip = inet_ntoa(client.sin_addr);
      

      解析客户端信息,包括IP地址与端口号,将网络字节序转化为主机字节序;

    • 检查用户是否存在

      	auto it = online_user_.find(ip);
          if (it == online_user_.end()) {
              online_user_[ip] = client;
              std::cout << "The " << ip << " first login..." << std::endl;
          }
      

      该类中的unordered_map<std::string, struct sockaddr_in> online_user_存放着用户的信息,检查该哈希表中是否存在该用户的信息,有则无行为,无则添加;

  • Broadcast广播消息函数

    该函数用于遍历哈希表,将消息广播回当前在线的所有用户;

    • 遍历哈希表(在线用户)

      for (const auto &usr : online_user_){ /* ... */ }
      

      遍历所有在线用户;

    • 构造广播消息

      // 构造广播消息
              std::string massage =
                  "[" + ip + ":" + std::to_string(port) + " echo]# " + info;
      

      创建一个包含发送者IP,端口号和原始消息的格式化字符串;

    • 发送消息

      		socklen_t len = sizeof(usr.second);
              // 发送消息给每个在线用户
              sendto(sockfd_, massage.c_str(), massage.size(), 0,
                     (struct sockaddr *)(&usr.second), len);
      

      对每个在线的用户调用sendto函数发送构造的消息,其中sockfd_是服务器的socket;

      massage.c_str()massage.size()用于提供消息内容和长度;

      (struct sockaddr *)(&usr.second)为目标用户的地址;

/* Main.cc */
#include <iostream>
#include <vector>
#include "UdpServer.hpp"

// 打印使用说明
void Usage(std::string proc) {
  std::cout << "\n\tUsage: " << proc << " port[1024+]\n" << std::endl;
}

// 回声处理函数
std::string EchoHandler(const std::string& datastr, const std::string& clientip, uint16_t clientport) {
  std::string echo_string = "[" + clientip + ":" + std::to_string(clientport) + " echo]# " + datastr;
  return echo_string;
}

int main(int argc, char* argv[]) {
  if (argc != 2) {
    Usage(argv[0]);
    exit(0);
  }

  // 创建并初始化UDP服务器
  std::unique_ptr<UdpServer> svr(new UdpServer(std::stoi(argv[1])));
  svr->Init();
  
  // 运行服务器
  svr->Run(EchoHandler);
  return 0;
}
  • EchoHandler函数

    std::string EchoHandler(const std::string& datastr, const std::string& clientip, uint16_t clientport) {
      std::string echo_string = "[" + clientip + ":" + std::to_string(clientport) + " echo]# " + datastr;
      return echo_string;
    }
    

    这是一个回声处理函数,用于接收客户端发送的数据,客户端IP和端口号,然后构造一个包含这些信息的字符串并返回;

  • main函数

      // 创建并初始化UDP服务器
      std::unique_ptr<UdpServer> svr(new UdpServer(std::stoi(argv[1])));
      svr->Init();
      
      // 运行服务器
      svr->Run(EchoHandler);
    

    主函数创建并初始化UDP服务器随后运行;


UDP套接字简单群聊客户端

请添加图片描述

/* UdpClient.hpp */
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <cstring>
#include <iostream>
#include <memory>
#include <string>

// 线程数据结构,用于在线程间传递必要的信息
struct ThreadData {
  sockaddr_in local;  // 服务器地址信息
  int sockfd;         // 套接字文件描述符
};

// 打印使用说明
void Usage(std::string proc) {
  std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

// 接收消息的线程函数
void* recv_message(void* data) {
  ThreadData* td = (ThreadData*)data;
  char buffer[1024] = {0};  // 接收缓冲区

  while (true) {
    struct sockaddr_in temp;
    bzero(&temp, sizeof(temp));
    socklen_t len = sizeof(temp);
    // 接收来自服务器的消息
    ssize_t n =
        recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
    if (n > 0) {
      buffer[n] = 0;                     // 确保字符串正确终止
      std::cout << buffer << std::endl;  // 打印接收到的消息
    }
  }
}

// 发送消息的线程函数
void* send_message(void* data) {
  ThreadData* td = (ThreadData*)data;
  std::string message;

  while (true) {
    std::cout << "Please Enter@";
    getline(std::cin, message);  // 获取用户输入
    socklen_t len = sizeof(td->local);
    // 发送消息到服务器
    int sdebug = sendto(td->sockfd, message.c_str(), message.size(), 0,
                        (struct sockaddr*)&td->local, len);
    if (sdebug < 0) {
      std::cout << "sendto fail, err: " << strerror(errno) << std::endl;
    }
  }
}

int main(int argc, char* argv[]) {
  // 检查命令行参数
  if (argc != 3) {
    Usage(argv[0]);
    exit(0);
  }

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

  // 初始化服务器地址结构
  bzero(&tdata.local, sizeof(tdata.local));
  tdata.local.sin_family = AF_INET;
  tdata.local.sin_port = htons(serverport);
  tdata.local.sin_addr.s_addr = inet_addr(serverip.c_str());

  // 创建UDP套接字
  tdata.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (tdata.sockfd < 0) {
    std::cout << "socket fail" << std::endl;
    exit(-1);
  }

  // 创建接收和发送线程
  pthread_t recver, sender;
  pthread_create(&recver, nullptr, recv_message, &tdata);
  pthread_create(&sender, nullptr, send_message, &tdata);

  // 等待线程结束(实际上这里的线程不会结束)
  pthread_join(recver, nullptr);
  pthread_join(sender, nullptr);

  // 关闭套接字(实际上这行代码永远不会被执行)
  close(tdata.sockfd);
  return 0;
}

这个客户端使用了双线程,即一个线程用于接收数据,一个线程用于发送数据以避免getline函数阻塞线程导致的无法正确接收消息;

  • ThreadData结构体

    定义了再线程间共享的数据结构,包含了服务器的地址信息和套接字描述符;

    // 线程数据结构,用于在线程间传递必要的信息
    struct ThreadData {
      sockaddr_in local;  // 服务器地址信息
      int sockfd;         // 套接字文件描述符
    };
    
  • recv_message函数

    该函数主要用于接收从服务端中转发的数据;

    // 接收消息的线程函数
    void* recv_message(void* data) {
      ThreadData* td = (ThreadData*)data;
      char buffer[1024] = {0};  // 接收缓冲区
    
      while (true) {
        struct sockaddr_in temp;
        bzero(&temp, sizeof(temp));
        socklen_t len = sizeof(temp);
        // 接收来自服务器的消息
        ssize_t n =
            recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if (n > 0) {
          buffer[n] = 0;                     // 确保字符串正确终止
          std::cout << buffer << std::endl;  // 打印接收到的消息
        }
      }
    }
    

    创建一个sockaddr_in结构体用于存储服务端的基本信息,而后调用recvfrom函数接收消息;

    接收消息后将消息末尾处添加\0作字符串并进行打印;

  • send_message函数

    该函数为发送消息的线程函数,即循环读取用户输入信息而后调用sendto函数将消息发送到服务器;

    • 循环读取用户输入信息

       while (true) {
          std::cout << "Please Enter@";
          getline(std::cin, message);  // 获取用户输入
          socklen_t len = sizeof(td->local);
       	// ...
       }
      
    • 调用sendto函数发送消息

      int sdebug = sendto(td->sockfd, message.c_str(), message.size(), 0,
                              (struct sockaddr*)&td->local, len);
          if (sdebug < 0) {
            std::cout << "sendto fail, err: " << strerror(errno) << std::endl;
          }
      

      调用函数后判断消息是否成功发出;

    • main函数

      main函数首先检查命令行参数判断是否需要调用Usage用户手册;

      初始化ThreadData结构体用于存储服务器对应信息;

      创建UDP套接字并创建两个线程,一个用于接收消息,一个用于发送消息;


运行测试及分离输入输出

请添加图片描述

运行测试结果显示UDP网络通信成功,但这里的输入输出混在在一起;

可以将服务端转发的数据重定向到另一个窗口以实现单终端输入单中端输出;

使用ls /dev/pts查看当前主机下多少Bash存在;

$ ls /dev/pts
0  1  2  3  ptmx

多个终端情况下使用echo "hello" > /dev/pts/[cmdnumber]检查哪个终端将接收到数据从而指定哪个终端作为输入哪个作为输出;

$ echo "hello world" > /dev/pts/1
hello world

根据对应的终端号使用open打开该文件并调用dup2重定向到对应的文件(终端)中实现输入输出分离;

std::string terminal = "/dev/pts/1";

int OpenTerminal() {
  int fd = open(terminal.c_str(), O_WRONLY);
  if (fd < 0) {
    std::cerr << "open terminal error" << std::endl;
    return 1;
  }
  dup2(fd, 2);
  return 0;
}

由于该处dup2的文件描述符为2(标准错误流);

故在客户端中接收到服务端发出的数据应使用std::cerr进行打印;

void* recv_message(void* data) {
   // 接收缓冲区 ...

  while (true) {
    // 接收来自服务器的消息 ...
      std::cerr << buffer << std::endl;  // 使用标准错误cerr打印接收到的消息
    }
  }
}

结果如下:


参考代码

请添加图片描述

[Gitee - 半介莽夫 / Dio夹心小面包]

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

led台灯对眼睛好不好?台灯太亮会影响视力吗?解锁护眼台灯小知识

中国的近视情况十分严峻&#xff0c;尤其在青少年群体中表现得更为突出。据统计&#xff0c;中国青少年的近视率高居世界第一&#xff0c;这主要与长时间的近距离用眼、过度使用电子产品以及户外活动时间减少等因素有关。优质的护眼台灯不仅能减少近视的发生率&#xff0c;还能…

创建一个简单的Vue3.0应用程序

1、Vue.createApp() 方法的介绍 每一个 Vue.js 的应用都需要创建一个应用程序的实例对象并挂载到指定 DOM 上。在 Vue3.0 中&#xff0c;创建一个应用程序实例的语法格式如下&#xff1a; Vue.createApp(App) createApp() 是一个全局 API&#xff0c;它接收一个根组件选项对…

基于SpringBoot+Vue疫情物资捐赠和分配系统--论文pf

TOC springboot518基于SpringBootVue疫情物资捐赠和分配系统--论文pf 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往…

开源一款H5自适应留言表白墙php源码下载

开源一款H5自适应留言表白墙php源码下载&#xff0c;优点就是安装简单&#xff0c;功能实用[滑稽][滑稽] 缺点就是UI简陋&#xff0c;功能稀少 第一张是首页&#xff0c;第二张是查看留言 第三张是留言列表(10秒自动刷新)&#xff0c;第四张是表白墙界面

小程序商城被盗刷,使用SCDN安全加速有用吗?

在电子商务蓬勃发展的今天&#xff0c;小程序商城因其便捷性和灵活性成为商家和消费者的新宠。然而&#xff0c;随着其普及&#xff0c;小程序商城的安全问题也日益凸显&#xff0c;尤其是盗刷现象频发&#xff0c;给商家和用户带来了巨大损失。面对这一挑战&#xff0c;是否可…

android13隐藏调节声音进度条下面的设置按钮

总纲 android13 rom 开发总纲说明 目录 1.前言 2.情况分析 3.代码修改 4.编译运行 5.彩蛋 1.前言 将下面的声音调节底下的三个点的设置按钮,隐藏掉。 效果如下 2.情况分析 查看布局文件 通过布局我们可以知道这个按钮就是 com.android.keyguard.AlphaOptimizedImageB…

火语言RPA流程组件介绍--变量赋值

变量赋值 &#x1f6a9;【组件功能】&#xff1a;对已定义的变量进行赋值操作。 支持对任意类型的变量进行赋值&#xff0c;赋值内容可以为字符串也可通过表达式进行计算运行后再赋值给指定变量。 配置预览 配置说明 变量名称 手动输入变量名称或点击输入框下拉选择已创建…

el-form的必填校验的星号*放在label的右边

1.el-form添加hide-required-asterisk <el-form :model"userInfoForm" label-width"80px" :inline"true" :rules"rules" ref"ruleForm"label-position"top"hide-required-asterisk>2.添加样式 .el-form-it…

美国政府紧急应对三星Galaxy手机安全漏洞

一、美国政府紧急通知更新三星Galaxy手机系统 美国政府近日发布紧急通知&#xff0c;要求联邦政府雇员在8月28日前更新三星Galaxy手机系统&#xff0c;否则将面临禁止使用这些设备的后果。这是继7月针对Pixel手机用户的类似要求之后的又一次紧急行动。此次事件的导火索是谷歌发…

Docker-制作镜像

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、操作系统的组成&#xff08;一&#xff09;bootfs&#xff08;二&#xff09;rootfs&#xff08;三&#xff09;Liunx操作系统的启动过程&#xff08;1&…

正确安装振弦式应变的步骤有哪些?

在工程结构监测与评估中&#xff0c;振弦式应变计广泛应用于桥梁、建筑、隧道、大坝等各类结构的健康监测中。它能够实时、准确地反映结构在承受荷载作用下的应变状态&#xff0c;为工程师们提供关键的数据支持&#xff0c;以便及时发现并处理潜在的结构问题。然而&#xff0c;…

运维学习————Redis在Linux(Centos7)单机部署和集群部署

目录 一、单机部署 1、软件准备 2、安装配置 3、启动Redis 二、Redis集群 2.1、主从模式 2.1.1、作用 2.1.2、规划图 2.1.3、具体配置 准备工作 主从配置 启动测试 2.1.4、主从复制原理 主从全量复制 主从增量同步(slave重启或后期数据变化) 2.1.5、缺点 2.2、哨兵…

Verilog刷题笔记52

题目&#xff1a; Fsm serial In many (older) serial communications protocols, each data byte is sent along with a start bit and a stop bit, to help the receiver delimit bytes from the stream of bits. One common scheme is to use one start bit (0), 8 data bit…

如何打包CST仿真结果【电磁仿真基础教程】

本期介绍一个小的技巧关于打包仿真结果。很多时候我们需要把仿真的结果传递给同事&#xff0c;领导或者导师等看。如果把整个仿真文件夹压缩&#xff0c;很不方便。其实CST软件自带Archive功能能很方便的打包你想要的部分结果。 比如我们仿真完一个喇叭天线&#xff0c;结果中带…

ubuntu-linux ifconfig只有回环IP问题解决

问题如下图所示&#xff1a; 解决方案&#xff1a; sudo dhclient

uniapp - plugins的组件配置使用

点击进入到uniapp中mp-weixin的配置中 点击进入小程序的plugin的配置 在项目中&#xff0c;我们可引用插件的使用&#xff0c;例如一些快递100&#xff0c;点餐插件的业务引入 添加插件 在使用插件前&#xff0c;首先要在小程序管理后台的“设置-第三方服务-插件管理”中添加…

【论文阅读】SegNeXt:重新思考卷积注意力设计

《SegNeXt: Rethinking Convolutional Attention Design for Semantic Segmentation》 原文&#xff1a;https://github.com/Visual-Attention-Network/SegNeXt/blob/main/resources/paper.pdf 源码&#xff1a;https://github.com/Visual-Attention-Network/SegNeXt 1、简介 …

图书管理系统900pf

TOC springboot533图书管理系统900pf 第一章 课题背景及研究内容 1.1 课题背景 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0…

(十四)SpringCloudAlibaba-Nacos集群

前言 Nacos集群官方部署说明集群部署说明https://nacos.io/docs/latest/guide/admin/cluster-mode-quick-start/ 1.官方Nacos集群图 2.集群计划 因为本次的重点在于搭建Nacos集群&#xff0c;为了方便本次数据库我们使用单节点; 3.资源规划 节点IPPortnacos_8845127.0.0.18…

Java语聊大厅个人厅陪玩厅陪玩系统小程序源码

&#x1f389;【探索语聊新纪元】&#x1f389; —— 语聊大厅&#xff1a;你的专属社交乐园 &#x1f31f;【初识语聊大厅&#xff0c;开启无限可能】&#x1f31f; 嘿宝贝们&#xff0c;今天我要带你们走进一个超燃的社交新天地——语聊大厅&#xff01;这可不是普通的聊天…