欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
目录
- 👉🏻客户端代码
- Makefile(生成目标文件)
- UdpClient.cc(客户端代码)
- 服务端代码部分优化1(接受客户端时显示客户端地址信息)
- InetAddrtoLocal.hpp(将网络地址信息转为主机地址信息)
- inet_ntoa
- 服务端代码部分优化2(不给服务器绑定具体IP地址)
- Udpserver.hpp(去除了ip成员)
- Main.cc
- 👉🏻windows充当client给服务端发消息
- 👉🏻实现处理命令
- Udpserver.hpp
- Main.cc
- popen函数
- fgets函数
👉🏻客户端代码
Makefile(生成目标文件)
UdpClient.cc(客户端代码)
#include<iostream>
#include"Log.hpp"
#include"ComErr.hpp"
#include<unistd.h>
#include<cstring>//包含strerror函数
#include<strings.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
using namespace std;
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_ip local_port\n" << std::endl;
}
int main(int argc,char* argv[3])
{
if(argc!=3)
{
Usage(argv[0]);
exit(Usage_Err);//unistd.h
}
//客户端输入的ip地址和端口号,是要发送对象的ip地址和端口号,这里我们从命令行记录一下
string ip = argv[1];
uint16_t port = stoi(argv[2]);
//给client创建套接字对象
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd==-1)
{
lg.LogMessage(Fatal,"socket err : %d %s\n",errno,strerror(errno));//cerrno
exit(Socket_Err);
}
//client不需要手动绑定唯一网络信息,具体解释后面给出
//接下来就是发送消息给服务端并接收服务端消息
//但是发送数据前我们需要准备好报头,即目标对象的地址信息
struct sockaddr_in dest;
memset(&dest,0,sizeof(dest));
dest.sin_family = AF_INET;//协议族
dest.sin_port = htons(port);//将主机号变为网络序列
dest.sin_addr.s_addr = inet_addr(ip.c_str());//ip地址转4字节且是网络序列
socklen_t len = sizeof(dest);
while(true)
{
//1.发送消息
string inbuffer;
cout<<"Please enter :";
getline(cin,inbuffer);//string
ssize_t n = sendto(sockfd,inbuffer.c_str(),inbuffer.size(),0,(struct sockaddr*)&dest,len);
if(n!=-1)
{
//发送成功
//2.接收服务端消息
int defaultbuffer = 1024;
char buffer[defaultbuffer];
struct sockaddr_in temp;//存储服务端地址信息
socklen_t templen = sizeof(temp);
ssize_t ret = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&templen);
if(ret!=0)
{
//接收到服务端消息
buffer[ret] = '\0';
cout<<"Server say :"<<buffer;
}
else
{
cout<<"服务端并未回应...\n";
}
}
else
{
cout<<"send fail\n";
break;
}
}
close(sockfd);
return 0;
}
至此我们实现了客户端代码,现在让我们将其和服务端进行联动吧😄
效果还是不错的。
这里要解释几个要点:
🍎1.为什么client不能绑定唯一端口而是随机绑定?
因为会有非常多的客户端,比如我今天打开qq,qq客户端绑定了一个唯一端口号999,然后我又打开了王者荣耀,此时王者荣耀也想绑定999的端口号,此时就会起冲突,导致客户端打开错误了!
所以client需要Bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号
什么时候bind? 在client首次发送数据的时候,进行由内核随机Bind
🍎2.ip地址127.0.0.1
IP地址127.0.0.1是IPv4地址中的一个特殊地址,它被保留用于本地环回测试。当你在计算机上访问127.0.0.1时,实际上是在访问你自己的计算机。这个地址通常被用来进行网络客户端/服务器测试,因为它允许在没有真实网络连接的情况下模拟网络通信。
当你将127.0.0.1作为目标IP地址时,你的计算机会尝试与自己进行通信。这对于测试网络应用程序、调试网络问题或者简单地检查网络服务是否正在正常运行非常有用。例如,当你在本地运行一个Web服务器时,可以通过在浏览器中输入"http://127.0.0.1"来访问它,而无需连接到真实的互联网。
服务端代码部分优化1(接受客户端时显示客户端地址信息)
这里我们专门写一个将网络信息转换为主机信息的类
InetAddrtoLocal.hpp(将网络地址信息转为主机地址信息)
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
class InetAddrtoLocal
{
public:
InetAddrtoLocal(){}
InetAddrtoLocal(struct sockaddr_in& addr)
:_addr(addr)
{
_port= ntohs(_addr.sin_port);//将16位无符号短整数从网络字节序(大端序)转换为主机字节序。
_ip = inet_ntoa(_addr.sin_addr);
}
string Ip(){return _ip;}
uint16_t Port(){return _port;}
string Info()
{
string info = _ip;
info+=":";
info+=to_string(_port);
return info;
}
private:
string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
而后服务端那边部分代码稍微改下就行
服务端部分代码:👇🏻
if(n!=-1)
{
//说明接受信息成功
//接下来我们将打印发送方的信息
buffer[n] = '\0';
InetAddrtoLocal sender(peer);
printf("Client[%s] : %s\n",(sender.Info()).c_str(),buffer);
//cout<<"Client say : "<<buffer<<endl;
//接下来也发送一些信息回应客户端
char retstr[] = "Got it,processing...\n";
sendto(_sockfd,retstr,sizeof(retstr),0,(struct sockaddr*)&peer,len);
}
效果如下:
inet_ntoa
inet_ntoa
函数是一个C语言中用于将32位IPv4地址转换为点分十进制字符串表示的函数。在头文件<arpa/inet.h>
中声明。它接受一个struct in_addr
类型的IPv4地址作为参数,并返回一个表示该地址的字符串。
以下是inet_ntoa
函数的原型:
char *inet_ntoa(struct in_addr in);
其中in
是一个struct in_addr
类型的IPv4地址结构体。
inet_ntoa
函数将32位的IPv4地址从二进制格式转换为点分十进制格式,并将结果存储在一个静态分配的缓冲区中,然后返回该缓冲区的指针。请注意,由于使用了静态缓冲区,因此每次调用inet_ntoa
函数时,都会覆盖上一次调用的结果。因此,如果需要保存多个IPv4地址的字符串表示,应该将结果复制到另一个缓冲区中,以免被覆盖。
以下是一个示例,展示了如何使用inet_ntoa
函数将IPv4地址转换为点分十进制字符串:
#include <stdio.h>
#include <arpa/inet.h>
int main() {
struct in_addr addr;
addr.s_addr = inet_addr("192.0.2.1");
char *ip_str = inet_ntoa(addr);
printf("IPv4 Address: %s\n", ip_str);
return 0;
}
在这个示例中,首先将IPv4地址字符串"192.0.2.1"转换为struct in_addr
类型的结构体,然后使用inet_ntoa
函数将其转换为点分十进制字符串,并最终打印出来。
服务端代码部分优化2(不给服务器绑定具体IP地址)
在网络编程中,通常更推荐将服务器绑定到本地的随机IP地址,而不是固定的IP地址。这种方式通常被称为"本地绑定随机IP
"或"通配绑定
"。
以下是几个原因:
-
灵活性和可移植性: 如果服务器绑定了一个固定的IP地址,那么当需要将服务器迁移到另一个网络环境时,可能需要重新配置服务器以适应新的网络环境。而本地绑定随机IP的方式可以使得服务器更加灵活和可移植,因为它会自动选择可用的本地IP地址进行绑定,无需手动配置。
-
多网卡支持: 如果服务器有多个网络接口或多个IP地址,本地绑定随机IP的方式可以更好地利用这些资源,使得服务器能够在多个网络接口或多个IP地址上同时监听连接。
-
容错性: 本地绑定随机IP的方式可以提高服务器的容错性。当服务器监听的IP地址不可用时(例如,由于网络故障或IP地址冲突),服务器可以尝试绑定到另一个可用的本地IP地址,从而避免服务中断。
-
安全性: 通过本地绑定随机IP的方式,可以减少服务器暴露在公共网络上的风险。固定IP地址可能会成为黑客攻击的目标,而使用随机IP地址可以增加攻击者对服务器的识别和定位的难度。
在网络编程中,经常会使用INADDR_ANY
来赋值给IP地址。这个常量表示"任意"或"所有"的IP地址,通常用于服务器端绑定监听所有可用的网络接口,而不关心具体是哪个网络接口。具体来说,将IP地址设置为INADDR_ANY可以使服务器监听所有可用的网络接口上的连接请求,而不必关心具体是哪个网卡或IP地址。
所以,客户端代码要改的话,就是命令行参数我们不需要指定具体的ip了,而是让内核自定义随机可用的ip地址。
Udpserver.hpp(去除了ip成员)
#include<iostream>
#include "ComErr.hpp"
#include"Log.hpp"
#include <cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<cerrno>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"InetAddrtoLocal.hpp"
using namespace std;
const string defaultip = "127.0.0.0";
const uint16_t defaultport = 8888;
class Udpsever
{
public:
Udpsever(Udpsever& udp)=delete;//防止拷贝
Udpsever(const Udpsever& udp)=delete;
const Udpsever& operator=(Udpsever& udp)=delete;
Udpsever(const uint16_t& port = defaultport)
:_port(port)
{}
void Init()
{
//1.创建套接字对象
_sockfd = socket(AF_INET,SOCK_DGRAM,0);//创建一个套接字对象,协议族为IPv4,套接字类型是数据报套接字
if(_sockfd<0)
{
//创建失败则打印错误日志
lg.LogMessage(Fatal,"socket err,%d : %s\n",errno,strerror(errno));//strerror需要包含头文件cstring
exit(Socket_Err);
}
//成功则打印返回的套接字描述符
lg.LogMessage(Info,"sockfd : %d\n",_sockfd);
//上述我们只是创建了一个套接字对象,即我们本地的ip和端口号已经有了
//但是接下来我们要与网络信息绑定,才能实现在网络中进行通信
//我们要用到socket编程中的bind函数:用于将套接字与特定的地址(IP地址和端口号)绑定在一起,以便其他计算机可以找到并与之通信
//2.bind并指定网络信息
//2.1指定网络地址信息
struct sockaddr_in local;//创建一个网络地址信息结构体,接下里初始化地址信息
bzero(&local,sizeof(local));//相当于memset
local.sin_family = AF_INET;//1.初始化协议族
local.sin_port = htons(_port);//2.将16位无符号短整数从主机字节序转换为网络字节序(大端序)
local.sin_addr.s_addr = INADDR_ANY;//3.初始化ip地址,这里我们想让_ip变为4字节且是网络字节序,所以使用转换函数inet_addr
//2.2将网络地址信息与socket文件对象进行绑定
//bind函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bd = bind(_sockfd,(const struct sockaddr *)&local,sizeof(local));
if(bd!=0)
{
lg.LogMessage(Fatal,"bind err :%d %s\n",errno,strerror(errno));
exit(Bind_Err);
}
//至此socket初始化完成
}
void Start()
{
int defaultbuffer = 1024;
char buffer[defaultbuffer];
while(true)//服务器永远不退出
{
//接受信息
struct sockaddr_in peer;//用来存储发送方的地址信息
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1/*再减1是给\0留位置*/,0,(struct sockaddr*)&peer,&len);
if(n!=-1)
{
//说明接受信息成功
//接下来我们将打印发送方的信息
buffer[n] = '\0';
InetAddrtoLocal sender(peer);
printf("Client[%s] : %s\n",(sender.Info()).c_str(),buffer);
//cout<<"Client say : "<<buffer<<endl;
//接下来也发送一些信息回应客户端
char retstr[] = "Got it,processing...\n";
sendto(_sockfd,retstr,sizeof(retstr),0,(struct sockaddr*)&peer,len);
}
}
}
private:
uint16_t _port;//端口号
int _sockfd;//套接字描述符
};
Main.cc
#include "Udpserver.hpp"
#include "ComErr.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << "local_port\n" << std::endl;
}
int main(int argc,char* argv[2])
{
if(argc!=2)//如果命令行指令数没有两个
{
Usage(argv[0]);
return Usage_Err;
}
//string ip = argv[1];//获取ip
uint16_t port = stoi(argv[1]);//获取端口号
//接下来创建Udpserver的指针
unique_ptr<Udpsever> usvr = std::make_unique<Udpsever>(port);//unique_ptr不支持拷贝构造和赋值操作,因此确保了资源的独占性
//使用make_unique记得添加memory头文件
usvr->Init();
usvr->Start();
return 0;
}
效果如下: 👇🏻
👉🏻windows充当client给服务端发消息
#include <WinSock2.h>
#include <Windows.h>
#include <iostream>
#include <string>
#pragma comment(lib,"ws2_32.lib")//引入动态库
using namespace std;
uint16_t serverport = 8081;
string serverip = "110.40.135.148";
int main()
{
WSADATA wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)//给动态库初始化
{
cout << "WSAStartup failed\n";
return 1;
}
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == INVALID_SOCKET)
{
cout << "socket create failed\n";
WSACleanup();
return 1;
}
struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(serverport);
dest.sin_addr.s_addr = inet_addr(serverip.c_str());
int len = sizeof(dest);
while (true)
{
string inbuffer;
cout << "Please enter :";
getline(cin, inbuffer);
int n = sendto(sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&dest, len);
if (n != SOCKET_ERROR)
{
cout << "send success" << endl;
const int defaultbuffer = 1024;
char buffer[defaultbuffer];
struct sockaddr_in temp;
int templen = sizeof(temp);
int ret = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &templen);
if (ret != SOCKET_ERROR)
{
buffer[ret] = '\0';
cout << "Server say: " << buffer << endl;
}
else
{
cout << "recvfrom failed: " << WSAGetLastError() << endl;
}
}
else
{
cout << "sendto failed: " << WSAGetLastError() << endl;
}
// Add a condition to exit the loop
if (inbuffer == "exit")
{
break;
}
}
closesocket(sockfd);
WSACleanup();//清理ws2库
return 0;
}
经过我的测试
说实话代码没有问题,数据也能发送成功。
但是不知道是不是因为网络防火墙的原因,服务端那边一直阻塞在recvfrom函数那里,导致数据一直不能被读出,也是很尴尬了(后续我再看看有没有什么办法)
👉🏻实现处理命令
这里我只给出修改过代码的源文件
Udpserver.hpp
#include<iostream>
#include "ComErr.hpp"
#include"Log.hpp"
#include <cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<cerrno>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"InetAddrtoLocal.hpp"
#include<functional>
using namespace std;
const string defaultip = "127.0.0.0";
const uint16_t defaultport = 8888;
const int defaultbuffer = 1024;
using func_t = function<string(string)>;//定义函数类型
class Udpsever
{
public:
Udpsever(Udpsever& udp)=delete;//防止拷贝
Udpsever(const Udpsever& udp)=delete;
const Udpsever& operator=(Udpsever& udp)=delete;
Udpsever(func_t OnMessage,const uint16_t& port = defaultport)
:_port(port),_OnMessage(OnMessage)
{}
void Init()
{
//1.创建套接字对象
_sockfd = socket(AF_INET,SOCK_DGRAM,0);//创建一个套接字对象,协议族为IPv4,套接字类型是数据报套接字
if(_sockfd<0)
{
//创建失败则打印错误日志
lg.LogMessage(Fatal,"socket err,%d : %s\n",errno,strerror(errno));//strerror需要包含头文件cstring
exit(Socket_Err);
}
//成功则打印返回的套接字描述符
lg.LogMessage(Info,"sockfd : %d\n",_sockfd);
//上述我们只是创建了一个套接字对象,即我们本地的ip和端口号已经有了
//但是接下来我们要与网络信息绑定,才能实现在网络中进行通信
//我们要用到socket编程中的bind函数:用于将套接字与特定的地址(IP地址和端口号)绑定在一起,以便其他计算机可以找到并与之通信
//2.bind并指定网络信息
//2.1指定网络地址信息
struct sockaddr_in local;//创建一个网络地址信息结构体,接下里初始化地址信息
bzero(&local,sizeof(local));//相当于memset
local.sin_family = AF_INET;//1.初始化协议族
local.sin_port = htons(_port);//2.将16位无符号短整数从主机字节序转换为网络字节序(大端序)
local.sin_addr.s_addr = INADDR_ANY;//3.初始化ip地址,这里我们想让_ip变为4字节且是网络字节序,所以使用转换函数inet_addr
//2.2将网络地址信息与socket文件对象进行绑定
//bind函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bd = bind(_sockfd,(const struct sockaddr *)&local,sizeof(local));
if(bd!=0)
{
lg.LogMessage(Fatal,"bind err :%d %s\n",errno,strerror(errno));
exit(Bind_Err);
}
//至此socket初始化完成
}
void Start()
{
cout<<"start"<<endl;
char buffer[defaultbuffer];
while(true)//服务器永远不退出
{
cout<<"running..."<<endl;
//接受信息
struct sockaddr_in peer;//用来存储发送方的地址信息
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1/*再减1是给\0留位置*/,0,(struct sockaddr*)&peer,&len);
if(n!=-1)
{
cout<<"收到\n";
//说明接受信息成功
//接下来我们将打印发送方的信息
buffer[n] = '\0';
//处理消息
string response = _OnMessage(buffer);
// InetAddrtoLocal sender(peer);
// printf("Client[%s] : %s\n",(sender.Info()).c_str(),buffer);
// //cout<<"Client say : "<<buffer<<endl;
// //接下来也发送一些信息回应客户端
// char retstr[] = "Got it,processing...\n";
sendto(_sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&peer,len);
}
else
{
cout<<"没收到\n";
strerror(errno);
}
}
}
private:
uint16_t _port;//端口号
int _sockfd;//套接字描述符
func_t _OnMessage;//回调函数
};
Main.cc
#include "Udpserver.hpp"
#include "ComErr.hpp"
#include <memory>
#include<cstdio>
#include<vector>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << "local_port\n" << std::endl;
}
string OnMessageDefault(string request)
{
return request + "[got message]\n";
}
std::vector<std::string> black_words = {//黑名单
"rm",
"unlink",
"cp",
"mv",
"chmod",
"exit",
"reboot",
"halt",
"shutdown",
"top",
"kill",
"dd",
"vim",
"vi",
"nano",
"man"
};
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;
}
string ExcuteCommand(string command)
{
if(!SafeCheck(command)) return "bad man!!\n";
cout<<"get a message: "<< command<<endl;
FILE* fp = popen(command.c_str(),"r");
if(fp == nullptr)
{
return "excute error,reason is unknown\n";
}
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()?"Nothing excute\n":response;//将处理结果输出到屏幕上
}
int main(int argc,char* argv[2])
{
if(argc!=2)//如果命令行指令数没有三个
{
Usage(argv[0]);
return Usage_Err;
}
//string ip = argv[1];//获取ip
uint16_t port = stoi(argv[1]);//获取端口号
//接下来创建Udpserver的指针
unique_ptr<Udpsever> usvr = std::make_unique<Udpsever>(ExcuteCommand,port);//unique_ptr不支持拷贝构造和赋值操作,因此确保了资源的独占性
//使用make_unique记得添加memory头文件
usvr->Init();
usvr->Start();
return 0;
}
popen函数
popen
函数是一个 C 标准库中的函数,用于创建一个进程,并建立一个管道连接到该进程的标准输入或标准输出。它通常用于创建子进程,并与其进行双向通信。
popen
函数的原型如下:
FILE *popen(const char *command, const char *mode);
其中,command
参数是一个字符串,表示要执行的命令,mode
参数是一个字符串,指定打开的管道是用于读取还是写入。mode
参数可以是 "r"
(读取)或 "w"
(写入)。
popen
函数返回一个 FILE
指针,可以像使用 fopen
返回的文件指针一样来操作管道。如果 popen
调用失败,它将返回 NULL
。
下面是一个简单的示例,演示如何使用 popen
函数来执行一个命令并读取其输出:
#include <stdio.h>
int main() {
FILE *pipe;
char buffer[128];
// 执行命令并读取其输出
pipe = popen("ls -l", "r");
if (pipe == NULL) {
perror("popen");
return -1;
}
// 读取管道的输出并打印到标准输出
while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
printf("%s", buffer);
}
// 关闭管道
pclose(pipe);
return 0;
}
在这个示例中,我们使用 popen
执行了 ls -l
命令,并从其输出中读取文件列表,并将其打印到标准输出。最后,使用 pclose
函数关闭了管道。
需要注意的是,popen
函数在 Windows 平台上不是标准 C 库的一部分,并且可能不可用。在 Windows 上,可以考虑使用 _popen
和 _pclose
函数来实现类似的功能。
fgets函数
fgets
函数是 C 标准库中用于从文件流中读取一行数据的函数。它的原型如下:
char *fgets(char *str, int n, FILE *stream);
其中:
str
是一个指向字符数组的指针,用于存储读取的字符串。n
是一个整数,表示要读取的最大字符数,包括最后的空字符 ‘\0’。stream
是一个指向文件的指针,指定要读取的文件流。
fgets
函数会从指定的文件流中读取字符,直到遇到换行符 ‘\n’、文件结束符 EOF 或者读取了 n-1 个字符为止(包括换行符)。它会将读取的字符存储到 str 指向的字符数组中,并在末尾添加一个空字符 ‘\0’,以表示字符串的结束。
fgets
函数的返回值是一个指向目标字符数组的指针,即参数 str
的值,或者在发生错误或到达文件末尾时返回 NULL
。
下面是一个简单的示例,演示如何使用 fgets
函数从文件中读取一行数据:
#include <stdio.h>
int main() {
FILE *file;
char buffer[128];
// 打开文件以供读取
file = fopen("example.txt", "r");
if (file == NULL) {
perror("fopen");
return -1;
}
// 从文件中读取一行数据
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}
// 关闭文件
fclose(file);
return 0;
}
在这个示例中,我们打开一个名为 example.txt
的文件以供读取,然后使用 fgets
函数从文件中逐行读取数据,并将其打印到标准输出。最后,使用 fclose
函数关闭文件。
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长