Linux网络编程之UDP
文章目录
- Linux网络编程之UDP
- 1、端口号
- 2、端口号和进程ID的区别
- 3、重新认识网络通讯过程
- 4、UDP协议的简单认识
- 5、网络字节序
- 6、socket编程接口
- 6.1、socket常见接口
- 6.2、sockaddr通用地址结构
- 7、简单的UDP网络程序
- 7.1、服务器响应程序
- 7.2、服务器执行命令行
- 7.3、服务器英语单词字典
- 7.4、服务器聊天室
- 8、补充内容
Linux网络编程之UDP
1、端口号
端口号是计算机网络中用来标识特定应用程序或服务的数字标识符。在网络通信中,每个数据包都包含一个目标端口号和源端口号,这样可以确保数据包能够正确地路由到目标应用程序或服务。
端口号是一个16位的数字,范围从0到65535。
用于在传输层(通常是TCP或UDP协议)标识特定的应用程序或服务。
允许同一台计算机上的多个应用程序同时进行网络通信,每个应用程序使用不同的端口号。
在目标设备上,操作系统通过端口号将数据包传递给正确的应用程序或服务。
IP地址 + 端口号 = 套接字,能够标识网络上的某一台主机的某一个进程。
端口号使得计算机网络中的不同应用程序能够通过网络进行可靠的通信和数据交换,同时确保数据能够安全地到达目标应用程序。
一个端口号只能被一个进程占用。
2、端口号和进程ID的区别
用途不同:端口号用于标识网络中的应用程序或服务,而进程ID用于操作系统内部管理和标识正在运行的进程。
作用范围:端口号主要在网络通信中使用,进程ID主要在操作系统内部使用。
分配方式:端口号是在应用程序设计或网络配置时指定的,而进程ID是由操作系统动态分配给每个新创建的进程。
如果端口号和进程ID合并,那么系统的耦合度会增加,不符合低耦合的编程思想。
另外,一个进程可以有多个端口号,但是一个端口号不能被多个进程使用。
3、重新认识网络通讯过程
我们上网,无非就是两种动作:a. 把远端数据拉取到本地 b. 把数据发送到远端。
大部分的网络通信行为,都是用户触发的,在计算机中,用户就是进程!
把数据发送到目标主机,不是目的,而是手段。真正的目的,是把数据交给目的主机上的某个服务(进程)。
网络通信的本质,其实就是进程在帮我们进行网络通信(进程间通信),无论是对于客户端还是服务器。
IP地址 + 端口号 = 套接字,能够标识网络上的某一台主机的某一个进程。
client --> server : client进程 – > server进程
client ip + client port = client进程
server ip + server port = server进程
在网络中都能唯一找到彼此
4、UDP协议的简单认识
传输层协议
无连接
不可靠传输
面向数据报
5、网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。 那么如何定义网络数据流的地址呢?
答:网络数据流的地址按大端来定义的。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
助记:h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
IP是32位,端口号是16位,因此IP转网络字节序是使用带l的,端口号是使用带s的
6、socket编程接口
6.1、socket常见接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器) int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 开始监听socket (TCP, 服务器) int listen(int socket, int backlog);// 接收请求 (TCP, 服务器) int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端) int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
我们可以看到,上述接口中基本都使用到了sockaddr结构体,下面我们想象解释一下。
6.2、sockaddr通用地址结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket,然而。各种网络协议的地址格式并不相同。sockaddr通用地址结构就是为了统一接口,在不同的操作系统下也可以正常使用(强转)。
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址.
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针作为参数。
sockaddr通用地址结构:
/* Structure describing a generic socket address. */ struct sockaddr{__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */};
sockaddr_in结构:
/* Structure describing an Internet socket address. */ struct sockaddr_in{__SOCKADDR_COMMON (sin_); // 地址类型in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr) // 8字节填充- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型,端口号,IP地址。
in_addr结构:
/* Internet address. */ typedef uint32_t in_addr_t; struct in_addr{in_addr_t s_addr;};
in_addr用来表示一个IPv4的IP地址。其实就是一个32位的无符号整数。
7、简单的UDP网络程序
7.1、服务器响应程序
主要功能是客户端给服务器发送消息,服务器收到消息进行响应(把收到的消息发送给客户端)。
用到的几个接口:接收和发送网络数据:
#include <sys/types.h> #include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);参数:buf -- 输出型参数,即存接收到的数据len -- 期望收到的数据长度flag -- 默认为0src_addr -- 输出型参数,存发送数据的对象addrlen -- 输入输出型参数,存src_addr的大小ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);参数:buf -- 输入型参数,即要存发送的数据len -- 发送数据的长度flag -- 默认为0src_addr -- 输出型参数,存发送数据的对象addrlen -- 输入输出型参数,存src_addr的大小
字节序列转换:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>// 将字符串类型的IP转换为无符号整数类型IP并转换为网络字节序列 in_addr_t inet_addr(const char *cp); // 从网络字节序列中将无符号整数类型IP转换为字符串类型的IP char *inet_ntoa(struct in_addr in);
接下来就是代码文件了。
InetAddr.hpp
文件#pragma once#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string>class InetAddr {void GetAddress(std::string *ip, uint16_t *port){// char *inet_ntoa(struct in_addr in);*ip = inet_ntoa(_addr.sin_addr);*port = ntohs(_addr.sin_port);}public:InetAddr(const struct sockaddr_in &addr) : _addr(addr){GetAddress(&_ip, &_port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port; };
LockGuard.hpp
文件#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex); // 析构解锁}private:pthread_mutex_t *_mutex; };
Log.hpp
文件#pragma once#include <string> #include <iostream> #include <fstream> #include <unistd.h> #include <stdarg.h> #include <sys/types.h> #include "LockGuard.hpp"bool isSave = false; // 默认向显示器打印 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; #define FILEPATH "./log.txt"enum level {DEBUG = 0,INFO,WARNING,ERROR,FATAL };void SaveToFile(const string &message) {ofstream out(FILEPATH, ios_base::app);if (!out.is_open())return;out << message;out.close(); }std::string LevelToString(int level) {switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknow";} }std::string GetTimeString() {time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char buff[1024];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return buff; }void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...) {std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t pid = getpid();char buff[1024];va_list arg;// int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数va_start(arg, format);vsnprintf(buff, sizeof(buff), format, arg);va_end(arg);LockGuard lock(&mutex);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n';if (issave == false)std::cout << message;elseSaveToFile(message); }// 固定文件名和行数 #define LOG(level, format, ...) \do \{ \LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableScreen() \do \{ \isSave = false; \} while (0)#define EnableFile() \do \{ \isSave = true; \} while (0)void Test(int num, ...) {va_list arg;va_start(arg, num);while (num--){int data = va_arg(arg, int);std::cout << data << " ";}std::cout << std::endl;va_end(arg); }
Main.cc
文件#include <iostream> #include <memory> #include "UdpServer.hpp"void Usage() {// printf("./udp_server serverip serverport\n");printf("Usage : ./udp_server serverport\n"); // ip 已经设置为0 }int main(int argc, char *argv[]) {// if (argc != 3)if (argc != 2){Usage();exit(USAGE_ERROR);}// std::string ip = argv[1];// uint16_t port = std::stoi(argv[2]);uint16_t port = std::stoi(argv[1]);// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->InitServer();usvr->Start();return 0; }
UdpClient.cc
文件#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h>enum errorcode {USAGE_ERROR = 1, };void Usage() {printf("Usage : ./udp_client serverip serverport\n"); }int main(int argc, char *argv[]) {if (argc != 3){Usage();exit(USAGE_ERROR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;}// 2. client要不要bind?一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!// a. 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind --- 为什么?防止client port冲突。要bind,必然要和port关联!// b. 什么时候bind呢?首次发送数据的时候struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);server.sin_addr.s_addr = inet_addr(serverip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrstd::string message;// 直接通信即可,已经自动绑定while (true){std::cout << "Please Enter:# ";std::getline(std::cin, message);// 发送数据// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrsendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));// 获取回应数据char buff[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrssize_t n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;std::cout << "Server Echo:# " << buff << std::endl;}} }
UdpServer.cc
文件#pragma once#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>#include <string.h> #include <error.h>using namespace std;#include "Log.hpp" #include "InetAddr.hpp"const int Defaultfd = -1;enum errorcode {CREATE_ERROR = 1,BIND_ERROR,USAGE_ERROR };class UdpServer { public:// UdpServer(std::string ip, uint16_t port) : _sockfd(Defaultfd), _ip(ip), _port(port), _isrunning(false)UdpServer(uint16_t port) : _sockfd(Defaultfd), _port(port), _isrunning(false){}void InitServer(){// 1. 创建udp套接字 -- 必须要做的_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp通信if (_sockfd < 0){LOG(FATAL, "socke create error ! error string %s error %d", strerror(errno), errno);exit(CREATE_ERROR);}// 创建socket成功LOG(INFO, "create socket success ! sockfd:", _sockfd);// 2.0. 填充sockaddr_in结构struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrlocal.sin_addr.s_addr = INADDR_ANY; // 0 -- 链接当前服务器的所有ip都接受// 2.1. 绑定网络信息int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "socke bind error ! error string %s error %d", strerror(errno), errno);exit(BIND_ERROR);}// 绑定成功LOG(INFO, "bind socket success !");}void Start(){// 一直运行,直到管理者不想运行了, 服务器都是死循环// UDP是面向数据报的协议_isrunning = true;while (true){// 获取数据// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 既是输入(必须初始化) ,也是输出(peer的实际长度)char buff[1024];ssize_t n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;// 回答发送方// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrInetAddr addr(peer);LOG(DEBUG, "get message , sender:[%s:%d] , content: %s", addr.Ip().c_str(), addr.Port(), buff);sendto(_sockfd, buff, strlen(buff), 0, (struct sockaddr *)&peer, len);}}_isrunning = false;}~UdpServer(){}private:int _sockfd;// std::string _ip; // 暂时先这样写 -- 不需要uint16_t _port;bool _isrunning; };
Makefile
文件.PHONY:all all:udp_client udp_serverudp_client:UdpClient.ccg++ -o $@ $^ -std=c++14 udp_server:Main.ccg++ -o $@ $^ -std=c++14.PHONY:clean clean:rm -f udp_server udp_client
运行结果:
7.2、服务器执行命令行
主要是客户端发送命令给服务器,服务器执行命令后返回给客户端。
InetAddr.hpp
文件#pragma once#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string>class InetAddr {void GetAddress(std::string *ip, uint16_t *port){// char *inet_ntoa(struct in_addr in);*ip = inet_ntoa(_addr.sin_addr);*port = ntohs(_addr.sin_port);}public:InetAddr(const struct sockaddr_in &addr) : _addr(addr){GetAddress(&_ip, &_port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port; };
LockGuard.hpp
文件#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex); // 析构解锁}private:pthread_mutex_t *_mutex; };
Log.hpp
文件#pragma once#include <string> #include <iostream> #include <fstream> #include <unistd.h> #include <stdarg.h> #include <sys/types.h> #include "LockGuard.hpp"bool isSave = false; // 默认向显示器打印 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; #define FILEPATH "./log.txt"enum level {DEBUG = 0,INFO,WARNING,ERROR,FATAL };void SaveToFile(const string &message) {ofstream out(FILEPATH, ios_base::app);if (!out.is_open())return;out << message;out.close(); }std::string LevelToString(int level) {switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknow";} }std::string GetTimeString() {time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char buff[1024];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return buff; }void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...) {std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t pid = getpid();char buff[1024];va_list arg;// int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数va_start(arg, format);vsnprintf(buff, sizeof(buff), format, arg);va_end(arg);LockGuard lock(&mutex);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n';if (issave == false)std::cout << message;elseSaveToFile(message); }// 固定文件名和行数 #define LOG(level, format, ...) \do \{ \LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableScreen() \do \{ \isSave = false; \} while (0)#define EnableFile() \do \{ \isSave = true; \} while (0)void Test(int num, ...) {va_list arg;va_start(arg, num);while (num--){int data = va_arg(arg, int);std::cout << data << " ";}std::cout << std::endl;va_end(arg); }
Main.cc
文件#include <iostream> #include <memory> #include <stdio.h> #include <vector> #include "UdpServer.hpp"void Usage() {// printf("./udp_server serverip serverport\n");printf("Usage : ./udp_server serverport\n"); // ip 已经设置为0 }std::string OnMessage(std::string request) {return request + " got you!"; }bool CheckCommand(std::string command) {vector<string> cmd = {"kill","rm","dd","top","reboot","shutdown","mv","cp","halt","unlink","exit","chmod"};for (auto &e : cmd){if (command.find(e) != std::string::npos)return false;}return true; }std::string OnCommand(std::string command) {if (!CheckCommand(command)){return "bad man!";}// FILE *popen(const char *command, const char *type);FILE *pp = popen(command.c_str(), "r");if (!pp){return "popen error!";}std::string response;char buff[1024];while (true){// char *fgets(char *s, int size, FILE *stream);char *s = fgets(buff, sizeof(buff), pp);if (!s)break;elseresponse += buff;}pclose(pp);return response.empty() ? "not command" : response; }int main(int argc, char *argv[]) {// if (argc != 3)if (argc != 2){Usage();exit(USAGE_ERROR);}// std::string ip = argv[1];// uint16_t port = std::stoi(argv[2]);uint16_t port = std::stoi(argv[1]);// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(OnCommand, port);usvr->InitServer();usvr->Start();return 0; }
Makefile
文件.PHONY:all all:udp_client udp_serverudp_client:UdpClient.ccg++ -o $@ $^ -std=c++14 udp_server:Main.ccg++ -o $@ $^ -std=c++14.PHONY:clean clean:rm -f udp_server udp_client
UdpClient.cc
文件#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h>enum errorcode {USAGE_ERROR = 1, };void Usage() {printf("Usage : ./udp_client serverip serverport\n"); }int main(int argc, char *argv[]) {if (argc != 3){Usage();exit(USAGE_ERROR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;}// 2. client要不要bind?一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!// a. 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind --- 为什么?防止client port冲突。要bind,必然要和port关联!// b. 什么时候bind呢?首次发送数据的时候struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);server.sin_addr.s_addr = inet_addr(serverip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrstd::string message;// 直接通信即可,已经自动绑定while (true){std::cout << "Please Enter:# ";std::getline(std::cin, message);// 发送数据// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrsendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));// 获取回应数据char buff[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrssize_t n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;std::cout << "Server Echo:# " << buff << std::endl;}} }
UdpServer.hpp
文件#pragma once#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>#include <string.h> #include <error.h> #include <functional>using namespace std;#include "Log.hpp" #include "InetAddr.hpp"using task_t = function<std::string(std::string)>;const int Defaultfd = -1;enum errorcode {CREATE_ERROR = 1,BIND_ERROR,USAGE_ERROR };class UdpServer { public:// UdpServer(std::string ip, uint16_t port) : _sockfd(Defaultfd), _ip(ip), _port(port), _isrunning(false)UdpServer(task_t OnMessage, uint16_t port) : _sockfd(Defaultfd), _port(port), _isrunning(false), _OnMessage(OnMessage){}void InitServer(){// 1. 创建udp套接字 -- 必须要做的_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp通信if (_sockfd < 0){LOG(FATAL, "socke create error ! error string %s error %d", strerror(errno), errno);exit(CREATE_ERROR);}// 创建socket成功LOG(INFO, "create socket success ! sockfd:", _sockfd);// 2.0. 填充sockaddr_in结构struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrlocal.sin_addr.s_addr = INADDR_ANY; // 0 -- 链接当前服务器的所有ip都接受// 2.1. 绑定网络信息int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "socke bind error ! error string %s error %d", strerror(errno), errno);exit(BIND_ERROR);}// 绑定成功LOG(INFO, "bind socket success !");}void Start(){// 一直运行,直到管理者不想运行了, 服务器都是死循环// UDP是面向数据报的协议_isrunning = true;while (true){// 获取数据// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 既是输入(必须初始化) ,也是输出(peer的实际长度)char buff[1024];ssize_t n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;// 回答发送方// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrInetAddr addr(peer);std::string response = _OnMessage(buff); // 处理收到的消息LOG(DEBUG, "get message:%s", buff);sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&peer, len);}}_isrunning = false;}~UdpServer(){}private:int _sockfd;// std::string _ip; // 暂时先这样写 -- 不需要uint16_t _port;bool _isrunning;task_t _OnMessage; };
- 运行结果:
7.3、服务器英语单词字典
即客户端输入英文单词,服务器返回中文意思。
Dict.hpp
文件#pragma once#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string> #include <fstream> #include <unordered_map>#include "Log.hpp"const std::string dict_path = "./dict.txt"; const std::string sep = ": ";class Dict { private:bool Load(){std::ifstream in(_file_path);if (!in.is_open()){LOG(FATAL, "open %s error", _file_path.c_str());return false;}std::string line;while (std::getline(in, line)){auto pos = line.find(sep);if (pos == std::string::npos)continue;std::string english = line.substr(0, pos);std::string chinese = line.substr(pos + sep.size()); // abc: cc -- pos:3LOG(DEBUG, "add %s : %s success", english.c_str(), chinese.c_str());_dict[english] = chinese;}LOG(DEBUG, "Load %s success", _file_path.c_str());return true;}public:Dict(const std::string file_path = dict_path) : _file_path(file_path){Load(); // 加载文件到内存}std::string Translate(std::string &str, bool *ok){*ok = false;for (auto &e : _dict){if (e.first == str){*ok = true;return e.second;}}return "未找到";}~Dict() {}private:const std::string _file_path;std::unordered_map<std::string, std::string> _dict; };
dict.txt
文件apple: 苹果 banana: 香蕉 cat: 猫 dog: 狗 elephant: 大象 fish: 鱼 grape: 葡萄 house: 房子 ice: 冰 juice: 果汁 kite: 风筝 lion: 狮子 monkey: 猴子 night: 夜晚 orange: 橙子 piano: 钢琴 queen: 女王 rabbit: 兔子 sun: 太阳 tree: 树 umbrella: 雨伞 violin: 小提琴 water: 水 xylophone: 木琴 yogurt: 酸奶 zebra: 斑马 book: 书 chair: 椅子 desk: 桌子 ear: 耳朵 flower: 花 glove: 手套 hat: 帽子 island: 岛 jacket: 夹克 key: 钥匙 lamp: 灯 mountain: 山 notebook: 笔记本 ocean: 海洋 pencil: 铅笔 queen: 女王 river: 河流 shoe: 鞋子 telephone: 电话 umbrella: 雨伞 vase: 花瓶 window: 窗户 yard: 院子 zoo: 动物园 ant: 蚂蚁 bird: 鸟 cloud: 云 door: 门 egg: 鸡蛋 frog: 青蛙 guitar: 吉他 horse: 马 ink: 墨水 jelly: 果冻 king: 国王 leaf: 叶子 moon: 月亮 nest: 鸟巢 octopus: 章鱼 pen: 钢笔 quilt: 被子 rain: 雨 star: 星星 turtle: 乌龟 vulture: 秃鹫 whale: 鲸鱼 x-ray: X光 yo-yo: 溜溜球 airplane: 飞机 beach: 海滩 car: 汽车 diamond: 钻石 eagle: 老鹰 forest: 森林 gold: 黄金 hill: 小山 igloo: 冰屋 jungle: 丛林 kangaroo: 袋鼠 lake: 湖泊 mango: 芒果 nest: 鸟巢 owl: 猫头鹰 pizza: 披萨 queen: 女王 road: 道路 ship: 船 train: 火车 volcano: 火山 window: 窗户
InetAddr.hpp
文件#pragma once#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string>class InetAddr {void GetAddress(std::string *ip, uint16_t *port){// char *inet_ntoa(struct in_addr in);*ip = inet_ntoa(_addr.sin_addr);*port = ntohs(_addr.sin_port);}public:InetAddr(const struct sockaddr_in &addr) : _addr(addr){GetAddress(&_ip, &_port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port; };
LockGuard.hpp
文件#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex); // 析构解锁}private:pthread_mutex_t *_mutex; };
Log.hpp
文件#pragma once#include <string> #include <iostream> #include <fstream> #include <unistd.h> #include <stdarg.h> #include <sys/types.h> #include "LockGuard.hpp"bool isSave = false; // 默认向显示器打印 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; #define FILEPATH "./log.txt"enum level {DEBUG = 0,INFO,WARNING,ERROR,FATAL };void SaveToFile(const string &message) {ofstream out(FILEPATH, ios_base::app);if (!out.is_open())return;out << message;out.close(); }std::string LevelToString(int level) {switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknow";} }std::string GetTimeString() {time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char buff[1024];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return buff; }void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...) {std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t pid = getpid();char buff[1024];va_list arg;// int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数va_start(arg, format);vsnprintf(buff, sizeof(buff), format, arg);va_end(arg);LockGuard lock(&mutex);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n';if (issave == false)std::cout << message;elseSaveToFile(message); }// 固定文件名和行数 #define LOG(level, format, ...) \do \{ \LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableScreen() \do \{ \isSave = false; \} while (0)#define EnableFile() \do \{ \isSave = true; \} while (0)void Test(int num, ...) {va_list arg;va_start(arg, num);while (num--){int data = va_arg(arg, int);std::cout << data << " ";}std::cout << std::endl;va_end(arg); }
Main.cc
文件#include <iostream> #include <memory> #include "UdpServer.hpp"void Usage() {// printf("./udp_server serverip serverport\n");printf("Usage : ./udp_server serverport\n"); // ip 已经设置为0 }int main(int argc, char *argv[]) {// if (argc != 3)if (argc != 2){Usage();exit(USAGE_ERROR);}// std::string ip = argv[1];// uint16_t port = std::stoi(argv[2]);uint16_t port = std::stoi(argv[1]);// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->InitServer();usvr->Start();return 0; }
Makefile
文件.PHONY:all all:udp_client udp_serverudp_client:UdpClient.ccg++ -o $@ $^ -std=c++14 udp_server:Main.ccg++ -o $@ $^ -std=c++14.PHONY:clean clean:rm -f udp_server udp_client
UdpClient.cc
文件#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h>enum errorcode {USAGE_ERROR = 1, };void Usage() {printf("Usage : ./udp_client serverip serverport\n"); }int main(int argc, char *argv[]) {if (argc != 3){Usage();exit(USAGE_ERROR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;}// 2. client要不要bind?一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!// a. 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind --- 为什么?防止client port冲突。要bind,必然要和port关联!// b. 什么时候bind呢?首次发送数据的时候struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);server.sin_addr.s_addr = inet_addr(serverip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrstd::string message;// 直接通信即可,已经自动绑定while (true){std::cout << "Please Enter:# ";std::getline(std::cin, message);// 发送数据// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrsendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));// 获取回应数据char buff[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrssize_t n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;std::cout << "Server Echo:# " << buff << std::endl;}} }
UdpServer.hpp
文件#pragma once#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>#include <string.h> #include <error.h> #include <functional>using namespace std;#include "Log.hpp" #include "InetAddr.hpp" #include "Dict.hpp"const int Defaultfd = -1;enum errorcode {CREATE_ERROR = 1,BIND_ERROR,USAGE_ERROR };class UdpServer { public:// UdpServer(std::string ip, uint16_t port) : _sockfd(Defaultfd), _ip(ip), _port(port), _isrunning(false)UdpServer(uint16_t port) : _sockfd(Defaultfd), _port(port), _isrunning(false){}void InitServer(){// 1. 创建udp套接字 -- 必须要做的_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp通信if (_sockfd < 0){LOG(FATAL, "socke create error ! error string %s error %d", strerror(errno), errno);exit(CREATE_ERROR);}// 创建socket成功LOG(INFO, "create socket success ! sockfd: %d", _sockfd);// 2.0. 填充sockaddr_in结构struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrlocal.sin_addr.s_addr = INADDR_ANY; // 0 -- 链接当前服务器的所有ip都接受// 2.1. 绑定网络信息int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "socke bind error ! error string %s error %d", strerror(errno), errno);exit(BIND_ERROR);}// 绑定成功LOG(INFO, "bind socket success !");}void Start(){// 一直运行,直到管理者不想运行了, 服务器都是死循环// UDP是面向数据报的协议_isrunning = true;while (true){// 获取数据// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 既是输入(必须初始化) ,也是输出(peer的实际长度)char buff[1024];ssize_t n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;// 回答发送方// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrInetAddr addr(peer);Dict dict;std::string message = buff;auto translate = std::bind(&Dict::Translate, Dict(), placeholders::_1, placeholders::_2);bool flag;std::string response = translate(message, &flag);if(!flag) {// 没找到response = "单词没找到";}LOG(DEBUG, "get message , sender:[%s:%d] , content: %s", addr.Ip().c_str(), addr.Port(), buff);sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&peer, len);}}_isrunning = false;}~UdpServer(){}private:int _sockfd;// std::string _ip; // 暂时先这样写 -- 不需要uint16_t _port;bool _isrunning; };
- 运行结果:
7.4、服务器聊天室
可以多人聊天的聊天室,一人发消息,在聊天室的其他人都可以收到消息内容。
InetAddr.hpp
文件#pragma once#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string>class InetAddr {void GetAddress(std::string *ip, uint16_t *port){// char *inet_ntoa(struct in_addr in);*ip = inet_ntoa(_addr.sin_addr);*port = ntohs(_addr.sin_port);}public:InetAddr(const struct sockaddr_in &addr) : _addr(addr){GetAddress(&_ip, &_port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}bool operator==(InetAddr &addr){return _ip == addr.Ip() && _port == addr.Port();}const struct sockaddr_in& GetAddr(){return _addr;}~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port; };
LockGuard.hpp
文件# pragma once#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex); // 析构解锁}private:pthread_mutex_t *_mutex; };
Log.hpp
文件#pragma once#include <string> #include <iostream> #include <fstream> #include <unistd.h> #include <stdarg.h> #include <sys/types.h> #include "LockGuard.hpp"bool isSave = false; // 默认向显示器打印 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; #define FILEPATH "./log.txt"enum level {DEBUG = 0,INFO,WARNING,ERROR,FATAL };void SaveToFile(const string &message) {ofstream out(FILEPATH, ios_base::app);if (!out.is_open())return;out << message;out.close(); }std::string LevelToString(int level) {switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknow";} }std::string GetTimeString() {time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char buff[1024];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return buff; }void LogMessage(const std::string filename, int line, bool issave, int level, const char *format, ...) {std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t pid = getpid();char buff[1024];va_list arg;// int vsnprintf(char *str, size_t size, const char *format, va_list ap); // 使用可变参数va_start(arg, format);vsnprintf(buff, sizeof(buff), format, arg);va_end(arg);LockGuard lock(&mutex);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" + "[pid:" + std::to_string(pid) + "]" + "[" + filename + "]" + "[" + std::to_string(line) + "] " + buff + '\n';if (issave == false)std::cout << message;elseSaveToFile(message); }// 固定文件名和行数 #define LOG(level, format, ...) \do \{ \LogMessage(__FILE__, __LINE__, isSave, level, format, ##__VA_ARGS__); \} while (0)#define EnableScreen() \do \{ \isSave = false; \} while (0)#define EnableFile() \do \{ \isSave = true; \} while (0)void Test(int num, ...) {va_list arg;va_start(arg, num);while (num--){int data = va_arg(arg, int);std::cout << data << " ";}std::cout << std::endl;va_end(arg); }
Main.cc
文件#include <iostream> #include <memory>#include "UdpServer.hpp"void Usage() {// printf("./udp_server serverip serverport\n");printf("Usage : ./udp_server serverport\n"); // ip 已经设置为0 }int main(int argc, char *argv[]) {// if (argc != 3)if (argc != 2){Usage();exit(USAGE_ERROR);}// std::string ip = argv[1];// uint16_t port = std::stoi(argv[2]);uint16_t port = std::stoi(argv[1]);// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->InitServer();usvr->Start();return 0; }
Makefile
文件.PHONY:all all:udp_client udp_serverudp_client:UdpClient.ccg++ -o $@ $^ -std=c++14 -lpthread udp_server:Main.ccg++ -o $@ $^ -std=c++14 -lpthread.PHONY:clean clean:rm -f udp_server udp_client
Thread.hpp
文件#ifndef __THREAD_HPP__ #define __THREAD_HPP__#include <iostream> #include <string> #include <unistd.h> #include <functional> #include <pthread.h>using namespace std;// 封装Linux线程 namespace ThreadModule {using func_t = function<void(string &)>;class Thread{public:// /* ThreadData* */Thread(func_t<T> func, T data, const string& name = "default name") : _func(func), _data(data), _threadname(name), _stop(true) {}Thread(func_t func, const string &name = "default name") : _func(func), _threadname(name), _stop(true) {}void Execute(){_func(_threadname);// _func(_data);}// 隐含thisstatic void *threadroutine(void *arg){Thread *self = static_cast<Thread *>(arg);self->Execute(); // static 访问不了成员变量return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if (!n){_stop = false;return true;}else{return false;}}void Detach(){if (!_stop){pthread_detach(_tid);}}void Join(){if (!_stop){pthread_join(_tid, nullptr);}}string name(){return _threadname;}void Stop(){_stop = true;}// ~Thread() {}private:pthread_t _tid;string _threadname;func_t _func;bool _stop;};} // namespace ThreadModule#endif
Threadpool.hpp
文件#pragma once#include <vector> #include <queue> #include <queue> #include "Thread.hpp" #include <pthread.h> #include "LockGuard.hpp"using namespace ThreadModule;const int NUM = 3;template <typename T> class Threadpool {void LockQueue(pthread_mutex_t &mutex){pthread_mutex_lock(&mutex);}void UnLockQueue(pthread_mutex_t &mutex){pthread_mutex_unlock(&mutex);}void SleepThread(pthread_cond_t &cond, pthread_mutex_t &mutex){pthread_cond_wait(&cond, &mutex);}void WakeUpThread(pthread_cond_t &cond){pthread_cond_signal(&cond);}void WakeUpAll(pthread_cond_t &cond){pthread_cond_broadcast(&_cond);}Threadpool(const int threadnum = NUM) : _threadnum(threadnum), _waitnum(0), _isrunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);LOG(INFO, "Threadpool Constructor successful ! ");}void TaskHandler(string &name){// sleep(1);// cout << name << " : hh " << endl;// sleep(1);LOG(DEBUG, "%s is running", name.c_str());while (true){LockQueue(_mutex);while (_task_queue.empty() && _isrunning){// 等待++_waitnum;SleepThread(_cond, _mutex);--_waitnum;}// 此时一定大于一个线程没有休眠if (_task_queue.empty() && !_isrunning){// 此时任务队列已经没有内容,且此时线程池已经停止UnLockQueue(_mutex);cout << name << " quit ... " << endl;break;}LOG(DEBUG, "%s get task sucessful !", name.c_str());// 其他情况就得处理任务T t = _task_queue.front();_task_queue.pop();UnLockQueue(_mutex);// 处理任务t();// cout << name << " : " << t.stringResult() << endl;// LOG(DEBUG, "%s handler task sucessful ! Result is %s", name.c_str(), t.stringResult().c_str());sleep(1);}}void InitThreadPool(){for (int i = 0; i < _threadnum; ++i){string name = "Thread - " + to_string(i + 1);_threads.emplace_back(bind(&Threadpool::TaskHandler, this, placeholders::_1), name);}_isrunning = true;LOG(INFO, "Init Threadpool successful !");}public:static Threadpool<T> *GetInstance(int threadnum = NUM){if (_instance == nullptr){LockGuard lockguard(&_lock);if (_instance == nullptr){// pthread_mutex_lock(&_lock);// 第一次创建线程池_instance = new Threadpool<T>(threadnum);_instance->InitThreadPool();_instance->Start();LOG(DEBUG, "第一次创建线程池");// pthread_mutex_unlock(&_lock);return _instance;}}LOG(DEBUG, "获取线程池");return _instance;}bool Enqueue(const T &in){bool ret = false;LockQueue(_mutex);if (_isrunning){_task_queue.push(in);if (_waitnum > 0)WakeUpThread(_cond);LOG(DEBUG, "enqueue sucessful...");ret = true;}UnLockQueue(_mutex);return ret;}void Stop(){LockQueue(_mutex);_isrunning = false;if (_waitnum > 0)WakeUpAll(_cond);UnLockQueue(_mutex);}void Start(){for (auto &thread : _threads){thread.Start();LOG(INFO, "%s is start sucessful...", thread.name().c_str());}}void Wait(){for (auto &thread : _threads){thread.Join();LOG(INFO, "%s is quit...", thread.name().c_str());}}~Threadpool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);LOG(INFO, "delete mutex sucessful !");}private:vector<Thread> _threads;queue<T> _task_queue;int _threadnum;int _waitnum;pthread_mutex_t _mutex; // 互斥访问任务队列pthread_cond_t _cond;bool _isrunning;// 懒汉模式static Threadpool<T> *_instance;static pthread_mutex_t _lock; };template <typename T> Threadpool<T> *Threadpool<T>::_instance = nullptr; template <typename T> pthread_mutex_t Threadpool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
UdpClient.cc
文件#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include "Thread.hpp"using namespace ThreadModule;enum errorcode {USAGE_ERROR = 1, };void Usage() {printf("Usage : ./udp_client serverip serverport\n"); }class ThreadData { public:ThreadData(int sock, struct sockaddr_in &server) : _sockfd(sock), _server(server) {}~ThreadData() {}public:int _sockfd;struct sockaddr_in _server; };void RecverRoute(ThreadData &td, std::string &threadname) {while (true){// 获取回应数据char buff[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrssize_t n = recvfrom(td._sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;std::cerr << threadname << " | " << buff << std::endl; // 方便重定位,分离发送窗口和接收窗口}} }void SenderRoute(ThreadData &td, std::string &threadname) {pthread_detach(pthread_self());while (true){std::string message;std::cout << threadname << " | Please Enter:# ";std::getline(std::cin, message);if (message == "QUIT"){std::cout <<threadname << " : 不玩了!" << std::endl;break;}// 发送数据// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrint n = sendto(td._sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&td._server, sizeof(td._server));if (n <= 0){std::cout << "sendto error" << std::endl;break;}} }int main(int argc, char *argv[]) {if (argc != 3){Usage();exit(USAGE_ERROR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;}// 2. client要不要bind?一定要,client也要有自己的IP和PORT。要不要显式[和server一样用bind函数]的bind?不能!不建议!!// a. 如何bind呢?udp client首次发送数据的时候,OS会自己自动随机的给client进行bind --- 为什么?防止client port冲突。要bind,必然要和port关联!// b. 什么时候bind呢?首次发送数据的时候struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);server.sin_addr.s_addr = inet_addr(serverip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addr// 直接通信即可,已经自动绑定// 创建两个线程,一个收消息,一个发消息ThreadData td(sockfd, server);auto boundRecv = std::bind(RecverRoute, td, std::placeholders::_1);auto boundSend = std::bind(SenderRoute, td, std::placeholders::_1);Thread recver(boundRecv, "recver");recver.Start();Thread sender(boundSend, "sender");sender.Start();// // udp是全双工的// // 下面代码只能半双工,不能不能同时收发// while (true)// {// std::cout << "Please Enter:# ";// std::getline(std::cin, message);// // 发送数据// // ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addr// sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));// // 获取回应数据// char buff[1024];// struct sockaddr_in peer;// socklen_t len = sizeof(peer);// // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addr// ssize_t n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);// if (n > 0)// {// buff[n] = 0;// std::cout << "Server Echo:# " << buff << std::endl;// }// }sender.Join();recver.Join();return 0; }
UdpServer.hpp
文件#pragma once#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>#include <string.h> #include <error.h> #include <pthread.h> #include <functional>using namespace std;#include "Log.hpp" #include "InetAddr.hpp" #include "Threadpool.hpp"const int Defaultfd = -1;enum errorcode {CREATE_ERROR = 1,BIND_ERROR,USAGE_ERROR };using task_t = function<void()>;class UdpServer { public:// UdpServer(std::string ip, uint16_t port) : _sockfd(Defaultfd), _ip(ip), _port(port), _isrunning(false)UdpServer(uint16_t port) : _sockfd(Defaultfd), _port(port), _isrunning(false){pthread_mutex_init(&_mutex, nullptr);}void InitServer(){// 1. 创建udp套接字 -- 必须要做的_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp通信if (_sockfd < 0){LOG(FATAL, "socke create error ! error string %s error %d", strerror(errno), errno);exit(CREATE_ERROR);}// 创建socket成功LOG(INFO, "create socket success ! sockfd:", _sockfd);// 2.0. 填充sockaddr_in结构struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port); // 主机序列转网络序列 -- 网络序列是大端// a.字符串序列点分十进制IP地址 转换为 4字节IP// b.主机序列转网络序列// in_addr_t inet_addr(const char *cp);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // sin_addr 是一个结构体,里面的成员是s_addrlocal.sin_addr.s_addr = INADDR_ANY; // 0 -- 链接当前服务器的所有ip都接受// 2.1. 绑定网络信息int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "socke bind error ! error string %s error %d", strerror(errno), errno);exit(BIND_ERROR);}// 绑定成功LOG(INFO, "bind socket success !");// Threadpool<task_t>::GetInstance()->Start(); // 启动线程池 // 单例模式GetInstance里面已经启动线程池了,直接放入任务即可}void AddOnlineUser(InetAddr user){LockGuard lockguard(&_mutex);for (auto &e : _online_user){if (e == user)return;}_online_user.push_back(user);}void DelOnlineUser(InetAddr user){LockGuard lockguard(&_mutex);for (auto iter = _online_user.begin(); iter != _online_user.end(); ++iter){if (*iter == user)_online_user.erase(iter);}}void Route(std::string message){LockGuard lockguard(&_mutex);for (auto &user : _online_user){sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&user.GetAddr(), sizeof(user.GetAddr()));}}void Start(){// 一直运行,直到管理者不想运行了, 服务器都是死循环// UDP是面向数据报的协议_isrunning = true;while (true){// 获取数据// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // src_addrstruct sockaddr_in peer;socklen_t len = sizeof(peer); // 既是输入(必须初始化) ,也是输出(peer的实际长度)char buff[1024];ssize_t n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n > 0){buff[n] = 0;// 回答发送方// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // dest_addrInetAddr addr(peer);AddOnlineUser(addr); // 添加用户if (!strcmp(buff, "QUIT")){DelOnlineUser(addr); // 删除用户continue;}// 转发std::string message = "[";message += addr.Ip();message += ":";message += std::to_string(addr.Port());message += "] ";message += buff;task_t task = std::bind(&UdpServer::Route, this, message); // 这里绑定后,task的参数就是void(),task就是绑定后的RouteThreadpool<task_t>::GetInstance()->Enqueue(task); // 启动线程池LOG(DEBUG, "get message , sender:[%s:%d] , content: %s", addr.Ip().c_str(), addr.Port(), buff);// sendto(_sockfd, buff, strlen(buff), 0, (struct sockaddr *)&peer, len);}}_isrunning = false;}~UdpServer(){pthread_mutex_destroy(&_mutex);}private:int _sockfd;// std::string _ip; // 暂时先这样写 -- 不需要uint16_t _port;bool _isrunning;pthread_mutex_t _mutex;std::vector<InetAddr> _online_user; };
运行结果:
注意,下面用到的clienta和clientb都是管道文件,用来对文件的标准输出和标准错误分离。
8、补充内容
地址转换函数:将点分十进制的IP字符串转换为in_addr类型的IP地址,或者反过来。
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst); int inet_aton(const char *cp, struct in_addr *inp);const char *inet_ntop(int af, const void *src,char *dst, socklen_t size); char *inet_ntoa(struct in_addr in); // The inet_ntoa() function converts the Internet host address in, given in networkbyte order, to a string in IPv4 dotted-decimal notation. The string is returned in a statically allocated buffer,which subsequent calls will overwrite.
对应inet_ntoa,man手册上说会返回一个静态分配的缓冲区,后续调用会覆盖前面的内容,不需要我们手动释放。那么如果我们多次调用这个函数会出现什么情况?
在我们使用多线程调用该函数时,可能会出现数据错误的情况(需要使用互斥锁的保护)!
并且,在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数。
在多线程环境下,推荐使用 inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
OKOK,Linux网络编程之UDP就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。
Xpccccc的github主页
相关文章:
Linux网络编程之UDP
文章目录 Linux网络编程之UDP1、端口号2、端口号和进程ID的区别3、重新认识网络通讯过程4、UDP协议的简单认识5、网络字节序6、socket编程接口6.1、socket常见接口6.2、sockaddr通用地址结构 7、简单的UDP网络程序7.1、服务器响应程序7.2、服务器执行命令行7.3、服务器英语单词…...
graham 算法计算平面投影点集的凸包
文章目录 向量的内积(点乘)、外积(叉乘)确定旋转方向numpy 的 cross 和 outernp.inner 向量与矩阵计算示例np.outer 向量与矩阵计算示例 python 示例生成样例散点数据图显示按极角排序的结果根据排序点计算向量转向并连成凸包 基本…...
【海外云手机】静态住宅IP集成解决方案
航海大背景下,企业和个人用户对于网络隐私、稳定性以及跨国业务的需求日益增加。静态住宅IP与海外云手机的结合,提供了一种创新的集成解决方案,能够有效应对这些需求。 本篇文章分为三个部分;静态住宅优势、云手机优势、集成解决…...
最新!CSSCI(2023-2024)期刊目录公布!
【SciencePub学术】据鲁迅美术学院7月16日消息,近日,南京大学中国社会科学研究评价中心公布了中文社会科学引文索引(CSSCI)(2023—2024)数据库最新入选目录。 C刊一般指CSSCI来源期刊,即南大核心…...
C语言 | Leetcode C语言题解之第237题删除链表中的节点
题目: 题解: /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/void deleteNode(struct ListNode* node) {struct ListNode * p node->next;int temp;temp node->val;node->val…...
linux LED代码设计
设计目标: 写RGB LED灭、亮、闪烁等效果,不同颜色也需要设置 #include <iostream> #include <unistd.h> // 对于usleep() #include <fcntl.h> // 对于open(), close() #include <sys/ioctl.h> // 对于ioctl() #include <li…...
Jvm基础(一)
目录 JVM是什么运行时数据区域线程私有1.程序计数器2.虚拟机栈3.本地方法栈 线程共享1.方法区2.堆 二、对象创建1.给对象分配空间(1)指针碰撞(2)空闲列表 2.对象的内存布局对象的组成Mark Word类型指针实例数据:对齐填充 对象的访问定位句柄法 三、垃圾收集器和内存…...
深入理解FFmpeg--软/硬件解码流程
FFmpeg是一款强大的多媒体处理工具,支持软件和硬件解码。软件解码利用CPU执行解码过程,适用于各种平台,但可能对性能要求较高。硬件解码则利用GPU或其他专用硬件加速解码,能显著降低CPU负载,提升解码效率和能效。FFmpe…...
新的铸造厂通过 PROFIBUS 技术实现完全自动化
钢铁生产商某钢以其在厚钢板类别中极高的产品质量而闻名。其原材料(板坯连铸机)在钢铁厂本地生产,该厂最近新建了一座垂直连铸厂。该项目的一个主要目标是从一开始就完全自动化这座新工厂和整个铸造过程,以高成本效率实现最佳产品…...
【UE5.1】NPC人工智能——04 NPC巡逻
效果 步骤 一、准备行为树和黑板 1. 对我们之前创建的AI控制器创建一个子蓝图类 这里命名为“BP_NPC_AIController_Lion”,表示专门用于控制狮子的AI控制器 2. 打开狮子蓝图“Character_Lion” 在类默认值中将“AI控制器类”修改为“BP_NPC_AIController_Lion” 3…...
计算机视觉主流框架及其应用方向
文章目录 前言一、计算机视觉领域的主要框架1、深度学习框架1.1、TensorFlow1.2、PyTorch 2、神经网络模型2.1、卷积神经网络(CNN)2.2、循环神经网络(RNN) 二、框架在计算机视觉任务中的应用1、TensorFlow1.1、概述:1.…...
群晖 搭建alist 记录
docker搭建 使用docker-compose 创建一个 docker-compose.yml version: 3.5services:qbittorrent:image: linuxserver/qbittorrent:latestcontainer_name: qbittorrent# network_mode: hostenvironment:- PUID1000- PGID100- TZAsia/Shanghai- WEBUI_PORT8181 # 将外部端口…...
【北航主办丨本届SPIE独立出版丨已确认ISSN号】第三届智能机械与人机交互技术学术会议(IHCIT 2024,7月27)
由北京航空航天大学指导,北京航空航天大学自动化科学与电气工程学院主办,AEIC学术交流中心承办的第三届智能机械与人机交互技术学术会议(IHCIT 2024)将定于2024年7月27日于中国杭州召开。 大会面向基础与前沿、学科与产业…...
深入浅出WebRTC—NACK
WebRTC 中的 NACK(Negative Acknowledgment)机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象,尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响,从而保持通信的连续性和质量…...
简单工厂模式、工厂模式和抽象工厂模式的区别
简单工厂模式、工厂模式和抽象工厂模式都是创建型设计模式,它们之间在目的、实现方式和适用场景上存在显著的区别。以下是对这三种模式的详细比较: 一、定义与目的 简单工厂模式(Simple Factory Pattern) 定义: 简单工…...
JVM-垃圾回收与内存分配
目录 垃圾收集器与内存分配策略 引用 对象的访问方式有哪些?(句柄和直接指针) Java的引用有哪些类型? 如何判断对象是否是垃圾? 请列举一些可作为GC Roots的对象? 对象头了解吗? mark word(hashcode、分代、锁标志位)、…...
Jolt路线图
1. 引言 a16z crypto团队2024年7月更新了其Jolt路线图: 主要分为3大维度: 1)链上验证维度: 1.1)Zeromorph:见Aztec Labs团队2023年论文 Zeromorph: Zero-Knowledge Multilinear-Evaluation Proofs from…...
NEEP-EN2-2019-Text4
英二-2019-Text4摘自赫芬顿邮报《The Huffington Post》2018年6月的一篇名为“Let’s Stop Pretending Quitting Straws Will Solve Plastic Pollution”的文章。 以下为个人解析,非官方公开标准资料,可能有误,仅供参考。(单词解释…...
docker 部署wechatbot-webhook 并获取接口实现微信群图片自动保存到chevereto图库等
功能如图: docker部署 version: "3" services:excalidraw:image: dannicool/docker-wechatbot-webhook:latestcontainer_name: wechatbot-webhookdeploy:resources:limits:cpus: 0.15memory: 500Mreservations:cpus: 0.05memory: 80Mrestart: alwayspor…...
OpenWrt安装快速入门指南
在刷新 OpenWrt 固件之前,建议进行以下准备: 1、不要急于安装,慢慢来。如果在安装过程中出现奇怪之处,请先找到答案,然后再继续。 2、准备好设备的精确型号,以便能够选择正确的OpenWrt固件。 3、手上有关…...
AIGC Kolors可图IP-Adapter-Plus风格参考模型使用案例
参考: https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus 代码环境安装: git clone https://github.com/Kwai-Kolors/Kolors cd Kolors conda create --name kolors python=3.8 conda activate kolors pip install -r requirements.txt python3 setup.py install…...
从零开始学量化~Ptrade使用教程(七)——期权相关操作
期权交易 可点击证券代码右侧的选,进入期权选择菜单。通过选择标的商品,认购期权和认沽期权中间的选项(包括代码、成交价、幅度%、隐波%、内在价值、时间价值等),以及认购期权或认沽期权,选择所需的期权标的…...
TeamViewer关闭访问密码或固定一组密码不变
TeamViewer的新UI界面变化较大,网上的一些信息已经不再有效,更新后的访问密码在如下图所示: 演示的版本为7.21.4—— 设置每次你的设备访问的密码...
iMazing 3 换手机后苹果游戏数据还有吗 换iPhone怎么转移游戏数据
当你想要更换手机,无论是选择升级到最新款iPhone,或者换到“经典”旧款iPhone,单机游戏数据的转移总是让人发愁。本文将详细介绍换手机后苹果游戏数据还有吗,以及换iPhone怎么转移游戏数据,确保你能无缝继续你的游戏体…...
正则表达式:电子邮件地址的格式详解,及常见正则表达式符号的详细解释和匹配方式
一、第一部分是对该段电子邮件的详解 var Regex /^(?:\w\.?)*\w(?:\w\.)*\w$/; 1.^:这个符号表示匹配输入字符串的开始位置。 2.(?:...):这是一个非捕获组(non-capturing group),用于将正则表达式的一部分组合在…...
AWS全服务历史年表:发布日期、GA和服务概述一览(一)
我一直在尝试从各种角度撰写关于Amazon Web Services(AWS)的信息和魅力。由于我喜欢技术历史,这次我总结了AWS服务发布的历史年表。 虽然AWS官方也通过“Whats New”发布了官方公告,但我一直希望能有一篇文章将公告日期、GA日期&…...
现场可重构CPLD芯片应用案例—蓝牙音箱
我司英尚微提供的高性能数模混合现场可重构IC、通用可配置的模数混合芯片内部集成丰富的模拟资源和数字资源,可轻松替代电路中的各种标准器件,并按照客户要求组合成最优小型ASIC,缩短开发周期,降低成本。下面介绍LS98002现场可重构…...
vue2关于Object.defineProperty实现响应式
实现步骤: 1. 初始化阶段 当 Vue 实例化时,会遍历data 选项中的属性,并使用 Object.defineProperty 将它们转换为 getter 和 setter。这样一来,每当访问或修改这些属性时, Vue就能捕获到这些操作,从而实现…...
中英双语介绍一级市场(Primary Market)和二级市场(Secondary Market)
中文版 一级市场和二级市场是金融市场中的两个主要部分,分别对应证券发行和交易的不同阶段。 一级市场(Primary Market) 定义: 一级市场,又称新发行市场,是指证券首次发行和出售的市场。在一级市场中&am…...
OpenCV 轮廓检测
在 OpenCV 中,轮廓检测是一种用于查找图像中具有相似颜色或强度的连通像素组的技术,这些像素组通常代表了图像中的物体边缘。轮廓可以用来识别和分割图像中的物体,是计算机视觉应用中的一个重要步骤,如目标识别、形状分析等。 轮…...
即墨公司做网站/百度资讯
C 预处理指令#pragma 一、定义介绍 #pragma是C预处理指令的一种,它可以设置编译器的状态,或者让编译器完成一些特定的工作。因此,它是一种操作编译器的指令。 二、功能作用 #pragma的作用是让编译器执行一些已经设定好的工作。通过#pragma后…...
淘宝上网站开发/nba最新交易新闻
1.将一个给定的整型数组转置输出, 源数组为:1 2 3 4 5 6转置之后输出的数组为:6 5 4 3 2 1 刚看到题目的时候没多想,只想着能倒着输出就好(正确代码错误想法) public static void main(String[] args){int…...
基于jquery做的网站/深圳产品网络推广
编译/运行 编译时是静态加载,如我们的new();运行时是动态加载,Class.forName(); Demo走起 class Main {public static void main(String[] args) throws Exception{if("Excel".equals(args[0])){Excel excel new Excel();excel.start();…...
网站备案 选项/线上推广有哪些
当前环境:Oracle enterprise Linux 5.6Oracle 10.2.0.1数据库安装成功后,如果操作系统重启,数据库不会自动启动,以下步骤将实现oralce数据库随操作系统自动启动。一、使用root用户修改/etc/oratab 文件:$ vi /etc/orat…...
做网上卖酒的网站有几家/seo模拟点击
H参数表示色彩信息,即所处的光谱颜色的位置。该参数用一角度量来表示,红、绿、蓝分别相隔120度。互补色分别相差180度。纯度S为一比例值,范围从0到1,它表示成所选颜色的纯度和该颜色最大的纯度之间的比率。S0时,只有灰…...
织梦源码怎样做单页网站/谷歌seo外链
三台主机192.168.191.106(代号106) 产生日志192.168.191.107(代号107) 实现存放日志的数据库192.168.191.173(代号173) 实现日志报表1、实现rsyslog将日志记录于MySQL中(1)在107上:yum install mariadb-serversystemctl start mariadbmysql_secure_installati…...