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

Linux网络--Socket 编程 TCP

大家好,上次我们学习了有关UDP的socket编程,今天我们来继续学习一下关于TCP的socket编程,那么我们开始今天的学习。

目录

1. TCP socket API 详解

1.1 socket

1.2 bind

1.3 listen

1.4 accept

1.5 connect

2. Echo Server

2.1 Echo Server 多进程版本

2.2 Echo Server 多线程版本

2.3 Echo Server 线程池版本


1. TCP socket API 详解

首先我们来看一下关于TCP的socket编程的相关接口:

1.1 socket

1. socket() 打开一个网络通讯端口 , 如果成功的话 , 就像 open() 一样返回一个文件描述符;
2. 应用程序可以像读写文件一样用 read/write 在网络上收发数据 ;
3. 如果 socket() 调用出错则返回 -1;
4. 对于 IPv4, family 参数指定为 AF_INET;
5. 对于 TCP 协议 ,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
6. protocol 参数的介绍从略 , 指定为 0 即可。

与UDP不同的是,在type参数中,UDP传入的是SOCK_DRGAM,TCP传入的是SOCK_STREAM,其余相同。

1.2 bind

1. 服务器程序所监听的网络地址和端口号通常是固定不变的 , 客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;
2. bind() 成功返回 0, 失败返回 -1
3. bind() 的作用是将参数 sockfd myaddr 绑定在一起 , 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号 ;
4. 前面讲过 ,struct sockaddr * 是一个通用指针类型 ,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体 , 而它们的长度各不相同 , 所以需要第三个参数 addrlen指定结构体的长度;

此接口使用与UDP处无异。

1.3 listen

1. listen() 声明 sockfd 处于监听状态 , 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略 , 这里设置不会太大 ( 一般是 5) ;
2. listen() 成功返回 0, 失败返回 -1;
1.4 accept

1. 三次握手完成后 , 服务器调用 accept() 接受连接 ;
2. 如果服务器调用 accept() 时还没有客户端的连接请求 , 就阻塞等待直到有客户端连接上来;
3. addr 是一个传出参数 ,accept() 返回时传出客户端的地址和端口号 ;
4. 如果给 addr 参数传 NULL, 表示不关心客户端的地址 ;
5. addrlen 参数是一个传入传出参数 (value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题 , 传出的是客户端地址结构体的实际长度( 有可能没有占满调用者提供的缓冲区 );

服务器端server通过accept来与客户端client进行连接。

1.5 connect

1. 客户端需要调用 connect() 连接服务器 ;
2. connect bind 的参数形式一致 , 区别在于 bind 的参数是自己的地址 , 而 connect 的参数是对方的地址 ;
3. connect() 成功返回 0, 出错返回 -1;

客户端client通过connect来与服务器端server进行连接。

介绍完了这些接口,下面我们继续看几个有关TCP的服务器实现:

2. Echo Server

Common.hpp:

#pragma once#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>enum ExitCode
{OK = 0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR
};class NoCopy
{
public:NoCopy(){}~NoCopy(){}NoCopy(const NoCopy &) = delete;const NoCopy &operator = (const NoCopy&) = delete;
};#define CONV(addr) ((struct sockaddr*)&addr)

由于服务器对象通常无法进行赋值和拷贝,所以我们定义基类将拷贝构造和赋值重载进行删除,且在Common.hpp中枚举了一些退出码,当socket等接口运行出错时,使用这些退出码进行退出。

InetAddr.hpp:

#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类class InetAddr
{
public:InetAddr(){}InetAddr(struct sockaddr_in &addr) : _addr(addr){// 网络转主机_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列// _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IPchar ipbuffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));_ip = ipbuffer;}InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port){// 主机转网络memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO}InetAddr(uint16_t port) :_port(port),_ip(){// 主机转网络memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_addr.s_addr = INADDR_ANY;_addr.sin_port = htons(_port);}uint16_t Port() { return _port; }std::string Ip() { return _ip; }const struct sockaddr_in &NetAddr() { return _addr; }const struct sockaddr *NetAddrPtr(){return CONV(_addr);}socklen_t NetAddrLen(){return sizeof(_addr);}bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};

我们将各种有关网络和主机间的转换接口都封装在InetAddr.hpp中。

TcpServer.hpp:

#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&, InetAddr &)>;const static int defaultsockfd = -1;
const static int backlog = 8;class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port, func_t func) : _port(port),_listensockfd(defaultsockfd),_isrunning(false),_func(func){}void Init(){// signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法// 1. 创建套接字文件_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3// 2. bind众所周知的端口号InetAddr local(_port);int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);}LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3// 3. 设置socket状态为listenn = listen(_listensockfd, backlog);if (n < 0){LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3}void Service(int sockfd, InetAddr &peer){char buffer[1024];while (true){// 1. 先读取数据// a. n>0: 读取成功// b. n<0: 读取失败// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipessize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// buffer是一个英文单词 or 是一个命令字符串buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;std::string echo_string = _func(buffer, peer);// // 2. 写回数据// std::string echo_string = "echo# ";// echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";close(sockfd);break;}}}void Run(){_isrunning = true;while (_isrunning){// a. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 如果没有连接,accept就会阻塞int sockfd = accept(_listensockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr addr(peer);LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();单进程程序 --- 不会存在的!Service(sockfd, addr);}_isrunning = false;}~TcpServer(){}private:uint16_t _port;int _listensockfd; // 监听socketbool _isrunning;func_t _func; // 设置回调处理
};

TcpServer.cc:

#include "TcpServer.hpp"std::string defaulthandler(const std::string &word, InetAddr &addr)
{LOG(LogLevel::DEBUG) << "回调到了defaulthandler";std::string s = "haha, ";s += word;return s;
}void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}// 远程命令执行的功能!
// ./tcpserver port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,defaulthandler);tsvr->Init();tsvr->Run();return 0;
}

TcpClient.cc:

#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;exit(SOCKET_ERR);}// 2. bind吗??需要。显式的bind?不需要!随机方式选择端口号// 2. 我应该做什么呢?listen?accept?都不需要!!// 2. 直接向目标服务器发起建立连接的请求InetAddr serveraddr(serverip, serverport);int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());if(n < 0){std::cerr << "connect error" << std::endl;exit(CONNECT_ERR);}// 3. echo clientwhile(true){std::string line;std::cout << "Please Enter@ ";std::getline(std::cin, line);write(sockfd, line.c_str(), line.size());char buffer[1024];ssize_t size = read(sockfd, buffer, sizeof(buffer)-1);if(size > 0){buffer[size] = 0;std::cout << "server echo# " << buffer << std::endl;}}close(sockfd);return 0;
}

这就是一个回显Echo Server服务器的代码,但是这其中有些问题,就是当我们服务器端server接收到客户端client之后会转到Service函数当中,此时若有新的客户端client请求连接,那服务器端server是无法接收的,所以这里是不可行的,我们要对其进行改造升级。

2.1 Echo Server 多进程版本

这里只给出TcpServer.hpp文件的内容,其他部分基本相同,就不再次给出了。

#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&, InetAddr &)>;const static int defaultsockfd = -1;
const static int backlog = 8;class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port, func_t func) : _port(port),_listensockfd(defaultsockfd),_isrunning(false),_func(func){}void Init(){// signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法// 1. 创建套接字文件_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3// 2. bind众所周知的端口号InetAddr local(_port);int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);}LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3// 3. 设置socket状态为listenn = listen(_listensockfd, backlog);if (n < 0){LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3}// 短服务// 长服务: 多进程多线程比较合适void Service(int sockfd, InetAddr &peer){char buffer[1024];while (true){// 1. 先读取数据// a. n>0: 读取成功// b. n<0: 读取失败// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipessize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// buffer是一个英文单词 or 是一个命令字符串buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;std::string echo_string = _func(buffer, peer);// // 2. 写回数据// std::string echo_string = "echo# ";// echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";close(sockfd);break;}}}void Run(){_isrunning = true;while (_isrunning){// a. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 如果没有连接,accept就会阻塞int sockfd = accept(_listensockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr addr(peer);LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();version1 --- 多进程版本pid_t id = fork(); // 父进程if(id < 0){LOG(LogLevel::FATAL) << "fork error";exit(FORK_ERR);}else if(id == 0){// 子进程,子进程除了看到sockfd,能看到listensockfd吗??// 我们不想让子进程访问listensock!close(_listensockfd);if(fork() > 0) // 再次fork,子进程退出exit(OK);Service(sockfd, addr); // 孙子进程,孤儿进程,1, 系统回收我exit(OK);}else{//父进程close(sockfd);//父进程是不是要等待子进程啊,要不然僵尸了??pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了(void)rid;}_isrunning = false;}~TcpServer(){}private:uint16_t _port;int _listensockfd; // 监听socketbool _isrunning;func_t _func; // 设置回调处理
};

父进程负责用来接收客户端client的连接,故不需要accept函数的返回值;子进程负责调用Service函数,不需要_listensockfd,这里将其进行回收。

父进程需要对子进程进行等待,否则子进程资源不会释放编程僵尸进程,但若是父进程调用waitpid函数还要进行阻塞,所以我们在子进程中继续创建子进程,创建之后让父进程直接退出,而它的子进程成为了孤儿进程交给操作系统进行回收。

2.2 Echo Server 多线程版本
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&, InetAddr &)>;const static int defaultsockfd = -1;
const static int backlog = 8;class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port, func_t func) : _port(port),_listensockfd(defaultsockfd),_isrunning(false),_func(func){}void Init(){// signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法// 1. 创建套接字文件_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3// 2. bind众所周知的端口号InetAddr local(_port);int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);}LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3// 3. 设置socket状态为listenn = listen(_listensockfd, backlog);if (n < 0){LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3}class ThreadData{public:ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s){}public:int sockfd;InetAddr addr;TcpServer *tsvr;};// 短服务// 长服务: 多进程多线程比较合适void Service(int sockfd, InetAddr &peer){char buffer[1024];while (true){// 1. 先读取数据// a. n>0: 读取成功// b. n<0: 读取失败// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipessize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// buffer是一个英文单词 or 是一个命令字符串buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;std::string echo_string = _func(buffer, peer);// // 2. 写回数据// std::string echo_string = "echo# ";// echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";close(sockfd);break;}}}static void *Routine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->tsvr->Service(td->sockfd, td->addr);delete td;return nullptr;}void Run(){_isrunning = true;while (_isrunning){// a. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 如果没有连接,accept就会阻塞int sockfd = accept(_listensockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr addr(peer);LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();// version2: 多线程版本ThreadData *td = new ThreadData(sockfd, addr, this);pthread_t tid;pthread_create(&tid, nullptr, Routine, td);_isrunning = false;}~TcpServer(){}private:uint16_t _port;int _listensockfd; // 监听socketbool _isrunning;func_t _func; // 设置回调处理
};

这里直接创建线程来进行Service函数的调用。

2.3 Echo Server 线程池版本
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&, InetAddr &)>;const static int defaultsockfd = -1;
const static int backlog = 8;class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port, func_t func) : _port(port),_listensockfd(defaultsockfd),_isrunning(false),_func(func){}void Init(){// signal(SIGCHLD, SIG_IGN); // 忽略SIG_IGN信号,推荐的做法// 1. 创建套接字文件_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3// 2. bind众所周知的端口号InetAddr local(_port);int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);}LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3// 3. 设置socket状态为listenn = listen(_listensockfd, backlog);if (n < 0){LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success: " << _listensockfd; // 3}// 短服务// 长服务: 多进程多线程比较合适void Service(int sockfd, InetAddr &peer){char buffer[1024];while (true){// 1. 先读取数据// a. n>0: 读取成功// b. n<0: 读取失败// c. n==0: 对端把链接关闭了,读到了文件的结尾 --- pipessize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// buffer是一个英文单词 or 是一个命令字符串buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;std::string echo_string = _func(buffer, peer);// // 2. 写回数据// std::string echo_string = "echo# ";// echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";close(sockfd);break;}}}void Run(){_isrunning = true;while (_isrunning){// a. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 如果没有连接,accept就会阻塞int sockfd = accept(_listensockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr addr(peer);LOG(LogLevel::INFO) << "accept success, peer addr : " << addr.StringAddr();// version3:线程池版本,线程池一般比较适合处理短服务// 将新链接和客户端构建一个新的任务,push线程池中ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){this->Service(sockfd, addr);});}_isrunning = false;}~TcpServer(){}private:uint16_t _port;int _listensockfd; // 监听socketbool _isrunning;func_t _func; // 设置回调处理
};

因为线程池中的线程个数是有限的,所以当服务器server中连接的客户端client多起来的时候,用线程池就有些麻烦了,所以线程池时候短服务,而多进程和多线程的模式更适合长服务。

总之,TCP的使用与UDP基本无异,所以就不再写其他服务器实现了,以上就是今天的内容,我们下次再见!

http://www.dtcms.com/a/605337.html

相关文章:

  • 【一文了解】C#反射
  • 网站建设seo推广外贸网站建设海外推广
  • 网站ip域名查询安徽省住房城乡建设厅网站电工
  • 202511-Selenium技术深度解析:Web自动化测试的王者之路
  • Android 打开 在线 pdf 文件
  • Python 教程:如何快速在 PDF 中添加水印(文字、图片)
  • 普中51单片机学习笔记-矩阵按键
  • 视觉语言模型新突破!开源项目解读
  • 深圳南山区住房和建设局网站官网天天向上做图网站
  • 微算法科技(NASDAQ MLGO)通过容量证明(PoC)构建全球存储资源池,为Web3应用提供低成本、抗审查的数据存储服务
  • 08-微服务原理篇(Canal-Redis)
  • 填写网站备案信息深圳建设材料价格网站
  • 【Spring Boot 报错已解决】Spring Boot开发避坑指南:Hibernate实体类主键配置详解与异常修复
  • 【CSS】cursor: auto, default, none 有什么区别?
  • 网站备案负责人三网合一营销型全网站
  • 7.2 Dify核心功能与技术架构:前后端分离、API接口、数据存储
  • 观察Springboot AI-Function Tools 执行过程
  • 信贷风控建设的多维意义解析
  • 如何在产品已上线后发现需求遗漏进行补救
  • 重卡充电桩平台支持针对不同车队单独配置计费规则
  • 美丽寮步网站建设高性能广州公关公司有哪些
  • Linux告别搜索卡顿:解决“Argument list too long”与实现文件内容秒搜
  • .NET驾驭Excel之力:工作簿与工作表操作基础
  • 基于 C++ OpenCV 生成小视频
  • 个人网站审批网站防止采集
  • 5.6 Multiple region interfaces
  • 聊聊缓存测试用例设计方案
  • IU5516T低功耗,1M@2.0A降压稳压器
  • Arbess从初级到进阶(3) - 使用Arbess+GitLab+SonarQube搭建Java项目自动化部署
  • 外贸的网站有哪些网站开发心得体会