当前位置: 首页 > news >正文

Linux之网络套接字

Linux之网络套接字

  • 一.IP地址和端口号
  • 二.TCP和UDP协议
    • 2.1网络字节序
  • 三.socket编程的常见API
  • 四.模拟实现UDP服务器和客户端
  • 五.模拟实现TCP服务器和客户端

一.IP地址和端口号

在了解了网络相关的基础知识之后我们知道了数据在计算机中传输的流程并且发现IP地址在其中占据了确定目标主机的功能,但是在计算机中我们网络通信的行为其实本质都是进程间进行通信。那么在一个主机中有那么多个进程我们要如何准确定位到某个进程呢?
那就要利用端口号了,所谓的端口,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。
所以我们使用IP地址+端口号的方法就可以找到互联网中唯一的一个进程,其中IP地址用来标记互联网中唯一的一台主机,端口号用来标记主机中唯一的一个进程。但是对于端口号有些地方是和IP地址相同的,一个主机既可以只有一个IP地址也可以有多个IP地址所以一个进程既可以有一个端口号也可以有多个端口号。同时多个主机不可以有一个IP地址多个进程也不可以有一个端口号。
在了解到其中的奥秘之后人们为了更好的称呼这两个概念的组合就又创造了一个概念叫套接字(socket),socket的英译其实是插座的意思从它的英译中我们也可以大概理解它的作用。插座是为了连接两端的物品所以将其具体到网络中socket的作用就是充当应用层和传输层之间的一个抽象层,它将TCP/IP传输层中的一些复杂的操作简单化接口化进而提供给应用层进行调用从而实现进程间的网络通信。

如果端口号port是用来确定计算机中的唯一的一个进程的话,那么可能有些人就有疑问了我们在操作系统的学习的时候使用过进程的pid当时说pid的作用也是确定唯一的一个进程。那么这两者之间有什么联系或者是有什么区别吗?
答案是没有什么区别,它两都可以用来确定主机中唯一的一个进程但是pid是在进行进程管理的时候使用的而端口号是在进行网络通信的时候使用的,这么做的原因其实也很简单就是将进程管理和网络通信进行解耦,pid就专职于进程管理port就专职于网络通信两个人谁都不打扰谁。

二.TCP和UDP协议

之前我们一直在谈协议的分层协议的意义等等但是我们一直没有见过真正的协议如今我们就先学习一下传输层中的两个协议:TCP协议和UDP协议
TCP协议

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

UDP协议

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

我们先大致了解一下这两个协议的基本特质其中具体的内容我们等之后再谈,在这里我要强调一个概念:可不可靠传输是协议的特性,这两个都是一个中性词没有好坏之分。就像函数的可复用性一样,可靠传输不代表就比不可靠传输好。可靠传输说明传输的过程更复杂更安全,不可靠传输说明传输的过程比较简单适合一些对数据可靠性不高的场景。

2.1网络字节序

在我们最开始学习C语言的时候我们在介绍数据的存储方式时提到过不同的机器存储方式不同其中分为大端机和小端机。大端模式也就是将低位数据存储到高地址中高位数据存储到低地址中,小端模式是将低位数据存储到低地址里高位数据存储到高地址里。大端模式和小端模式没有明显的优劣之差,如果非要说哪个优一点大端机器的可读性比小端机器好一点。

对于内存的中的数据存储方式有大小端之分那么对于网络传输的数据流是否也要有大小端之分呢?我们又该选择哪个呢?
虽然内存地址有大小端之分但是我们要知道计算机对于数据的读取和写入是从低地址到高地址的这也就是导致为什么大端模式比小端模式可读性更高的原因。
在这里插入图片描述
所以在数据从一个主机发送到另外一个主机读取的过程中发送和读取都是从低地址到高地址的所以网络数据流的数据的存储方法也就是数据的地址是按照大端模式的也就是低地址高字节。
要注意这是一个规定不是你先用哪个方式发送就是哪个方式的,所有的机器在进行TCP/IP通信的时候发送数据的时候都需要将其转换为大端模式。而这个转换为大端模式后的数据存储方式叫做网络字节序。
而为了方便用户将各种整型变量网络字节序化系统提供了几个库函数
在这里插入图片描述

三.socket编程的常见API

  1. 创建socket文件描述符(TCP/UDP, 客户端 + 服务器)
    在这里插入图片描述

对于这三个参数和返回值大家应该有不少的疑惑:为什么返回值是一个文件描述符?socket类型是什么?SOCK_STREAM和SOCK_DGRAM又分别是什么意思?我一个一个的给大家解释一下。
对于返回值是文件描述符的问题我们需要知道网络传输的原理是什么,简单来说两个主机互相传输数据其实就是在服务器和客户端中分别创建一个文档然后服务器会向自己新创建的文档中写入和读取数据,而写入的数据又会通过网络拷贝到客户端新创建的文档中而客户端亦是如此。所以在创建套接字的时候会返回一个文件描述符。
在进行网络编程的时候socket是有很多类型的有些是由于传输的环境不同有些是由于协议的不同,例如unix_socket:域间socket,在同一台主机中进行通信,网络socket:使用ip+port的方式运用于网络通信,原始socket:编写一些网络工具。
SOCK_STREAM和SOCK_DGRAM分别是流格式套接字和数据报格式套接字,这两个概念在我们介绍TCP和UDP协议的提到过这也就是为什么这两个socket类型是和TCP和UDP协议组合在一起的原因。

  1. 绑定端口号 (TCP/UDP, 服务器)
    在这里插入图片描述
    对于第二个参数我需要给大家讲解一下,sockaddr是一个结构体代表的是一个通用的地址类型它其中存储了地址族和套接字中的目标地址信息和端口号。
    在这里插入图片描述
    但是这个结构体是有漏洞的它将目标的地址信息和端口号混在了一起。所以还设计了另外两个结构体sockaddr_in。
    在这里插入图片描述
    而sockaddr和sockaddr_in的就像我们在C++中学习的多态的基类和子类一样,我们在socket编程中使用的一般都是sockaddr_in但是由于某些函数的参数是sockaddr所以我们可以将sockaddr_in强转为sockaddr。

  2. 开始监听socket (TCP, 服务器)
    在这里插入图片描述
    listen函数的作用是将套接字文件描述符从主动转为被动文件描述符,然后用于被动监听客户端的连接,具体的会在讲解协议的时候再谈。如今我们只需要知道怎么用它即可。

  3. 接收请求 (TCP, 服务器)
    在这里插入图片描述
    为什么还要创建一个新文件呢不是已经创建了一个文件了吗为什么不用那个文件描述符呢?
    这个问题其实在使用listen的时候就已经有答案了,大家可以仔细看看listen函数的作用:套接字文件描述符从主动转为被动文件描述符,然后用于被动监听客户端的连接。socket返回的文件描述符已经是被动模式那么肯定需要我们再创建一个新的文件来对客户端进行服务。就好像我们在路上遇到的拉客的人,在我们被他拉进去吃饭之后服务我们的是另外的服务员并不是那个拉我们进来的人。使用套接字文件描述符就是那个拉客的人而这个新的文件描述符才是服务我们的人。

  4. 建立连接 (TCP, 客户端)
    在这里插入图片描述

在大概了解了模拟实现UDP和TCP服务器和客户端时会出现的新函数后我们就可以来分别模拟实现UDP服务器和客户端以及TCP服务器和客户端了。
这两个协议的不同所以模拟实现的过程也不同并且它两服务器和客户端的运行逻辑也不同,到了模拟实现的时候我会具体来说。

四.模拟实现UDP服务器和客户端

我们先来看一下UDP服务器和客户端的运行逻辑
在这里插入图片描述

#Makefile
.PHONY:all
all:udp_server udp_clientudp_server:Main.ccg++ -o $@ $^ -std=c++14
udp_client:udp_client.ccg++ -o $@ $^ -std=c++14.PHONY:clean
clean:rm -f udp_server udp_client
//log.hpp
#pragma once
#include <iostream>
#include <stdarg.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>enum
{Debug = 0,Info,Warning,Error,Fatal
};// 分为打印到屏幕上,打印到一个文件中,打印到不同的等级的文件中
enum
{Screen = 10,Onefile,Classfile
};int defaultstyle = Screen;
std::string default_filename = "log.";
std::string logdir = "log";class Log
{
public:Log(int style = defaultstyle, std::string file = default_filename): _style(style), _file(file){// mkdir不止是指令,还是一个函数可以用来调用然后创建目录mkdir(logdir.c_str(), 0775);}// 更改模式void Enable(int sty){std::cout << "Enable success" << std::endl;_style = sty;}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";}return "null";}// 返回本地时间std::string TimeChangeLocal(){time_t currtime = time(nullptr);// 我们利用time返回的时间是以时间戳的形式返回的不太好看// 所以我们可以利用localtime将其转化为一个类struct tm *local = localtime(&currtime);char time_buffer[128];// 利用snprintf以年-月-日 小时-分钟-秒的形式输入到tim_buffer中// 注意:tm类的年是以1900年开始的的snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d-%d-%d",local->tm_year + 1900, local->tm_mon, local->tm_mday,local->tm_hour, local->tm_min, local->tm_sec);return time_buffer;}// 打印到具体的某个文件中void WriteLogToOneFile(const std::string &logname, const std::string &message){umask(0);int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);write(fd, message.c_str(), message.size());close(fd);}// 打印到不同等级的文件中void WriteLogToClassFile(const std::string &leverstr, const std::string &message){std::string logname = logdir;logname += "/";logname += _file;logname += leverstr;WriteLogToOneFile(logname, message);}void Writelog(std::string &str, std::string &message){switch (_style){case Screen:std::cout << message;break;case Onefile:WriteLogToClassFile("all", message);break;case Classfile:WriteLogToClassFile(str, message);break;default:break;}}// 我们想要日志像printf一样打印所以要使用可变参数// 但是我们可以利用va_list参数的变量将可变参数存储在变量中void LogMessage(int level, const char *format, ...){// 左边信息为:[消息等级][时间][进程编号]char leftmessage[1024];std::string leverstr = LevelToString(level);std::string currtime = TimeChangeLocal();std::string idstr = std::to_string(getpid());// 右边信息为:想输出的内容char rightmessage[1024];va_list args;va_start(args, format);                                      // 将args指向可变参数部分vsnprintf(rightmessage, sizeof(rightmessage), format, args); // 将可变参数写入到rightmessage中va_end(args);                                                // args == nullptrsnprintf(leftmessage, sizeof(leftmessage), "[%s][%s][%s]", leverstr.c_str(), currtime.c_str(), idstr.c_str());std::string loginfo = leftmessage;loginfo += rightmessage;Writelog(leverstr, loginfo);}~Log(){}private:int _style;std::string _file;
};Log lg;
//nocopy.hpp
#pragma onceclass NoCopy
{
public:NoCopy(){}~NoCopy(){}NoCopy(const NoCopy &) = delete;const NoCopy &operator=(const NoCopy &) = delete;
};
//Main.cc
#include "udp_server.hpp"
#include <iostream>
#include <memory>void Usage(std::string process)
{std::cout << "Usage:\n" << process << "local_ip local_port\n" << std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return -1;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpServer> up_udp = std::make_unique<UdpServer>(ip,port);up_udp->Init();up_udp->Start();return 0;
}
//udp_client.hpp
#include <iostream>
#include <memory>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>const static int defaultsize = 1024;void Usage(std::string process)
{std::cout << "Usage:\n"<< process << "server_ip server_port\n"<< std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return -1;}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);// 1.创建socketint socketfd = socket(AF_INET, SOCK_DGRAM, 0);if (socketfd < 0){std::cout << "client socket error" << errno << " " << strerror(errno) << std::endl;return -1;}std::cout << "socket create success socket:" << socketfd << std::endl;// 2.绑定网络信息// client需要绑定网络信息但是不需要显式绑定,在client发送第一条消息时系统会随机给他绑定一个端口号// 为什么client需要绑定网络信息是因为client会有很多如果我们显式绑定很容易重复// 收集server的信息等到发送时使用struct sockaddr_in servermessage;memset(&servermessage, 0, sizeof(servermessage));servermessage.sin_family = AF_INET;servermessage.sin_port = htons(server_port);servermessage.sin_addr.s_addr = inet_addr(server_ip.c_str());// 3.发送消息// 服务器是个死循环,客户端同理while (1){std::string inbuffer;std::cout << "Please Enter:";std::getline(std::cin, inbuffer);// 发送消息ssize_t n = sendto(socketfd, inbuffer.c_str(), inbuffer.size(),0,(struct sockaddr *)&servermessage, sizeof(servermessage));if (n > 0){// 接收消息char outbuffer[defaultsize];struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t m = recvfrom(socketfd, &outbuffer, sizeof(outbuffer) - 1,0,(struct sockaddr*)&temp, &len);if (m > 0){outbuffer[m] = 0;std::cout << "server echo:" << outbuffer << std::endl;}else{break;}}else{break;}}close(socketfd);return 0;
}
//udp_server.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "log.hpp"
#include "nocopy.hpp"const static uint16_t defaultport = 8888;
const static int defaultsocketfd = -1;
const static int defaultsize = 1024;class UdpServer : public NoCopy
{
public:UdpServer(std::string ip, uint16_t port = defaultport, int socketfd = defaultsocketfd): _ip(ip), _port(port), _socketfd(socketfd){}~UdpServer(){}void Init(){// 1.创建socket,创建文件信息_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if (_socketfd < 0){lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(-1);}lg.LogMessage(Info, "socket create success, socketfd:%d\n", _socketfd);// 2.绑定网络信息// 保存本地的信息struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;// 在保存本地信息时要将ip和端口号port转换为网络序列// ip同时要改为四字节的local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());// 本地信息的结构体填完了后要将其加载到内核中int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));if (n != 0){lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(-1);}}void Start(){char buffer[defaultsize];// 服务器是一个死循环while (1){// 保存client客户端的信息struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接收消息ssize_t n = recvfrom(_socketfd, &buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;std::cout << "client say :" << buffer << std::endl;// 发送消息sendto(_socketfd, &buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);}}}private:std::string _ip;uint16_t _port;int _socketfd;
};

这些代码只能完成最简单的UDP的服务器和客户端仅仅只能发消息,大家还可以利用我们之前学习的程序替换来完成发送并且执行指令的功能以及利用多线程来完成一个聊天室的功能。
这里的代码我就不贴出来了大家需要的话可以私聊我。

五.模拟实现TCP服务器和客户端

在这里插入图片描述

在TCP编程中我们就不需要使用sendto和recvform这两个函数了我们可以直接用write和read来完成文件的读写操作。
下面我也是直接贴出代码,这次的代码就是最终版了比较的复杂,所以我就把主要的关于客户端和服务端的代码贴出来了。

#Makefile
.PHONY:all
all:tcp_server tcp_clienttcp_server:Main.cc g++ -o $@ $^ -std=c++14 -lpthread
tcp_client:tcp_client.ccg++ -o $@ $^ -std=c++14 .PHONY:clean
clean:rm -f tcp_server tcp_client
//tcp_client.cc
#include <iostream>
#include <memory>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>#include "log.hpp"#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
#define Retry_Count 5const static int defaultsize = 1024;void Usage(std::string process)
{std::cout << "Usage:\n"<< process << "server_ip server_port\n"<< std::endl;
}bool VisitServer(std::string server_ip, uint16_t server_port, int *count)
{// 1.创建socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);ssize_t m = 0;ssize_t r = 0;std::string inbuffer;char service_list[1024];if (sockfd < 0){std::cout << "client socket error" << errno << " " << strerror(errno) << std::endl;return -1;}// std::cout << "socket create success socket:" << sockfd << std::endl;// 2.绑定网络信息// client需要绑定网络信息但是不需要显式绑定,在client发送第一条消息时系统会随机给他绑定一个端口号// 为什么client需要绑定网络信息是因为client会有很多如果我们显式绑定很容易重复// 收集server的信息等到发送时使用struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); // 从字符串ip到4字符ip 从系统序列变为网络序列bool result = true;// 3.连接客户端int n = connect(sockfd, CONV(&server), sizeof(server));if (n < 0){result = false;goto END;}*count = 0;std::cout << "connect success"<< std::endl;// 3.发送消息和接收信息r = read(sockfd, &service_list, sizeof(service_list) - 1);if (r > 0){service_list[r] = 0;std::cout << "service list is " << service_list << std::endl;}std::cout << "Please select service: ";getline(std::cin, inbuffer);write(sockfd, inbuffer.c_str(), inbuffer.size());std::cout << "Enter: ";getline(std::cin, inbuffer);if (inbuffer == "quit"){return true;}m = write(sockfd, inbuffer.c_str(), inbuffer.size());if (m > 0){char outbuffer[defaultsize];r = read(sockfd, &outbuffer, sizeof(outbuffer) - 1);if (r > 0){outbuffer[r] = 0;std::cout << outbuffer << std::endl;}else if (r == 0){return result;}else{lg.LogMessage(Fatal, "client read error, %d : %s\n", errno, strerror(errno));result = false;goto END;}}else{lg.LogMessage(Fatal, "client write error, %d : %s\n", errno, strerror(errno));result = false;goto END;}
END:close(sockfd);return result;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return -1;}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);int cnt = 1;while (cnt <= Retry_Count){bool result = VisitServer(server_ip, server_port, &cnt);if (result){break;}else{sleep(1);std::cout << "client is retring connect, count: " << cnt << std::endl;cnt++;}}if (cnt > Retry_Count){std::cout << "server is unonline..." << std::endl;}return 0;
}
//Main.cc
#include <iostream>
#include <memory>
#include <algorithm>#include "tcp_server.hpp"
#include "Translate.hpp"
#include "Daemon.hpp"void Usage(std::string process)
{std::cout << "Usage:\n"<< process << " local_port\n"<< std::endl;
}void Interact(int fd, InetAddr addr, std::string inbuffer)
{char outbuffer[1024];// 先读再写while (1){ssize_t n = read(fd, &outbuffer, sizeof(outbuffer) - 1);if (n > 0){outbuffer[n] = 0;std::cout << "[" << addr.PrintDebug() << "]# " << outbuffer << std::endl;write(fd, inbuffer.c_str(), inbuffer.size());}else if (n < 0){lg.LogMessage(Fatal, "server read error, %d : %s\n", errno, strerror(errno));break;}else if (n == 0){lg.LogMessage(Debug, "client quit...\n");break;}}
}void Ping(int fd, InetAddr addr)
{lg.LogMessage(Debug, "%s select %s success, fd:%d\n", addr.PrintDebug().c_str(), "Ping", fd);Interact(fd, addr, "pong");
}translate ts;
void Translate(int fd, InetAddr addr)
{lg.LogMessage(Debug, "%s select %s success, fd:%d\n", addr.PrintDebug().c_str(), "Translate", fd);char english[128];ssize_t m = read(fd, &english, sizeof(english) - 1);if (m > 0){english[m] = 0;}std::string chinese = ts.Execute(english);write(fd, chinese.c_str(), chinese.size());lg.LogMessage(Debug, "translate success, %s->%s\n", english, chinese.c_str());
}void Transform(int fd, InetAddr addr)
{lg.LogMessage(Debug, "%s select %s success, fd:%d\n", addr.PrintDebug().c_str(), "Transform", fd);char message[128];int n = read(fd, message, sizeof(message) - 1);if (n > 0)message[n] = 0;std::string messagebuf = message;std::transform(messagebuf.begin(), messagebuf.end(), messagebuf.begin(), [](unsigned char c){ return std::toupper(c); });write(fd, messagebuf.c_str(), messagebuf.size());lg.LogMessage(Debug, "transform success, %s->%s\n", message, messagebuf.c_str());
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return -1;}uint16_t port = std::stoi(argv[1]);// 将进程转换为守护进程化Daemon(false, false);lg.Enable(Classfile);lg.LogMessage(Debug, "Daemon success...\n");std::unique_ptr<TcpServer> tp_tcp = std::make_unique<TcpServer>(port);tp_tcp->EnrollFunc("ping", Ping);tp_tcp->EnrollFunc("translate", Translate);tp_tcp->EnrollFunc("transform", Transform);tp_tcp->Init();tp_tcp->Start();return 0;
}
//tcp_server.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <functional>
#include <unordered_map>#include "log.hpp"
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include "threadpool.hpp"#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
#define ERROR "error"const static int default_backlog = 1;using task_t = std::function<void()>;
using callback_t = std::function<void(int, InetAddr &)>;class TcpServer;// class ThreadDate
// {
// public:
//     ThreadDate(int fd, TcpServer *server_ptr, struct sockaddr_in addr)
//         : _fd(fd), _server_ptr(server_ptr), _addr(addr)
//     {
//     }
//     ~ThreadDate()
//     {
//         close(_fd);
//     }
//     int GetFd()
//     {
//         return _fd;
//     }
//     TcpServer *GetServer()
//     {
//         return _server_ptr;
//     }
//     InetAddr GetAddr()
//     {
//         return _addr;
//     }//     int _fd;
//     TcpServer *_server_ptr;
//     InetAddr _addr;
// };class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port): _port(port), _isrunning(false){}~TcpServer(){}void Init(){// 1.创建socket套接字_listenfd = socket(AF_INET, SOCK_STREAM, 0);if (_listenfd < 0){lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(-1);}// 在使用tcp时会出现有多个套接字链接到一个端口上的情况,所以我们需要运行端口复用int opt = 1;setsockopt(_listenfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Info, "socket create success, socketfd:%d\n", _listenfd);// 2.填充本地网络信息并且bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = htonl(INADDR_ANY);local.sin_port = htons(_port);int n = bind(_listenfd, CONV(&local), sizeof(local));if (n != 0){lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(-1);}lg.LogMessage(Info, "bind success, listenfd:%d\n", _listenfd);// 3.将socket设置为监听状态// 因为tcp协议是依靠链接来进行交互的所以我们需要将客户端的套接字// 从主动设置为被动从而监听用户端的连接行为int m = listen(_listenfd, default_backlog);if (m != 0){lg.LogMessage(Fatal, "listen error, %d : %s\n", errno, strerror(errno));exit(-1);}lg.LogMessage(Info, "listen success, listenfd:%d\n", _listenfd);threadpool<task_t>::Getinstance()->ThreadStart();_funcs.insert(std::make_pair("defaulthandle", std::bind(&TcpServer::DefaultHandle, this, std::placeholders::_1, std::placeholders::_2)));}void Service(int sockfd, InetAddr addr){char outbuffer[1024];// 先读再写while (1){ssize_t n = read(sockfd, &outbuffer, sizeof(outbuffer) - 1);if (n > 0){outbuffer[n] = 0;std::cout << "[" << addr.PrintDebug() << "]# " << outbuffer << std::endl;std::string inbuffer = "pong";write(sockfd, inbuffer.c_str(), inbuffer.size());}else if (n < 0){lg.LogMessage(Fatal, "server read error, %d : %s\n", errno, strerror(errno));break;}else if (n == 0){lg.LogMessage(Debug, "client quit...\n");break;}}}// 因为这个函数是成员函数所以会带有一个this指针从而导致参数不对// 所以我们需要将其转换为静态成员函数// static void *HandleFunction(void *args)// {//     // 线程分离//     pthread_detach(pthread_self());//     ThreadDate *td = static_cast<ThreadDate *>(args);//     td->GetServer()->Service(td->GetFd(),td->GetAddr());//     delete td;//     return nullptr;// }void Routinue(int fd, InetAddr addr){_funcs["defaulthandle"](fd, addr);std::string type = Read(fd);lg.LogMessage(Debug, "%s select %s\n", addr.PrintDebug().c_str(), type.c_str());if (type == "ping"){_funcs[type](fd, addr);}else if (type == "translate"){_funcs[type](fd, addr);}else if (type == "transform"){_funcs[type](fd, addr);}else{}close(fd);}void EnrollFunc(std::string name, callback_t func){_funcs[name] = func;}std::string Read(int fd){char type[128];ssize_t n = read(fd, &type, sizeof(type) - 1);if (n > 0){type[n] = 0;lg.LogMessage(Debug,"server read success, message:%s\n",type);}else if (n < 0){lg.LogMessage(Fatal, "server read error, %d : %s\n", errno, strerror(errno));return ERROR;}else if (n == 0){lg.LogMessage(Debug, "client quit...\n");return ERROR;}return type;}void DefaultHandle(int fd, InetAddr addr){std::string lists = "|";for (auto &ch : _funcs){lists += ch.first;lists += "|";}write(fd, lists.c_str(), lists.size());}void Start(){// 服务器是一个死循环_isrunning = true;signal(SIGCHLD, SIG_IGN);while (_isrunning){sleep(1);// 4.获取连接// 当客户端进入监听状态后我们还需要让客户端使用accept函数来获取连接也就是获取一个全新的文件描述符struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listenfd, CONV(&peer), &len);if (sockfd < 0){lg.LogMessage(Fatal, "accept error, %d : %s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get a new sockfd:%d\n", sockfd);// 5.提供服务// 版本1// 问题:只能有一个客户端连接服务器// Service(sockfd);// close(sockfd);// 版本2// 利用多进程会继承文件描述符表来解决版本1的问题//     pid_t id = fork();//     if (id == 0)//     {//         // 子进程//         // 子进程需要关闭不需要的文件描述符即_listenfd//         close(_listenfd);//         pid_t iid = fork();//         if (iid > 0)//         {//             //子进程//             exit(0);//         }//         else//         {//             //孙子进程//             //在子进程退出后孙子进程就变成了孤儿进程//             //然后就被系统领养//             Service(sockfd);//             close(sockfd);//             exit(0);//         }//     }//     else if (id > 0)//     {//         // 父进程//         // 子进程需要关闭不需要的文件描述符即sockfd//         close(sockfd);//         pid_t rid = waitpid(id, nullptr, 0);//         if (rid == id)//         {//             lg.LogMessage(Debug, "wait success...\n");//         }//     }//     else//     {//         lg.LogMessage(Fatal, "fork error, %d : %s\n", errno, strerror(errno));//         close(sockfd);//         continue;//     }// 版本3// 利用多进程中子进程退出时会发出信号后修改信号的处理方法// 从而让父进程不用等待子进程,系统会自动回收子进程的资源// pid_t id = fork();// if (id == 0)// {//     // 子进程//     // 子进程需要关闭不需要的文件描述符即_listenfd//     close(_listenfd);//     Service(sockfd);//     close(sockfd);//     exit(0);// }// else if (id > 0)// {//     // 父进程//     // 子进程需要关闭不需要的文件描述符即sockfd//     close(sockfd);// }// else// {//     lg.LogMessage(Fatal, "fork error, %d : %s\n", errno, strerror(errno));//     close(sockfd);//     continue;// }// 版本4// 利用多线程会共享一个文件描述符表的特性来让新线程去执行服务// 并且将新线程进行分离从而达到自动回收资源的目的// ThreadDate *td = new ThreadDate(sockfd, this, peer);// pthread_t tid;// pthread_create(&tid, nullptr, HandleFunction, td);// 版本5// 利用线程池提前创建线程并向其分配任务// 问题:不能给客户端提供长服务task_t ts = std::bind(&TcpServer::Routinue, this, sockfd, InetAddr(peer));threadpool<task_t>::Getinstance()->Push(ts);}}private:uint16_t _port;// int _fd;int _listenfd;bool _isrunning;std::unordered_map<std::string, callback_t> _funcs;
};

如果大家需要全部的代码可以去我的gitee中自行拷贝或者私信我。

相关文章:

Linux之网络套接字

Linux之网络套接字 一.IP地址和端口号二.TCP和UDP协议2.1网络字节序 三.socket编程的常见API四.模拟实现UDP服务器和客户端五.模拟实现TCP服务器和客户端 一.IP地址和端口号 在了解了网络相关的基础知识之后我们知道了数据在计算机中传输的流程并且发现IP地址在其中占据了确定…...

LeetCode 2266.统计打字方案数:排列组合

【LetMeFly】2266.统计打字方案数&#xff1a;排列组合 力扣题目链接&#xff1a;https://leetcode.cn/problems/count-number-of-texts/ Alice 在给 Bob 用手机打字。数字到字母的 对应 如下图所示。 为了 打出 一个字母&#xff0c;Alice 需要 按 对应字母 i 次&#xff0c…...

Jmeter 动态参数压力测试时间段预定接口

&#x1f3af; 本文档详细介绍了如何使用Apache JMeter进行压力测试&#xff0c;以评估预定接口在高并发场景下的性能表现。通过创建线程组模拟不同数量的用户并发请求&#xff0c;利用CSV文件动态配置时间段ID和用户token&#xff0c;确保了测试数据的真实性和有效性。文档中还…...

Learning Prompt

说明&#xff1a;这是我的学习笔记&#xff0c;很多内容转自网络&#xff0c;请查阅文章末尾的参考资料。 目录 基本要求(C.R.E.A.T.E)总结文章(Summarise)改写文章(Rewrite)根据参考资料回答问题(Question & Answer)参考资料 基本要求(C.R.E.A.T.E) Character This is th…...

微信消息群发(定时群发)-UI自动化产品(基于.Net平台+C#)

整理 | 小耕家的喵大仙 出品 | CSDN&#xff08;ID&#xff1a;lichao19897314&#xff09; 关联源码及工具下载https://download.csdn.net/download/lichao19897314/90096681https://download.csdn.net/download/lichao19897314/90096681https://download.csdn.net/download/…...

华为HuaweiCloudStack(一)介绍与架构

本文简单介绍了华为HCS私有云解决方案&#xff0c;并从下至上介绍HCS的整体架构&#xff0c;部署架构、部署方式等内容。 目录 HCS简介 HCS架构 纵向结构 ?管理平台类型 HCS节点类型 FusionSphere OpenStack CPS ServiceOM SC 运营面 OC 运维面 HCS部署架构 regi…...

【博客之星】2024年度个人成长、强化学习算法领域总结

&#x1f4e2;在2025年初&#xff0c;非常荣幸能通过审核进入到《2024年度CSDN博客之星总评选》TOP300的年度评选中&#xff0c;排名40。这还是第一次来到这个阶段&#xff0c;作为一名博士研究生&#xff0c;还是备受鼓舞的。在这里我将以回顾的方式讲述一下这一年在CSDN中走过…...

Git 分支策略

文章目录 1. Git Flow2. GitHub Flow3. GitLab Flow4. Trunk-Based Development5. Release Flow分支最佳实践 Git 分支策略是组织和管理工作特性开发、协作和版本控制的技术。选择合适的策略取决于团队规模、项目需求和部署需求。以下是常见的 Git 分支策略&#xff1a; 1. Git…...

《自动驾驶与机器人中的SLAM技术》ch8:基于预积分和图优化的紧耦合 LIO 系统

和组合导航一样&#xff0c;也可以通过预积分 IMU 因子加上雷达残差来实现基于预积分和图优化的紧耦合 LIO 系统。一些现代的 Lidar SLAM 系统也采用了这种方式。相比滤波器方法来说&#xff0c;预积分因子可以更方便地整合到现有的优化框架中&#xff0c;从开发到实现都更为便…...

Mysql学习笔记

连接数据库 找到 MySQL 安装目录下的 bin 目录&#xff0c;然后打开命令窗口&#xff0c;在命令窗口中按如下语法输入命令&#xff1a; mysql - h MySQL 数据库服务器的 IP 地址 - u 用户名 - p 然后按下回车键&#xff0c;输入密码即可 数据库操作 创建数据库 CREAT…...

Safari常用快捷键

一、书签边栏 1、显示或隐藏书签边栏&#xff1a;Control-Command-1 2、选择下一个书签或文件夹&#xff1a;向上头键或向下头键 3、打开所选书签&#xff1a;空格键 4、打开所选文件夹&#xff1a;空格键或右箭头键 5、关闭所选文件夹&#xff1a;空格键或左箭头键 6、更…...

OpenEuler学习笔记(二):用通俗的道理讲操作系统原理

用通俗的道理讲操作系统原理 基础概念类比 把OpenEuler操作系统想象成一个大型的工厂&#xff0c;这个工厂有各种各样的部门&#xff0c;每个部门都有自己的职责&#xff0c;共同协作来让整个工厂正常运转。内核就像是工厂的管理中心&#xff0c;它负责指挥和协调所有的工作。 …...

ros2-7.5 做一个自动巡检机器人

7.5.1 需求及设计 又到了小鱼老师带着做最佳实践项目了。需求&#xff1a;做一个在各个房间不断巡逻并记录图像的机器人。 到达目标点后首先通过语音播放到达目标点信息&#xff0c; 再通过摄像头拍摄一张图片保存到本地。 7.5.2 编写巡检控制节点 在chapt7_ws/src下新建功…...

使用 `scanpy` 观察 `AnnData` 对象内部数据结构

以下是使用 scanpy 观察 AnnData 对象内部数据结构的步骤: 一、导入必要的库: import scanpy as sc二、读取 AnnData 对象: 假设你的 AnnData 对象存储在一个文件中,例如 adata.h5ad,你可以使用以下代码读取它: adata = sc.read(adata.h5ad)如果你已经有了 adata 对象…...

《CPython Internals》阅读笔记:p232-p249

《CPython Internals》学习第 13天&#xff0c;p232-p249 总结&#xff0c;总计 18 页。 一、技术总结 无。 二、英语总结(生词&#xff1a;1) 1.overhead (1)overhead: over-(“above”) head(“top part, uppermost section”) overhead的字面意思是&#xff1a;above…...

Java并发08 - 并发安全容器详解

并发容器详解 文章目录 并发容器详解一&#xff1a;不使用并发容器如何保证安全二&#xff1a;阻塞队列容器2&#xff1a;ArrayBlockingQueue2.1&#xff1a;内部成员2.2&#xff1a;put方法的实现2.3&#xff1a;take方法的实现 3&#xff1a;LinkedBlockingQueue3.1&#xff…...

抽奖系统(3——奖品模块)

1. 图片上传 application.properties 配置上传文件路径 ## 文件上传 ## # 目标路径 pic.local-pathD:/PIC # spring boot3 升级配置名 spring.web.resources.static-locationsclasspath:/static/,file:${pic.local-path} tip&#xff1a; 1. 如果访问的是本地路径&#xff0c…...

36.centos7上安装python3.6.5、安装卸载依赖包

查看openssl的版本号&#xff0c;默认python3.6.5需要OpenSSL 1.0.2以上的版本支持。 监测安装好的python,是否可以正确导入ssl和_ssl包 pip3安装依赖包 通过Pycharm工具导出requirements.txt文件 查看/usr/bin/目录下的软连接 pip3, python...

微透镜阵列精准全检,白光干涉3D自动量测方案提效70%

广泛应用的微透镜阵列 微透镜是一种常见的微光学元件&#xff0c;通过设计微透镜&#xff0c;可对入射光进行扩散、光束整形、光线均分、光学聚焦、集成成像等调制&#xff0c;进而实现许多传统光学元器件难以实现的特殊功能。 微透镜阵列&#xff08;Microlens Array&#x…...

nature genetics | scATAC-seq预测scRNA-seq,识别影响基因表达的新染色质区域

–https://doi.org/10.1038/s41588-024-01689-8 Single-cell multi-ome regression models identify functional and disease-associated enhancers and enable chromatin potential analysis 研究团队和单位 Christina S. Leslie–Memorial Sloan Kettering Cancer Center …...

简述mysql 主从复制原理及其工作过程,配置一主两从并验证。

MySQL 主从同步是一种数据库复制技术&#xff0c;它通过将主服务器上的数据更改复制到一个或多个从服务器&#xff0c;实现数据的自动同步。 主从同步的核心原理是将主服务器上的二进制日志复制到从服务器&#xff0c;并在从服务器上执行这些日志中的操作。 MySQL主从同步是基…...

Java API:封装自定义响应类

本文介绍 Web 服务开发中自定义响应&#xff0c;涵盖标准 HTTP 响应状态码局限性、自定义响应价值、设计原则与实现、在 Spring Boot 项目应用、与其他响应格式对比总结及应用场景。 1. 标准HTTP响应与自定义响应 1.1标准HTTP响应状态码 在 Web 服务开发中&#xff0c;HTTP…...

【Unity3D】利用Hinge Joint 2D组件制作绳索效果

目录 一、动态绳索 &#xff08;可移动根节点&#xff09; 二、静态绳索 三、利用Skinning Editor(Unity2022.3.15f1正常使用) 四、注意事项 一、动态绳索 &#xff08;可移动根节点&#xff09; 动态绳索 DynamicRope空物体 Anchor和whitecircle是相同位置的物体&#xff…...

vim练级攻略(精简版)

vim推荐配置: curl -sLf https://gitee.com/HGtz2222/VimForCpp/raw/master/install.sh -o ./install.sh && bash ./install.sh 0. 规定 Ctrl-λ 等价于 <C-λ> :command 等价于 :command <回车> n 等价于 数字 blank字符 等价于 空格&#xff0c;tab&am…...

嵌入式硬件篇---PID控制

文章目录 前言第一部分&#xff1a;连续PID1.比例&#xff08;Proportional&#xff0c;P&#xff09;控制2.积分&#xff08;Integral&#xff0c;I&#xff09;控制3.微分&#xff08;Derivative&#xff0c;D&#xff09;控制4.PID的工作原理5..实质6.分析7.各种PID控制器P控…...

技术洞察:C++在后端开发中的前沿趋势与社会影响

文章目录 引言C在后端开发中的前沿趋势1. 高性能计算的需求2. 微服务架构的兴起3. 跨平台开发的便利性 跨领域技术融合与创新实践1. C与人工智能的结合2. C与区块链技术的融合 C对社会与人文的影响1. 提升生产力与创新能力2. 促进技术教育与人才培养3. 技术与人文的深度融合 结…...

C语言程序设计之小系统

&#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 目录 系统说明 1.1 系统概述 1.2 功能模块总体设计详细设计 3.1 程序中使用的函数 3.2各类问…...

pyinstaller : 无法将“pyinstaller”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。

pyinstaller : 无法将“pyinstaller”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff0c;请确保路径正确&#xff0c;然后再试一次。 所在位置 行:1 字符: 1pyinstaller --onefile --windowed 过年烟花.py~~~~~~~~~~~ …...

接口传参 data格式和json格式区别是什么

接口传参 data格式和json格式区别是什么 以下是接口传参 data 格式和 JSON 格式的区别&#xff1a; 定义和范围 Data 格式&#xff1a; 是一个较为宽泛的概念&#xff0c;它可以指代接口传递参数时所使用的任何数据的组织形式。包括但不限于 JSON、XML、Form 数据、纯文本、二进…...

ClickHouse 入门

简介 ClickHouse 是一个列式数据库&#xff0c;传统的数据库一般是按行存储&#xff0c;而ClickHouse则是按列存储&#xff0c;每一列都有自己的存储空间&#xff0c;并且只存储该列的数值&#xff0c;而不是存储整行的数据。这样做主要有几个好处&#xff0c;压缩率高&#x…...

Python自动化:基于faker批量生成模拟数据(以电商行业销售数据为例)

引言&#xff1a;个人认为&#xff0c;“造数据”是一个数据分析师的一项基本技能&#xff0c;当然啦&#xff0c;“造数据”不是说胡编乱造&#xff0c;而是根据自己的需求去构造一些模拟数据集&#xff0c;用于测试等用途&#xff0c;而且使用虚拟数据不用担心数据隐私和安全…...

3.3 OpenAI GPT-4, GPT-3.5, GPT-3 模型调用:开发者指南

OpenAI GPT-4, GPT-3.5, GPT-3 模型调用:开发者指南 OpenAI 的 GPT 系列语言模型,包括 GPT-4、GPT-3.5 和 GPT-3,已经成为自然语言处理领域的标杆。无论是文本生成、对话系统,还是自动化任务,开发者都可以通过 API 调用这些强大的模型来增强他们的应用。本文将为您详细介…...

【Spring Boot】掌握 Spring 事务:隔离级别与传播机制解读与应用

前言 &#x1f31f;&#x1f31f;本期讲解关于spring 事务传播机制介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话…...

力扣203题—— 移除链表元素

题目 递归法使用 if(headnull){return null; }//假设remove返回后面已经去掉val值的链表 我们用head.next去存放他&#xff0c;接着我们要判断此时head head值是否等于val&#xff0c;如果等于我们就返回后继元素即可 head.nextremove(head.next,val); if(head.valval){return…...

Express中间件

目录 Express中间件 中间件的概念 next函数 全局中间与局部中间件 多个中间件 中间的5个注意事项 中间的分类 应用级中间件 路由级中间件 错误级中间件 Express内置中间件 express.json express.urlencoded 第三方中间件​编辑 自定义中间件 Express中间件 中间…...

【AIGC】SYNCAMMASTER:多视角多像机的视频生成

标题&#xff1a;SYNCAMMASTER: SYNCHRONIZING MULTI-CAMERA VIDEO GENERATION FROM DIVERSE VIEWPOINTS 主页&#xff1a;https://jianhongbai.github.io/SynCamMaster/ 代码&#xff1a;https://github.com/KwaiVGI/SynCamMaster 文章目录 摘要一、引言二、使用步骤2.1 TextT…...

模块化架构与微服务架构,哪种更适合桌面软件开发?

前言 在现代软件开发中&#xff0c;架构设计扮演着至关重要的角色。两种常见的架构设计方法是模块化架构与微服务架构。它们各自有独特的优势和适用场景&#xff0c;尤其在C#桌面软件开发领域&#xff0c;模块化架构往往更加具有实践性。本文将对这两种架构进行对比&#xff0…...

Ubuntu 24.04 LTS 安装 tailscale 并访问 SMB共享文件夹

Ubuntu 24.04 LTS 安装 tailscale 安装 Tailscale 官方仓库 首先&#xff0c;确保系统包列表是最新的&#xff1a; sudo apt update接下来&#xff0c;安装 Tailscale 所需的仓库和密钥&#xff1a; curl -fsSL https://tailscale.com/install.sh | sh这会自动下载并安装 …...

fgets、scanf存字符串应用

题目1 夺旗&#xff08;英语&#xff1a;Capture the flag&#xff0c;简称 CTF&#xff09;在计算机安全中是一种活动&#xff0c;当中会将“旗子”秘密地埋藏于有目的的易受攻击的程序或网站。参赛者从其他参赛者或主办方偷去旗子。 非常崇拜探姬的小学妹最近迷上了 CTF&am…...

C#高级:用Csharp操作鼠标和键盘

一、winform 1.实时获取鼠标位置 public Form1() {InitializeComponent();InitialTime(); }private void InitialTime() {// 初始化 Timer 控件var timer new System.Windows.Forms.Timer();timer.Interval 100; // 设置为 100 毫秒&#xff0c;即每 0.1 秒更新一次timer.…...

关于AI agent的学术论文实验部分:准确率,响应时间,用户满意度

关于AI agent的学术论文实验部分 在撰写关于AI agent的学术论文时,实验设计和实施是关键部分,仅搭建完成AI agent通常是不够的,需要通过严谨的实验来验证其性能、效果和创新性。以下以一个在智能客服场景中应用AI agent的例子,说明如何完成实验: 明确实验目的:确定通过实…...

消息队列实战指南:三大MQ 与 Kafka 适用场景全解析

前言&#xff1a;在当今数字化时代&#xff0c;分布式系统和大数据处理变得愈发普遍&#xff0c;消息队列作为其中的关键组件&#xff0c;承担着系统解耦、异步通信、流量削峰等重要职责。ActiveMQ、RabbitMQ、RocketMQ 和 Kafka 作为市场上极具代表性的消息队列产品&#xff0…...

postgresql表分区及测试

本文主要采用list类型实现表分区&#xff0c;并对表分区数据进行查询对比&#xff0c;数据量6000万条以上&#xff0c;速度相差10倍以上。 一、创建表&#xff0c;以substationcode字段为ist类型表分区 CREATE TABLE "public"."d_population_partition" …...

VUE学习笔记(入门)1__创建VUE实例

核心步骤 <div id"app"><!-- 这里存放渲染逻辑代码 --><h1>{{ msg }}</h1><a href"#">{{count}}</a> </div><!-- 引入在线的开发版本核心包 --> <!-- 引入核心包后全局可使用VUE构造函数 --> <…...

STL—stack与queue

目录 Stack stack的使用 stack的模拟实现 queue queue的使用 queue的模拟实现 priority_queue priority_queue的用法 priority_queue的模拟实现 容器适配器 种类 Stack http://www.cplusplus.com/reference/stack/stack/?kwstack stack是栈&#xff0c;后入先出 stack的…...

pthread_create函数

函数原型 pthread_create 是 POSIX 线程&#xff08;pthread&#xff09;库中的一个函数&#xff0c;用于在程序中创建一个新线程。 #include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *a…...

suctf2025

Suctf2025 --2标识为看的wp&#xff0c;没环境复现了 所有参考资料将在文本末尾标明 WEB SU_photogallery 思路&#x1f447; 构造一个压缩包&#xff0c;解压出我们想解压的部分&#xff0c;然后其他部分是损坏的&#xff0c;这样是不是就可以让整个解压过程是出错的从而…...

二、点灯基础实验

嵌入式基础实验第一个就是点灯&#xff0c;地位相当于编程界的hello world。 如下为LED原理图&#xff0c;要让相应LED发光&#xff0c;需要给I/O口设置输出引脚&#xff0c;低电平&#xff0c;二极管才会导通 2.1 打开初始工程&#xff0c;编写代码 以下会实现BLINKY常亮&…...

ESP8266-01S、手机、STM32连接

1、ESP8266-01S的工作原理 1.1、AP和STA ESP8266-01S为WIFI的透传模块&#xff0c;主要模式如下图&#xff1a; 上节说到&#xff0c;我们需要用到AT固件进行局域网应用&#xff08;ESP8266连接的STM32和手机进行连接&#xff09;。 ESP8266为一个WiFi透传模块&#xff0c;和…...

微服务学习:基础理论

一、微服务和应用现代化 1、时代的浪潮&#xff0c;企业的机遇和挑战 在互联网化数字化智能化全球化的当今社会&#xff0c;IT行业也面临新的挑战&#xff1a; 【快】业务需求如“滔滔江水连绵不绝”&#xff0c;企业需要更快的交付【变】林子大了&#xff0c;百色用户&…...