网络编程套接字之UDP
1.基本知识介绍
1.1 IP地址和端口号
关于IP地址
IP地址是互联网上设备的唯一逻辑地址,可以唯一地确定一台网络设备。
IP地址不单单可以在网络中标识一台设备,而且还可以直到数据包的发送(路由寻址),区分不同的网络和子网。
由于现在网络设备的增多,原有的IPv4地址已经不太够用,因此新一代的IPv6正在逐渐普及。
关于端口号
端口号是设备上应用程序的唯一标识
正是有了端口号可以标识应用程序这个功能,才能让一台设备上可以运行多个程序而网络数据可以精确的送达到对应的程序。
一个端口号只能被一个进程占用
0-1023的端口是一些知名的端口,后面我们自定义时候应该尽量避开这些端口。
正是有了IP地址和端口号的存在,我们才能在浩瀚的网络中唯一确定一台主机上的一个程序来进行通信,说到底这就是一次跨主机的进程间通信。
1.2 认识UDP协议
传输层协议 无连接 不可靠传输 面向数据报
无连接通信的核心特点是:每个数据包(数据报)都独立发送,不需要预先建立连接。它不保证数据包的送达顺序、也不保证它们一定能送达。
面向数据报的核心特点:保留消息边界
维护简单
1.3 网络字节序列
Internet标准规定:网络字节序列是大端序。也就是最高有效字节排在前面(也就是低地址)。
采用这一规定是为了屏蔽底层实现的不同,不同的CPU架构字节序不一致,如果不同意,双方对同一串字节的解释不同就会导致协议不可互通。同时这也简化了协议,协议不需要为当前内容标记字节序。
h对应的是主机,n对应的是网络,l表示长整数,s表示短整数。
如果主机端是小端字节序,那么就做大小端的转换;如果是大端,那么就直接返回即可。
2. 套接字的创建和使用
2.1 套接字创建接口
socket 函数是创建这个通信端点的系统调用。它位于大多数操作系统的网络库中(如 Linux、Windows、macOS),其核心概念和参数是通用的,通常遵循 POSIX 标准。
参数:第一个参数domain指定使用那种协议族进行通信(AF_INET表示使用IPV4、AF_INET6表示使用IPV6、AF_UNIX/AF_LOCAL用于本地进程通信,使用文件名作为地址而非网络地址);第二个参数type指定套接字的通信语义,即数据传输方式。SOCK_STREAM(流式套接字 TCP协议)、SOCK_DGRAM(数据报套接字 UDP协议);第三个参数protocol表示协议类型,通常设置为0,系统会根据domain和type的组合自动选择合适的默认协议。
返回值:成功则返回一个新的套接字描述符(socket descriptor),这是一个非负整数。它和文件描述符(file descriptor)类似,在后续的操作(如连接、发送、接收、关闭)中,都通过这个描述符来引用这个套接字。失败则返回-1,并且设置相应的错误码。
注意:在代码中,你可能会看到 PF_INET 而不是 AF_INET。历史上,PF(Protocol Family)和 AF(Address Family)可能有细微差别,但在现代操作系统中,它们通常被定义为相同的值,可以互换使用。约定俗成的是,AF_xxx 用于定义地址结构,PF_xxx 用于创建套接字,但实践中用 AF_INET 即可。
2.2 套接字绑定接口
bind函数用于为一个套接字分配一个固定的“通信地址”,让其他主机知道可以通过这个地址来找到它。(IP地址+端口号)
对于服务器程序,必须调用bind手动进行绑定。服务器需要在一个固定的地址(IP+端口号)上等待客户的连接请求。如果服务器不手动固定绑定一个端口号,那么内核则会随机分配一个端口号,那么客客户则无法得知应该连接到哪一个端口号了。
对于客户程序,通常不需要手动bind,内核会自动临时生成一个端口号,这样可以防止端口号重复的问题。(例如某两个程序指定客户端口号相同从而出现问题,或是我们指定的端口号已经被使用)
参数:第一个参数sockfd为该套接字的套接字描述符,指定需要绑定地址的套接字;第二个参数addr为指向通用地址结构的指针,它实际上指向一个特定的地址结构(如 struct sockaddr_in for IPv4),但函数要求以通用类型传入以保证接口统一。程序员需要先将具体的地址结构强制转换为 struct sockaddr * 类型;第三个参数addrlen为addr所指向的地址结构的长度。
返回值:成功返回零;失败返回-1并设置错误码。
2.3 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层的网络协议,例如IPv4、IPv6、甚至UNIX Domain Socket,然而各种网络协议的格式各不同,因此设置了一个抽象的接口,需要程序员自己手动转化。
1.IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。2.IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
3.socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
2.4 地址转换函数
字符串转int_addr的函数(点分十进制转化为网络字节序列的二进制形式)
int inet_aton(const char *cp, struct in_addr *inp);
该函数是用于将点分十进制格式 的 IPv4 地址字符串转换为 网络字节序的二进制形式(即 struct in_addr)(仅支持IPv4)
参数:第一个参数const char *cp是一个输入参数,指向一个以空字符终止的字符串,该字符串包含一个点分十进制格式的 IPv4 地址;第二个参数是struct in_addr *inp: 这是一个输出参数,指向一个 struct in_addr 类型的对象。函数成功时,转换后的二进制网络字节序地址将存储在这个对象中。
返回值:非零值表示转换成功,零值表示转换失败,但errno不会被设置。
in_addr_t inet_addr(const char *cp);
net_addr 是一个传统的、现在已过时的函数,用于将 点分十进制格式 的 IPv4 地址字符串转换为 网络字节序 的 32 位二进制整数。(仅支持IPv4)
参数:const char *cp: 这是一个输入参数,指向一个以空字符终止的字符串,该字符串包含一个点分十进制格式的 IPv4 地址。
返回值:成功返回一个in_addr_t类型表示网络字节序的 IPv4 地址;失败返回INADDR_NONE。
注意:该函数有极大的缺陷,无法区分广播地址转换和转换失败,宏INADDR_NONE的值就是0xFFFFFFFF。
int inet_pton(int af, const char *src, void *dst);
inet_pton 是一个现代、安全的网络编程函数,用于将可打印的文本格式的 IP 地址字符串转换为网络字节序的二进制格式。
参数:第一个参数int af: 地址族(Address Family),指定要转换的 IP 地址类型(AF_INET表示IPv4地址,AF_INET6表示IPv6地址);第二个参数const char *src是输入参数,指向以空字符终止的字符串,包含要转换的 IP 地址文本;第三个参数void *dst是输出参数,指向存储转换结果的缓冲区。根据地址族的不同,指向不同的结构体。
返回值:成功返回1;输入的字符串不是一个有效的网络地址格式返回0;如果af不是一个合法的协议族返回-1并设置errno为EAFNOSUPPORT。
in_addr转字符串函数
char *inet_ntoa(struct in_addr in);
inet_ntoa 是一个用于将网络字节序的二进制 IPv4 地址(struct in_addr)转换回点分十进制格式字符串的函数。
参数:struct in_addr in是输入参数,包含要转换的网络字节序二进制 IPv4 地址
返回值:成功返回一个指向点分十进制字符串的指针;失败该函数没有定义失败返回值,总是返回一个有效字符串指针。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_ntop 是一个现代、安全、线程安全的网络编程函数,用于将网络字节序的二进制 IP 地址转换回可打印的文本格式。
参数:第一个参数int af是地址族(Address Family),指定要转换的 IP 地址类型。(AF_INET表示IPv4地址,AF_INET6表示IPv6地址);第二个参数const void *src是输入参数,指向包含二进制IP地址的结构体;第三个参数char *dst是输出参数,指向用户提供的缓冲区,用于存储转换后的字符串;第四个参数socklen_t size是缓冲区 dst 的大小。
返回值:成功返回一个非空指针指向dst,失败返回空指针并设置errno。
两个in_addr转字符串函数对比;因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果,但这在多线程中是存在线程安全问题的。在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。
2.5 接受数据接口
recvfrom 函数的主要作用是:从一个已连接的或未连接的套接字上接收数据,并获取数据发送源的地址。
参数:int sockfd套接字文件描述符,必须是已绑定的UDP套接字;void* buf是接收数据的缓冲区指针;size_t len是缓冲区的最大容量(字节数);int flag是接收标志位(0表示阻塞接收、MSG_DONTWAIT表示非阻塞接收,立即返回);struct sockaddr *src_addr是输出参数,用于存储发送方地址信息;socklen_t * addrlen是输入输出参数,地址结构体的长度。
返回值:成功返回实际接收到的字节数;失败返回-1并设置errno;连接关闭,对于面向连接的套接字返回0来表示对端关闭连接;无数据,在非阻塞模式下返回-1并且设置errno为EAGAIN或者EWOULDBLOCK。
我们使用上面的知识简单的编写一个服务器,稍微对服务器进行封装。
UdpServer.hpp
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include "log.hpp"Log log;
enum
{SOCKET_ERR = 1
};
const std::string DefaultIp = "0.0.0.0";
const uint16_t DefaultPort = 8080;
class UdpServer
{
public:UdpServer(uint16_t port = DefaultPort, std::string ip = DefaultIp): sockfd_(-1), ip_(ip), port_(port), is_running_(false){}void Init(){// 1.创建套接字 Udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // UDP AF_INET IPv4 SOCK_DGRAM 面向数据报套接字if (sockfd_ < 0){log.LogMessage(Fatal, "socket error! errno:%d,reason:%s", errno, strerror(errno));exit(SOCKET_ERR);}log.LogMessage(Info, "socket create success!");// 2.绑定 bindstruct sockaddr_in local;bzero(&local, sizeof(local)); // 清零local.sin_family = AF_INET;local.sin_port = htons(port_); // 主机字节序转网络字节序// local.sin_addr.s_addr = inet_addr(ip_.c_str());local.sin_addr.s_addr = INADDR_ANY; // 绑定到所有网络接口(所有网卡)if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0){log.LogMessage(Fatal, "bind error! errno:%d,reason:%s", errno, strerror(errno));exit(SOCKET_ERR);}}void Start(){log.LogMessage(Info, "UdpServer Start!");is_running_ = true;while (is_running_){// 3.收发数据 recvfrom sendtochar inbuffer[1024];struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len); // sizeof(inbuffer) - 1为\0留位置if (n < 0){log.LogMessage(Warning, "recvfrom error! errno:%d,reason:%s", errno, strerror(errno));continue;}inbuffer[n] = 0; // 字符串结尾// 打印接收到的数据std::string info = inbuffer;std::string echo_string = "Server echo:" + info;std::cout << echo_string << std::endl;// 回显sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len);}}~UdpServer(){if (sockfd_ != -1){close(sockfd_);}}
private:int sockfd_; // 网络文件描述符std::string ip_; // 服务器ipuint16_t port_; // 服务器端口号bool is_running_; // 服务器运行状态
};
Main.cpp
#include "UdpServer.hpp"
#include <memory>int main(int argc,char *argv[])
{if(argc != 2){std::cout << "Usage: ./udpserver port[1023+]" << std::endl;return -1;}uint16_t port = atoi(argv[1]);std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(port);us->Init();us->Start();return 0;
使用netstat -naup可以查看当前系统内的udp链接
问题:为什么我们换为我们自己云服务器的ip地址后就无法成功绑定呢?
云服务器禁止直接绑定公网ip!但虚拟机是可以绑定自己的ip。
当服务器将 IP 地址绑定为 0(INADDR_ANY)时,系统会监听本机所有网卡的所有 IP 地址,凡是目的端口号与绑定端口一致的数据包,均会被正确交付给应用层,从而避免多网卡情况下信息接收不全的问题。
一般绑定端口时候,0~1023都是系统内定的端口号,一般都需要固定的应用层协议使用,http:80,https:443,mysql:3306
如果端口被使用,再次绑定就会出现permission denied!
问题:客户端需要bind吗?为什么?客户端是如何进行bind的?
客户端是需要bind的,但是可以不用我们去显示bind,可以由操作系统自由选择。为消除客户端端口号冲突,建议交由操作系统动态分配(服务器端通常不受此限制)。客户端的具体端口值并不关键,只需确保在主机内唯一;相对地,服务器端口需明确、固定,便于客户端连接。
问题:客户端是什么时候bind的呢?
对于UDP来说,第一次调用sendto发送数据时候,内核会自动绑定一个本地IP+临时端口号;如果需要客户端先接受数据再发送,通常就需要显示bind到某个本地端口以便内核将数据投递给该socket。
服务器测试:
127.0.0.1是一个本地环回的地址,它允许一台计算机上的网络应用和同一台计算机上运行的其他网络应用程序进行通信。它是一个内部的,封闭的网络环路。数据从这个接口发送出去又立刻回到同一台机器,不会离开物理网卡、进入真实网络。
UdpClient.cpp
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <cstring>int main(int argc, char *argv[])
{if (argc != 3){std::cout << "./udpclient ServerIp ServerPort" << std::endl;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cout << "socket error!" << std::endl;return -1;}sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t len = sizeof(server);std::string message;char buffer[1024];while (true){std::cout << "Please Enter# ";std::getline(std::cin, message);// 向服务器发送数据sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);sockaddr_in temp;socklen_t temp_len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&temp, &temp_len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}close(sockfd);return 0;
}
3.UDP运用
3.1 使用udp完成一个简单的远端命令行
我们在原始的Start函数基础上加上回调函数来调用handler处理命令。(其他的不变)
void Start(Handler handler){log.LogMessage(Info, "UdpServer Start!");is_running_ = true;while (is_running_){// 3.收发数据 recvfrom sendtochar inbuffer[1024];struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len); // sizeof(inbuffer) - 1为\0留位置if (n < 0){log.LogMessage(Warning, "recvfrom error! errno:%d,reason:%s", errno, strerror(errno));continue;}inbuffer[n] = 0; // 字符串结尾// 打印接收到的数据std::string info = inbuffer;std::string echo_string = handler(info);//std::string echo_string = "Server echo:" + info;std::cout << echo_string << std::endl;// 回显sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len);}}
std::string ExcuteCommand(const std::string& cmd)
{FILE *fp = popen(cmd.c_str(), "r");if (fp == nullptr){return "popen error!";}char buffer[4096];std::string result;while (true){char* isread = fgets(buffer, sizeof(buffer)-1, fp);if (isread == nullptr){break;}result += buffer;}pclose(fp);return result;
}
函数popen介绍
FILE *popen(const char *command, const char *type);
popen 是一个用于创建进程管道的标准库函数,它允许在程序中启动另一个进程,并与之建立单向的输入/输出通道。
参数:第一个参数是const char *command指的是要执行的shell命令字符串;第二个参数是const char *type是管道类型,指定数据流的方向(r表示读取,w表示写入)。
返回值:成功返回一个指向FILE结构体的指针,可以像普通文件一样使用标准I/O函数;失败返回NULL并设置errno。
这样客户端发送的命令就能被服务端执行并被从新写回给客户端。
3.2 制作一个聊天室,可以看到自己和大家发送的消息,并且可以能看到有人上线
为了区分发送消息和打印其他人的消息,因此我们采用两个终端,一个打印一个发送消息。/dev/pts下的终端编号可以向任意终端打印。
UdpServer.hpp
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <functional>
#include <unordered_map>
#include "log.hpp"Log log;enum
{SOCKET_ERR = 1
};const std::string DefaultIp = "0.0.0.0";
const uint16_t DefaultPort = 8080;typedef std::function<std::string(const std::string &, const std::string &, const uint16_t)> func_t;
using Handler = std::function<std::string(const std::string &)>;class UdpServer
{
public:UdpServer(uint16_t port = DefaultPort, std::string ip = DefaultIp): sockfd_(-1), ip_(ip), port_(port), is_running_(false){}void Init(){// 1.创建套接字 Udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // UDP AF_INET IPv4 SOCK_DGRAM 面向数据报套接字if (sockfd_ < 0){log.LogMessage(Fatal, "socket error! errno:%d,reason:%s", errno, strerror(errno));exit(SOCKET_ERR);}log.LogMessage(Info, "socket create success!");// 2.绑定 bindstruct sockaddr_in local;bzero(&local, sizeof(local)); // 清零local.sin_family = AF_INET;local.sin_port = htons(port_); // 主机字节序转网络字节序// local.sin_addr.s_addr = inet_addr(ip_.c_str());local.sin_addr.s_addr = INADDR_ANY; // 绑定到所有网络接口(所有网卡)if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0){log.LogMessage(Fatal, "bind error! errno:%d,reason:%s", errno, strerror(errno));exit(SOCKET_ERR);}}void CheckUser(const struct sockaddr_in &client, const std::string &client_ip, const uint16_t client_port){//printf("CheckUser\n");auto findres = user_map_.find(client_ip);if (findres == user_map_.end()){// 说明用户不在user_map_中user_map_.insert({client_ip, client});// 服务端打印用户上线信息printf("User Online! [ip:%s:port:%d]\n", client_ip.c_str(), client_port);sendto(sockfd_, "Welcome to chat room!", 22, 0, (struct sockaddr *)&client, sizeof(client));}}void BrocastMessage(const std::string &info, const std::string &client_ip, const uint16_t client_port){//printf("BrocastMessage\n");std::string message = "[" + client_ip + ":" + std::to_string(client_port) + "]" + "#" + info;for (const auto &[user_ip, user_addr] : user_map_){// if (user_ip == client_ip)// {// continue; // 不给自己发// }sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr *)&user_addr, sizeof(user_addr));}std::cout << message << std::endl;}void Start(){log.LogMessage(Info, "UdpServer Start!");is_running_ = true;while (is_running_){// 3.收发数据 recvfrom sendtochar inbuffer[1024];struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len); // sizeof(inbuffer) - 1为\0留位置if (n < 0){log.LogMessage(Warning, "recvfrom error! errno:%d,reason:%s", errno, strerror(errno));continue;}inbuffer[n] = 0; // 字符串结尾// 分析客户端的信息std::string client_ip = inet_ntoa(client.sin_addr);uint16_t client_port = ntohs(client.sin_port);// 1.查看用户是否在user_map_中CheckUser(client, client_ip, client_port);// 2.向所有用户广播信息std::string info = inbuffer;BrocastMessage(info, client_ip, client_port);}}~UdpServer(){if (sockfd_ != -1){close(sockfd_);}}private:int sockfd_; // 网络文件描述符std::string ip_; // 服务器ipuint16_t port_; // 服务器端口号bool is_running_; // 服务器运行状态std::unordered_map<std::string, struct sockaddr_in> user_map_; // 用户信息
};
Terminal.hpp
#include <iostream>
#include <unistd.h>
#include <string>
#include <fcntl.h>
using namespace std;const string terminal = "/dev/pts/7";void Terminal()
{int fd = open(terminal.c_str(), O_WRONLY);if (fd < 0){cerr << "open " << terminal << " error!" << endl;return;}// 重定向标准输出dup2(fd,1);close(fd);
}
#include "UdpServer.hpp"
#include <iostream>
#include <string>
#include <memory>std::string ReciveMessage(const std::string& cmd)
{const std::string res = "Server revice a message: " + cmd;return res;
}int main(int argc,char *argv[])
{if(argc != 2){std::cout << "Usage: ./udpserver port[1023+]" << std::endl;return -1;}uint16_t port = atoi(argv[1]);std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(port);us->Init();us->Start();return 0;
}
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <cstring>
#include "Terminal.hpp"struct ThreadData
{int sockfd;struct sockaddr_in server;string server_ip;
};
void *RecvMessage(void *arg)
{Terminal();ThreadData *td = static_cast<ThreadData *>(arg);int sockfd = td->sockfd;char buffer[1024];while (true){sockaddr_in temp;socklen_t temp_len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&temp, &temp_len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}return nullptr;
}
void *SendMessage(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);std::string message;socklen_t len = sizeof(td->server);//std::string welcome_msg = "Welcome to Server: " + td->server_ip;//std::cout << welcome_msg << std::endl;while (true){std::cerr << "Please Enter# ";std::getline(std::cin, message);// 向服务器发送数据sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);}return nullptr;
}
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "./udpclient ServerIp ServerPort" << std::endl;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cout << "socket error!" << std::endl;return -1;}ThreadData td;td.sockfd = sockfd;td.server_ip = ip;bzero(&td.server, sizeof(td.server));td.server.sin_family = AF_INET;td.server.sin_port = htons(port);td.server.sin_addr.s_addr = inet_addr(ip.c_str());pthread_t recv_tid, send_tid;pthread_create(&recv_tid, nullptr, RecvMessage, (void*)&td);pthread_create(&send_tid, nullptr, SendMessage, (void*)&td);pthread_join(recv_tid, nullptr);pthread_join(send_tid, nullptr);close(sockfd);return 0;
}
最后实现的效果如下: