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

文章目录

    • 服务端通过传入命令处理实现远程命令执行
    • 使用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结构体为0if (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_; // IPUINT16 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 socketsockfd_ = 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() {}// 初始化服务器,创建并绑定socketvoid Init() {// 创建UDP socketsockfd_ = 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.mzph.cn/news/877567.shtml

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

相关文章

springboot医疗远程管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据库参…

el-table实现动态添加行,并且有父子级联动下拉框

<template><div><el-button click"addRow">添加行</el-button><el-table :data"tableData" style"width: 100%"><el-table-column label"序号"type"index"width"100"align"…

【ARM+Codesys 客户案例 】基于RK3568/A40i/STM32+CODESYS开发的控制器在自动输送分拣系统上的应用,支持定制

2021年“京东618” 累计下单金额超3438亿元,再次刷新纪录! 从下单到收货&#xff0c;各种货品均可在短短几天内通过四通八达的物流网络送达全国任何一个家庭。电子商务和快递物流的迅猛发展对仓储、分拣、配送效率和准确性均提出了更高的要求&#xff0c;加速了智能物流的发展。…

如何免费获取乡镇级边界数据geoJson数据

如何免费获取乡镇级边界数据geoJson数据 我们可以通过 阿里云数据可视化平台 &#xff0c;可以获取到中国各个省份/区级/县级的json数据&#xff0c;但是区级和县级&#xff0c;并没有包含街道和乡镇的数据 获取乡镇级边界数据 1.下载bigemap全能版 安装好后选择你要导出的…

C++竞赛初阶L1-13-第五单元-循环嵌套(29~30课)537: T456456 质因数分解

题目内容 已知正整数 n 是两个不同的质数的乘积&#xff0c;试求出较大的那个质数。 输入格式 输入只有一行&#xff0c;包含一个正整数 n&#xff08;6<n<109&#xff09;。 输出格式 输出只有一行&#xff0c;包含一个正整数 p&#xff0c;即较大的那个质数。 样例…

《黑神话:悟空》媒体评分解禁 M站均分82

《黑神话&#xff1a;悟空》媒体评分现已解禁&#xff0c;截止发稿时&#xff0c;M站共有43家媒体评测&#xff0c;均分为82分。 部分媒体评测&#xff1a; God is a Geek 100&#xff1a; 毫无疑问&#xff0c;《黑神话&#xff1a;悟空》是今年最好的动作游戏之一&#xff…

ant design pro v6 如何做好角色管理

先上图&#xff1a; 整个角色管理是如何做的吗&#xff1f; 首先你要处理后端&#xff0c;要先把角色存到用户那。 这是用户管理部分的内容&#xff1a; 可以看到一个用户是有多个角色的。 看到没有&#xff0c;存的是数组 数组的是一个 role 对象 role 对象是这样&#xf…

在选择或推荐数据恢复软件之前,您如何测试和审查它?

数据恢复软件可以帮助您从各种存储设备中检索丢失或删除的文件&#xff0c;例如硬盘驱动器&#xff0c;USB闪存驱动器&#xff0c;存储卡或智能手机。但是&#xff0c;并非所有数据恢复软件都是一样的&#xff0c;根据您的情况和需求&#xff0c;有些软件的性能可能比其他软件更…

网安入门—信息收集

1.定义 信息收集是指收集有关目标应用程序和系统的相关信息。这些信息可以帮助攻击者了解目标系统的架构、技术实现细节、运行环境、网络拓扑结构、安全措施等方面的信息&#xff0c;以便我们在后续的渗透过程更好的进行。 2.分类 主动信息收集和被动信息收集 区别&#xf…

微信公众号发送模板消息使用说明

一、获取access_token def get_access():appid secret url fhttps://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credential&appid{appid}&secret{secret}res requests.get(url).json()return res 返回结果如下&#xff1a; {access_token: 83_TAxuwdt…

android FD_SET_chk问题定位

android FD_SET_chk问题定位 一、FD报错二、问题定位2.1 APM定位2.2 adb定位2.3. 代码获取FD数 三、FD优化 一、FD报错 App在运行中记录报错如下&#xff0c;FD_SET&#xff0c;这个问题大概是文件描述符&#xff08;File Descriptor&#xff0c;简称FD&#xff09;超过了最大…

MySQL InnoDB引擎四大特性ACID实现方案分析

文章目录 概要InnoDb引擎ACID模型的实现方案小结 概要 对于Mysql&#xff0c;事物的支撑并不依赖于Server层&#xff0c;不同的存储引擎对于事物的支持也不一样&#xff0c;对于我们常用的InnoDB引擎&#xff0c;其提供了一套基于【ACID模型】的事物完整的解决方案。为什么MyIS…

云计算实训32——安装nginx(修改端口为8080)、roles基本用法、使用剧本安装nginx、使用roles实现lnmp

一、安装nginx并更改其端口 编辑hosts配置文件 [rootmo ~]# vim /etc/ansible/hosts 创建目录 [rootmo ~]# mkdir /etc/ansible/playbook 编辑配置文件 [rootmo ~]# vim /etc/ansible/playbook/nginx.yml 执行测试 [rootmo ~]# ansible-playbook /etc/ansible/playbook/n…

有源音箱申请Hi-Res认证指南

有源音箱&#xff08;也称为主动式音箱&#xff09;是一种内置功率放大器的音箱&#xff0c;其显著特点是音箱内部含有一套功率放大电路&#xff0c;可以直接通过音频线&#xff08;如RCA线、3.5mm音频线或莲花线&#xff09;与信号源&#xff08;如电视、电脑、DVD播放器等&am…

详细扒一扒css的背景渐变(通俗易懂)

前言&#xff1a; CSS 渐变使您可以显示两种或多种指定颜色之间的平滑过渡。 CSS 定义了两种渐变类型&#xff1a; 线性渐变&#xff08;向下/向上/向左/向右/对角线&#xff09;径向渐变&#xff08;由其中心定义&#xff09; 下面来详细看看吧~ &#x1f308;&#x1f308;文…

【知识分享】ubuntu22.04-ESP32环境搭建

文章目录 一、概要二、环境及工具介绍三、名词解释四、环境搭建 一、概要 手上有一块安信可的WIFI开发板&#xff0c;用的是乐鑫的ESP32模组。刚好最新装了双系统&#xff0c;貌似在Linux环境使用gcc编译器会快一些。     万事开头难&#xff0c;要在Linux环境下进行开发工…

[数据集][目标检测]瞳孔虹膜检测数据集VOC+YOLO格式8768张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8768 标注数量(xml文件个数)&#xff1a;8768 标注数量(txt文件个数)&#xff1a;8768 标注…

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(二)---ROS2与UE5进行图像数据传输

前言 本系列教程旨在使用UE5配置一个具备激光雷达深度摄像机的仿真小车&#xff0c;并使用通过跨平台的方式进行ROS2和UE5仿真的通讯&#xff0c;达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础&#xff0c;Nav2相关的学习教程可以参考本人的其他博…

【MySQL进阶之路】表的约束——主键,自增长,唯一键,外键

目录 主键 复合主键 自增长 唯一键 unique 外键 方案一 方案二 方案三 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 主键 主键&#xff1a;primary key用来唯一的约束该字段里面的数据&#xff0c;不能重复&#xff0c;不能为空&#xff08;必须有非空约束&#xf…

微知-lspci如何查看pcie设备树状结构(-t)

对于查看pcie设备列表除了看是否存在 还需要看拓扑结构。如何看&#xff1f; lspci -t以减号为分割说明 第一列数字是域段 和 bus id。比如0000:00中0000是域 00是busid 第二列 01.2中01是device id。2是functionid 如果还有下游设备device还有一个指定busid的序号