文章目录
- 服务端通过传入命令处理实现远程命令执行
- 使用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.1
或127.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
); - 进入循环:
- 获取用户输入
- 发送到服务器
- 接收服务器响应并显示
由于协议分层,Windows
与Linux
不同平台可以使用同一套协议簇进行网络通信;
因为即使是不同平台下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夹心小面包]