网站开发后端选择,关于网站建设调查问卷,注册传媒公司需要多少钱,wordpress广告图片自动轮播代码目录
引入
基础版
服务端
思路
头文件log类
套接字的初始化
思路
代码
服务器开始运行
思路
代码
注意点 -- ip地址和端口号的来源
ip地址的选择
本地环回地址
端口号
编辑
运行情况
netstat -nlup
客户端
思路
初始化
思路
代码
客户端的运行
思…目录
引入
基础版
服务端
思路
头文件log类
套接字的初始化
思路
代码
服务器开始运行
思路
代码
注意点 -- ip地址和端口号的来源
ip地址的选择
本地环回地址
端口号
编辑
运行情况
netstat -nlup
客户端
思路
初始化
思路
代码
客户端的运行
思路
代码
运行情况
运行情况
改成命令行输入
服务端 客户端
运行情况
远程让服务器执行命令
引入
分层
代码
运行情况 引入 我们将使用udp协议ipv4协议来写cs之间的网络通信 将服务端运行在我们的云服务器上,其他机器可以运行客户端来向服务端发送消息服务端收到消息后,经过一定处理直接发回给相应的客户端并且,我们将一步一步改善我们的代码,让他变得更加美观拥有更多功能 基础版
服务端将收到的数据经过包装后直接回传给发送端,类似于echo
服务端 思路 封装成一个类,提供运行接口即可 启动后创建套接字,并保持等待数据的状态 头文件log类 下面是用到的头文件: #include netinet/in.h
#include sys/types.h
#include sys/socket.h
#include unistd.h
#include arpa/inet.h
#include strings.h
#include cstring#include string
#include iostream#include Log.hpp 以及用来提示消息的log类 数据格式 -- 时间消息 #pragma once#include iostream
#include time.h
#include stdarg.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include unistd.h
#include stdlib.h#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4 // 致命的错误#define SIZE 1024class Log
{
public:Log(){}void operator()(int level, const char *format, ...){time_t t time(nullptr);struct tm *ctime localtime(t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), [%s][%d-%d-%d %d:%d:%d], levelToString(level).c_str(),ctime-tm_year 1900, ctime-tm_mon 1, ctime-tm_mday,ctime-tm_hour, ctime-tm_min, ctime-tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式默认部分自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), %s %s, leftbuffer, rightbuffer);printf(%s\n, logtxt);}~Log(){}private:std::string levelToString(int level){switch (level){case INFO:return INFO;case DEBUG:return DEBUG;case WARNING:return WARNING;case ERROR:return ERROR;case FATAL:return FATAL;default:return NONE;}}
}; 套接字的初始化 思路 首先创建出套接字文件 这里使用udp协议ipv4网络协议所以,可以确定socket函数中使用的参数为AF_INET, SOCK_DGRAM 然后我们需要构建一个存放了网络地址信息(ip端口号其他信息)的结构体 传参传的是公共类型的结构体,但实际使用的还是那两个中的一个所以需要手动构建一个初始化可以用memset,也可以用bzero 端口号需要在网络中传输,但它最初是在类内初始化的,是跟随系统字节序列来的 我们无法保证系统一定是大端 所以在将端口号传入结构体中时(网络中使用的端口号就是从这个结构体中存放的),需要转换为网络字节序列 同理,ip地址也需要传输,所以也需要转换为网络序列 (ip地址和端口号的选择在后面讲) 我们虽然使用了系统提供的类型,但这里的变量依然是在用户空间中定义的,并没有和内核中的套接字产生关联 套接字需要这个结构体中的数据,将数据传进内核的套接字里,才叫做绑定前面的都只是预备工作,实际的绑定工作由bind函数完成 -- 通过网络文件描述符和结构体对象,将文件信息和网络信息相关联 以上,服务器端的套接字就建立完成 代码 void init(){// 创建套接字文件sockfd_ socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ 0){lg(FATAL, socket create error, sockfd : %d,%s, sockfd_,strerror(errno));exit(SOCKET_ERR);}// 创建sockaddr结构struct sockaddr_in addr;socklen_t len sizeof(addr);bzero(addr, len);addr.sin_addr.s_addr inet_addr(ip_.c_str());addr.sin_family AF_INET;addr.sin_port htons(port_);// 绑定套接字信息int res bind(sockfd_, reinterpret_castconst struct sockaddr *(addr), len);if (res 0){lg(FATAL, bind error, sockfd : %d,%s, sockfd_,strerror(errno));exit(BIND_ERR);}lg(INFO, bind success, sockfd : %d, sockfd_);} 服务器开始运行 思路 服务端负责获取客户端发送的数据 使用recvfrom函数获得数据报形式的数据(也就是定义一个缓冲区)且拿到发送方的网络地址信息(结构体),方便将响应数据发回给对方 拿到数据后,进行处理 这里在消息前封装一些标识字段 -- ip端口号发送时间 处理完后,就发回给发送方 使用sendto函数发送数据报 代码 std::string generate_id(const std::string ip, const uint16_t port){return [ ip : std::to_string(port) ];}
std::string process_info(const std::string info){time_t t time(nullptr);struct tm *ctime localtime(t);char time_stamp[SIZE];snprintf(time_stamp, sizeof(time_stamp), [%d-%d-%d %d:%d:%d]:,ctime-tm_year 1900, ctime-tm_mon 1, ctime-tm_mday,ctime-tm_hour, ctime-tm_min, ctime-tm_sec);std::string id generate_id(ip_, port_);std::string res id time_stamp info \n;return res;}void run(){init();// 开始收发数据char buffer[buff_size];std::string message;while (true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len sizeof(src_addr);// 获取数据ssize_t n recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_caststruct sockaddr *(src_addr), src_len);if (n 0){lg(WARNING, recvfrom error, errno: %d, err string: %s, errno, strerror(errno));continue;}buffer[n] 0;std::string id generate_id(inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));message id sever recvfrom success;lg(INFO, message.c_str());// 处理数据std::string echo_info process_info(buffer);// 响应给发送端sendto(sockfd_, echo_info.c_str(), echo_info.size(), 0, reinterpret_castconst struct sockaddr *(src_addr), src_len);message id sever sendto success;lg(INFO, message.c_str());}} 注意点 -- ip地址和端口号的来源
ip地址的选择 可以使用默认ip,也可以自己手动传入 udp_server(const uint16_t port 8080,const std::string ip 0.0.0.0): ip_(ip), port_(port), sockfd_(0){} #include udp_server.hppint main()
{udp_server s;//udp_server s(8080,127.0.0.1);s.run();return 0;
} 这里如果服务端直接连接云服务器的公有ip,会报错: 因为云服务器禁止直接绑定公网ip一台主机上可能会配置多个网卡,你看到的ip只是其中一个如果就固定地绑定了那一个,你的服务器就只能收到那一个ip地址收到的报文,其他ip收到的就看不到了所以,如果我们将ip地址写成0(也就是那个0.0.0.0),就可以将发往这台主机(无论是哪张网卡,它会动态识别是否属于该主机)的所有信息都向上交付给对应端口或者用宏 INIDDR_ANY虚拟机是可以直接绑定ip的 本地环回地址 这里的127.0.0.1指的是本地环回地址 如果服务器绑定这个ip,就不会将数据发送到网络,而是贯穿网络协议栈后,重新发回本主机也就是本地通信 端口号 所以,我们一般用大一点的数字分配给我们的服务端 运行情况 #include udp_server.hpp
#include cstdlib
using namespace std;int main()
{udp_server s;// udp_server s(8080,127.0.0.1);s.run();return 0;
}netstat -nlup n表示,把所有可以显示为数字的显示成数字,否则有些会是字符串p表示,显示pid信息 用于检查网络状态和网络连接: 每列表示的含义: 协议 收发报文的个数 本地ip地址端口号 可以接收什么客户端的消息 只要可以查到信息,就说明这个服务器已经启动成功了 客户端 思路 和服务端的流程差不多,依然是封装成类,调用启动接口就可以启动客户端 启动后,就等待用户进行输入 初始化 思路 客户端也需要创建自己的套接字,但是不需要手动绑定 os会为我们自由分配一个端口号(客户端的端口号没有什么要求,只需要保证最基本的唯一性即可),且将本机ip和端口号与创建出的套接字绑定os什么时候为我们绑定?首次发送消息的时候,也就是需要网络通信的时候再绑定 因为需要将输入的数据发给服务端,所以就需要提前知道服务端的套接字地址信息 日常生活中,我们一般直接通过网址获得网站提供的服务 网址经过处理后,就是ip地址,而端口号是固定的,一个ip地址就会有对应的固定端口号 所以我们知道网址客户端知道了ip地址服务端的套接字被我们获取到 但这里我们写的很简陋,只能手动提供服务端的ip和端口号,然后构建出服务端的网络地址信息(结构体) (使用的接口还是那些) 代码 struct data
{int sockfd_;struct sockaddr_in *paddr_;socklen_t len_;
};data *init(){// 创建套接字文件int sockfd socket(AF_INET, SOCK_DGRAM, 0);if (sockfd 0){lg(FATAL, socket create error, sockfd : %d, sockfd);exit(SOCKET_ERR);}// 创建sockaddr结构struct sockaddr_in *svr_paddr new sockaddr_in;socklen_t svr_len sizeof(*svr_paddr);bzero(svr_paddr, svr_len);svr_paddr-sin_addr.s_addr inet_addr(ip_.c_str());svr_paddr-sin_family AF_INET;svr_paddr-sin_port htons(port_);return new data({sockfd, svr_paddr, svr_len});} 客户端的运行 思路 就是等待用户输入,然后发送给服务端 然后等待服务端的响应数据 最后将服务端处理后的数据打印出来 代码 void run(){data *d init();std::string info;char buffer[buff_size];while (true){std::cout Please enter:;std::getline(std::cin, info);// 将消息发送给服务器sendto(d-sockfd_, info.c_str(), info.size(), 0, reinterpret_castconst struct sockaddr *(d-paddr_), d-len_);info.clear();struct sockaddr_in addr; // 仅用于填充参数,拿到自己的地址信息没啥意义socklen_t len sizeof(addr);// 获取数据ssize_t n recvfrom(d-sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_caststruct sockaddr *(addr), len);if (n 0){lg(WARNING, recvfrom error, errno: %d, err string: %s, errno, strerror(errno));continue;}buffer[n] 0;std::cout buffer std::endl;memset(buffer, 0, sizeof(buffer));}} 运行情况 #include udp_client.hpp
#include cstdlib
using namespace std;int main()
{udp_client c;//udp_client c(8080,127.0.0.1);c.run();return 0;
} 运行情况 这样我们就完成了cs互相通信的过程 改成命令行输入
上面我们是在代码中定义了ip地址和端口号,也可以以命令行的形式,将服务器的ip地址和端口号输入
也就是利用main函数的参数只需要改一下主函数代码即可
服务端
#include udp_server.hpp
#include cstdlib
using namespace std;//./udp_server port
int main(int argc, char *argv[])
{if (argc ! 2){std::cout ./udp_server port(port1024) std::endl;exit(1);}udp_server s(atoi(argv[1]));s.run();return 0;
} 客户端
#include udp_client.hpp
#include cstdlib
using namespace std;//./udp_client ip port
int main(int argc, char *argv[])
{while (argc ! 3){std::cout ./udp_client ip port(port1024) std::endl;exit(1);}udp_client s(atoi(argv[2]), argv[1]); // port,ips.run();return 0;
} 运行情况 如果输入的内容不足,就会提示: 然后cs端都运行起来,并发送消息: 远程让服务器执行命令 引入 除了简单的转发消息,我们也可以发送命令,远程让服务器执行命令,且将执行结果传给客户端,让我们可以看到 这不就是我们的shell吗其实之前有模拟过简单的shell(创建子进程,使用exec系列函数,让子进程去执行命令) 或者直接用popen函数 它会在内部帮助我们创建一条管道,且创建子进程执行命令,然后把执行后的结果写入到管道文件中,以文件指针的形式返回给我们 command 要执行的shell命令 mode 表示要打开的管道的模式看你想要如何与命令交互:如果是只想要查看命令的输出,就以r打开 (ls)如果想要将一些数据发送给它作为输入,就以w打开 (cat) 分层 所以我们考虑改变一下代码结构,将处理数据与服务器收到数据解耦 也就是在服务端处理数据时,使用自定义的处理方法(以回调的方式使用) 代码 服务端类里面修改的不多 在处理数据那里,我们直接调用参数: void run(func_t func){init();// 开始收发数据char buffer[buff_size];std::string message;while (true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len sizeof(src_addr);// 获取数据ssize_t n recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_caststruct sockaddr *(src_addr), src_len);if (n 0){lg(WARNING, recvfrom error, errno: %d, err string: %s, errno, strerror(errno));continue;}buffer[n] 0;std::string id generate_id(inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));message id sever recvfrom success;lg(INFO, message.c_str());// 处理数据std::string echo_info func(buffer);// 响应给发送端sendto(sockfd_, echo_info.c_str(), echo_info.size(), 0, reinterpret_castconst struct sockaddr *(src_addr), src_len);message id sever sendto success;lg(INFO, message.c_str());}} 因为我们需要将处理函数放在类外,所以多增加了一个接口,用于外部调用 static std::string get_id() //外部使用{udp_server obj;return obj.generate_id(obj.ip_, obj.port_);}private:std::string generate_id(const std::string ip, const uint16_t port){return [ ip : std::to_string(port) ];} 主函数这里多修改了一些: 分出两个处理函数把获取时间的功能也单拎了出来 #include udp_server.hpp
#include cstdlib
#include cstdio
using namespace std;std::string get_time()
{time_t t time(nullptr);struct tm *ctime localtime(t);char time_stamp[SIZE];snprintf(time_stamp, sizeof(time_stamp), [%d-%d-%d %d:%d:%d]:,ctime-tm_year 1900, ctime-tm_mon 1, ctime-tm_mday,ctime-tm_hour, ctime-tm_min, ctime-tm_sec);return time_stamp;
}std::string process_info(const std::string info)
{std::string time_stamp get_time();std::string id udp_server::get_id();std::string res id time_stamp info;return res;
}std::string process_command(const std::string cmd)
{std::string time_stamp get_time();std::string id udp_server::get_id();std::string res;FILE *fp popen(cmd.c_str(), r);if (fp nullptr){lg(ERROR, popen error, errno: %d, err string: %s, errno, strerror(errno));return ;}char buffer[buff_size];memset(buffer, 0, sizeof(buffer));if (fgets(buffer, sizeof(buffer) - 1, fp) nullptr){res id time_stamp command error;return res;}while (true){memset(buffer, 0, sizeof(buffer));if (fgets(buffer, sizeof(buffer) - 1, fp) ! nullptr){res buffer;}else{break;}}return res;
}
int main()
{udp_server s;// udp_server s(8080,127.0.0.1);s.run(process_command);return 0;
} 运行情况 如果输入的不是命令,会提示给用户: 如果是命令,就会返回给我们运行结果: