TCP并发服务器(多进程与多线程)

欢迎关注博主 Mindtechnist 或加入【Linux C/C++/Python社区】一起探讨和分享Linux C/C++/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。
TCP并发服务器(多进程与多线程)
- 1. 多进程并发服务器
- (1)什么是并发
- (2)多进程并发服务器需要注意的几个要点
- (3)读时共享写时复制详解
- 2. 多进程并发服务器代码实现
- 3. 多线程并发服务器
- 4. 多线程并发服务器代码实现
- 5. 扩展:Socket API封装
专栏:《Linux从小白到大神》《网络编程》
1. 多进程并发服务器
我们在上一节写的TCP服务器只能处理单连接,在代码实现时,多进程并发服务器与非并发服务器在创建监听套接字、绑定、监听这几个步骤是一样的,但是在接收连接请求的时候,多进程并发服务器是这样实现的:父进程负责接受连接请求,一旦连接成功,将会创建一个子进程与客户端通信。示意图如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoUooLkG-1676856429297)(Typora_picture_reference/1661857992909.png)]](https://img-blog.csdnimg.cn/2cd38508395a4bc5a9293dafd37983bf.png)
(1)什么是并发
-
单核CPU → 多进程/线程并发 → 时间片轮转
-
并发 → 某一个时间片/点所能处理的任务数
-
服务器并发:服务器在某个时间点/片所能处理的连接数所能接收的client连接越多,并发量越大
(2)多进程并发服务器需要注意的几个要点
使用多进程的方式来解决服务器处理多连接的问题,需要注意下面几点:
- 共享:读时共享、写时复制。有血缘关系的进程间将会共享
- 文件描述符
- 内存映射区mmap
- 父进程扮演什么角色?
- 等待接受客户端连接accept()
- 有连接的时候通过fork()创建一个子进程。父进程只负责等待客户端连接,即通过accept()阻塞等待连接请求,一旦有连接请求,马上通过fork()创建一个子进程,子进程通过共享父进程的文件描述符来实现和client通信。
- 将用于通信的文件描述符关闭。accept()接受连接请求后会返回一个用于通信的文件描述符,而父进程的职责是等待连接并fork()创建用于通信的子进程,所以对于父进程来说,用于通信的文件描述符是没有用处的,关闭该文件描述符来节省开销。我们知道,文件描述符是有上限的,最多1024个(0-1023),如果不关闭的话,每次fork()一个子进程都要浪费一个文件描述符,如果进程多了,可能文件描述符就不够用了。
- 等待接受客户端连接accept()
- 子进程扮演什么角色?
- 通信。通过共享的父进程accept()返回的文件描述符来与客户端通信。
- 将用于监听的文件描述符关闭。同样是为了节省资源,子进程被fork()出来后也会拥有一个用于监听的文件描述符(因为子进程是对父进程的拷贝),但是子进程的作用是与客户端通信,所以用于监听的文件描述符对子进程而言并无用处,关闭以节省资源。
- 创建的子进程个数有限制吗?
- 受硬件限制
- 文件描述符默认上限1024
- 子进程资源回收
- wait/waitpid
- 使用信号回收
- signal
- sigaction
- 捕捉信号SIGCHLD
(3)读时共享写时复制详解
首先看图
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2isLxIfZ-1676856429298)(Typora_picture_reference/1661859472789.png)]](https://img-blog.csdnimg.cn/4d9fb1a867624c3789417138ef2c6708.png)
如果父子进程都只是读数据,那么他们都通过虚拟地址去访问1号物理地址的内容,如果此时父进程修改了数据a=8,那么父进程会先复制一份数据到2号内存,然后修改2号内存的数据,父进程再读的时候就去2号内存读,而子进程依然去1号内存读。如果子进程也要修改这个全局变量,那么子进程也会拷贝一份数据到内存3,然后修改内存3的数据,子进程访问数据时会访问内存3的数据。(多个子进程就会拷贝多份)
2. 多进程并发服务器代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>// 进程回收函数
void recyle(int num)
{pid_t pid;while( (pid = waitpid(-1, NULL, WNOHANG)) > 0 ){printf("child died , pid = %d\n", pid);}
}int main(int argc, const char* argv[])
{if(argc < 2){printf("eg: ./a.out port\n");exit(1);}struct sockaddr_in serv_addr;socklen_t serv_len = sizeof(serv_addr);int port = atoi(argv[1]);// 创建套接字int lfd = socket(AF_INET, SOCK_STREAM, 0);// 初始化服务器 sockaddr_in memset(&serv_addr, 0, serv_len);serv_addr.sin_family = AF_INET; // 地址族 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IPserv_addr.sin_port = htons(port); // 设置端口 // 绑定IP和端口bind(lfd, (struct sockaddr*)&serv_addr, serv_len);// 设置同时监听的最大个数listen(lfd, 36);printf("Start accept ......\n");// 使用信号回收子进程pcb //这个子进程回收机制会被子进程复制struct sigaction act;act.sa_handler = recyle;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaction(SIGCHLD, &act, NULL);struct sockaddr_in client_addr;socklen_t cli_len = sizeof(client_addr);while(1){// 父进程接收连接请求// accept阻塞的时候被信号中断, 处理信号对应的操作之后(比如子进程终止,收到信号后去回收子进程)// 回来之后不阻塞了, 直接返回-1, 这时候 errno==EINTRint cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);//解决方法就是,在一个循环中判断,如果accept阻塞过程中被信号打断//也就是返回值-1且errno == EINTR,那么再一次调用accept//这样accept会再次回到阻塞状态,并且返回值不是-1,也就不会进入循环//等到再次被信号打断的时候才会再次进入循环/*这里的cfd虽然只定义了一个,但是在每个子进程中都会有一个拷贝,并且修改一个子进程的cfd不会影响其它子进程*/while(cfd == -1 && errno == EINTR){cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);}printf("connect sucessful\n");// 创建子进程pid_t pid = fork();if(pid == 0){close(lfd);// child process// 通信char ip[64];while(1){// client ip portprintf("client IP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),ntohs(client_addr.sin_port));char buf[1024];int len = read(cfd, buf, sizeof(buf));if(len == -1){perror("read error");exit(1);}else if(len == 0){printf("客户端断开了连接\n");close(cfd);break;}else{printf("recv buf: %s\n", buf);write(cfd, buf, len);}}// 干掉子进程return 0;}else if(pid > 0){// parent processclose(cfd);}}close(lfd);return 0;
}
3. 多线程并发服务器
多线程并发服务器示意图如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kq3yPqaI-1676856429299)(Typora_picture_reference/1661859902571.png)]](https://img-blog.csdnimg.cn/984e7baa5cba429b83991650b00908f9.png)
在多进程模型中,fork得到的子进程会复制父进程的文件描述符cfd等信息,每个进程的cfd都是自己的,操作互不影响。但是线程不同,现在只有主线程的cfd,多个线程间的信息是共享的,假如说传递给每个子线程的cfd都是同一个,那么线程1修改该文件描述符指向的内容会影响到线程2的通信,因为它们共享这一个文件描述符。所以这里需要建立一个文件描述符数组,每个子线程对应数组中的一个文件描述符。
另外连接主线程的client是哪一个,也就是说哪个client对应和哪个子线程通信,这也需要把和子线程通信的client的ip和port传给和该client通信的子线程,这样子线程才能知道通信的客户端的ip和port。
于是我们需要创建一个结构体数组,每个子线程对应结构体数组中的一个成员,而结构体数组中的每个成员将作为参数传递给子进程的回调函数。
归根到底就是因为,进程是独立的,线程是共享的。
线程共享下面的资源:
- 全局数据区
- 堆区
- 一块有效内存的地址,比如说把线程1的一块内存的地址传给线程2,那么线程2也可以操作这块内存。
4. 多线程并发服务器代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>// 自定义数据结构 //把线程处理函数所需要的信息封装进来
typedef struct SockInfo
{int fd; // 文件描述符struct sockaddr_in addr; //ip地址结构体pthread_t id; //线程id
}SockInfo;// 子线程处理函数
void* worker(void* arg)
{char ip[64];char buf[1024];SockInfo* info = (SockInfo*)arg;// 通信while(1){printf("Client IP: %s, port: %d\n",inet_ntop(AF_INET, &info->addr.sin_addr.s_addr, ip, sizeof(ip)),ntohs(info->addr.sin_port));int len = read(info->fd, buf, sizeof(buf));if(len == -1){perror("read error");pthread_exit(NULL); //只退出子线程//exit(1); //exit会把主线程也一块退出}else if(len == 0){printf("客户端已经断开了连接\n");close(info->fd);break;}else{printf("recv buf: %s\n", buf);write(info->fd, buf, len);}}return NULL;
}int main(int argc, const char* argv[])
{if(argc < 2){printf("eg: ./a.out port\n");exit(1);}struct sockaddr_in serv_addr;socklen_t serv_len = sizeof(serv_addr);int port = atoi(argv[1]);// 创建套接字int lfd = socket(AF_INET, SOCK_STREAM, 0);// 初始化服务器 sockaddr_in memset(&serv_addr, 0, serv_len);serv_addr.sin_family = AF_INET; // 地址族 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IPserv_addr.sin_port = htons(port); // 设置端口 // 绑定IP和端口bind(lfd, (struct sockaddr*)&serv_addr, serv_len);// 设置同时监听的最大个数listen(lfd, 36);printf("Start accept ......\n");int i = 0;SockInfo info[256]; //每个线程对应数组的一个元素,最多256个线程// 规定 fd == -1 说明这是一个无效文件描述符,也就是说这个文件描述符是空闲的,没被占用for(i=0; i<sizeof(info)/sizeof(info[0]); ++i){info[i].fd = -1; //所有文件描述符全部初始化为-1}socklen_t cli_len = sizeof(struct sockaddr_in);while(1){// 选一个没有被使用的, 最小的数组元素//因为有可能我们使用的文件描述符对应数组下标i已经累加到了100,但是前面//99个都已经被释放了(断开连接了),我们最好选用一个当前空闲的数组下标最小//的文件描述符,以合理利用资源for(i=0; i<256; ++i){if(info[i].fd == -1){break; //这样就能把数组下标最小的fd找出来,并确保i指向它,直接break出去}}if(i == 256) //整个数组都被用完了,直接break出while循环{break;}// 主线程 - 等待接受连接请求info[i].fd = accept(lfd, (struct sockaddr*)&info[i].addr, &cli_len); //第二个参数是传出参数,//传出客户端ip信息(struct sockaddr*)类型// 创建子线程 - 通信pthread_create(&info[i].id, NULL, worker, &info[i]);// 设置线程分离 //这样子线程终止的时候会自动释放,就不需要主线程去释放了pthread_detach(info[i].id);}close(lfd);// 只退出主线程 //对子线程无影响,子线程可以继续通信pthread_exit(NULL);return 0;
}
5. 扩展:Socket API封装
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>void perr_exit(const char *s)
{perror(s);exit(-1);
}//也可以在vim下按2K跳转到man文档中的accept函数,因为man文档跳转不区分大小写
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{int n;again:if ((n = accept(fd, sa, salenptr)) < 0) {//ECONNABORTED 发生在重传(一定次数)失败后,强制关闭套接字//EINTR 进程被信号中断 //如果accept函数在阻塞时被信号打断,处理完信号//返回时就不会在阻塞了,而是直接返回-1if ((errno == ECONNABORTED) || (errno == EINTR)){goto again; //如果accept阻塞时被信号打断了,需要在执行一次accept继续阻塞}else{perr_exit("accept error");}}return n;
}int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{int n;if ((n = bind(fd, sa, salen)) < 0){perr_exit("bind error");}return n;
}int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{int n;n = connect(fd, sa, salen);if (n < 0) {perr_exit("connect error");}return n;
}int Listen(int fd, int backlog)
{int n;if ((n = listen(fd, backlog)) < 0){perr_exit("listen error");}return n;
}int Socket(int family, int type, int protocol)
{int n;if ((n = socket(family, type, protocol)) < 0){perr_exit("socket error");}return n;
}ssize_t Read(int fd, void *ptr, size_t nbytes)
{ssize_t n;again:if ( (n = read(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again; //如果read被信号中断了,应该让它继续去read等待读数据 (read阻塞时)elsereturn -1;}return n;
}ssize_t Write(int fd, const void *ptr, size_t nbytes)
{ssize_t n;again:if ((n = write(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n;
}int Close(int fd)
{int n;if ((n = close(fd)) == -1)perr_exit("close error");return n;
}/*参三: 应该读取的字节数*/ //一直读到n字节数才会返回,否则阻塞等待
//socket 4096 readn(cfd, buf, 4096) nleft = 4096-1500
ssize_t Readn(int fd, void *vptr, size_t n)
{size_t nleft; //usigned int 剩余未读取的字节数ssize_t nread; //int 实际读到的字节数char *ptr;ptr = vptr;nleft = n; //n 未读取字节数while (nleft > 0) {if ((nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR){nread = 0;}else{return -1;}} else if (nread == 0){break;}nleft -= nread; //nleft = nleft - nread ptr += nread;}return n - nleft;
}ssize_t Writen(int fd, const void *vptr, size_t n)
{size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n;
}static ssize_t my_read(int fd, char *ptr) //静态函数保证了读完第一个100字节才去读下一个100字节,而不是每次调用都读100字节
{static int read_cnt; //改变量存在静态数据区,下次调用my_read函数的时候,read_cnt会保留上次的值static char *read_ptr;static char read_buf[100];//因为这里的变量都是static的,所以并非每次调用my_read都会读100字节,而是读完100字节再去读下一个100字节if (read_cnt <= 0) {
again:if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) //"hello\n"{if (errno == EINTR)goto again;return -1;} else if (read_cnt == 0)return 0;read_ptr = read_buf;}read_cnt--; //在上次调用结束的值基础上--,保证了读完100字节再去读下一个100字节*ptr = *read_ptr++;return 1;
}/*readline --- fgets*/
//传出参数 vptr
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ((rc = my_read(fd, &c)) == 1) //ptr[] = hello\n{*ptr++ = c;if (c == '\n') //先读100个字节,依次遍历,遇到 '\n' 说明一行读完了break;} else if (rc == 0) {*ptr = 0;return n-1;} elsereturn -1;}*ptr = 0;return n;
}


相关文章:
TCP并发服务器(多进程与多线程)
欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 TCP并发服务器(多进程与多线程)1. 多进程并发服务器(1)…...
第1章 Memcached 教程
Memcached是一个自由开源的,高性能,分布式内存对象缓存系统。 Memcached是以LiveJournal旗下Danga Interactive公司的Brad Fitzpatric为首开发的一款软件。现在已成为mixi、hatena、Facebook、Vox、LiveJournal等众多服务中提高Web应用扩展性的重要因素…...
【2022.12.9】Lammps+Python 在计算g6(r)时遇到的问题
目录写在前面绘制g6( r )执行步骤【updated】如何检查图像的正确性:不是编程问题,而是数学问题的一个小bug废稿2则:写在前面 全部log: 【2022.11.16】LammpsPythonMATLAB在绘制维诺图时遇到的问题 绘制g6( r )执行步骤【updated…...
MySQL使用C语言连接
文章目录MySQL使用C语言连接引入库下载库文件在项目中使用库使用库连接数据库下发SQL请求获取查询结果MySQL使用C语言连接 引入库 要使用C语言连接MySQL,需要使用MySQL官网提供的库。 下载库文件 下载库文件 首先,进入MySQL官网,选择DEVEL…...
JavaScript随手笔记---比较两个数组差异
💌 所属专栏:【JavaScript随手笔记】 😀 作 者:我是夜阑的狗🐶 🚀 个人简介:一个正在努力学技术的CV工程师,专注基础和实战分享 ,欢迎咨询! &#…...
【C++修炼之路】21.红黑树封装map和set
每一个不曾起舞的日子都是对生命的辜负 红黑树封装map和set前言一.改良红黑树的数据域结构1.1 改良后的结点1.2 改良后的类二. 封装的set和map2.1 set.h2.2 map.h三. 迭代器3.1 迭代器封装3.2 const迭代器四.完整代码实现4.1 RBTree.h4.2 set.h4.3 map.h4.4 Test.cpp前言 上一节…...
下载ojdbc14.jar的10.2.0.1.0版本的包
一、首先要有ojdbc14.jar包 没有的可以去下载一个,我的是从这里下载的ojdbc14.jar下载_ojdbc14.jar最新版下载[驱动包软件]-下载之家, 就是无奈关注了一个公众号,有的就不用下了。 二、找到maven的本地仓库的地址 我的地址在这里D:\apach…...
关于欧拉角你需要知道几个点
基础理解,参照:https://www.cnblogs.com/Estranged-Tech/p/16903025.html 欧拉角、万向节死锁(锁死)理解 一、欧拉角理解 举例讲解 欧拉角用三次独立的绕确定的轴旋转角度来表示姿态。如下图所示 经过三次旋转,旋…...
git ssh配置
ssh配置 执行以下命令进行配置 git config --global user.name “这里换上你的用户名” git config --global user.email “这里换上你的邮箱” 执行以下命令生成秘钥: ssh-keygen -t rsa -C “这里换上你的邮箱” 执行命令后需要进行3次或4次确认。直接全部回车就…...
Linux进程概念(三)
环境变量与进程地址空间环境变量什么是环境变量常见环境变量环境变量相关命令环境变量的全局属性PWDmain函数的三个参数进程地址空间什么是进程地址空间进程地址空间,页表,内存的关系为什么存在进程地址空间环境变量 什么是环境变量 我们所有写的程序都…...
新手福利——x64逆向基础
一、x64程序的内存和通用寄存器 随着游戏行业的发展,x32位的程序已经很难满足一些新兴游戏的需求了,因为32位内存的最大值为0xFFFFFFFF,这个值看似足够,但是当游戏对资源需求非常大,那么真正可以分配的内存就显得捉襟…...
虚幻c++中的细节之枚举类型(enum)
文章目录前言一、原生c的枚举类型关键字classint8 - 枚举的基础类型(underlying type)二、枚举类型的灵活运用位运算枚举循环遍历三、虚幻风格的枚举类型UENUMUMETATEnumAsByte总结前言 虚幻引擎中的代码部分实现了一套反射机制,为c代码带了…...
判断某个字符串在另一个字符串中的个数
/** * 用于判断字符串中字符的个数 * * param str1 原字符串 * param str2 需要判断的字符 * return 返回有几个 */ private int getCount(String str1, String str2) { //获取两个字符串的长度 int oneLength str1.length(); int toLength str2.length(); //定义两个整数&am…...
测试人员如何运用好OKR
在软件测试工作中是不是还不知道OKR是什么?又或者每次都很害怕写OKR?或者总觉得很迷茫,不知道目标是什么? OKR 与 KPI 的区别 去年公司从KPI换OKR之后,我也有一段抓瞎的过程,然后自己找了两本书看,一本是《OKR工作法》…...
CentOS7 Hive2.3.9 安装部署(mysql 8.0)
一、CentOS7安装MySQL数据库 查询载mariadb rpm -qa | grep mariadb卸载mariadb rpm -e --nodeps [查询出来的内容]安装wget为下载mysql准备 yum -y install wget在tools目录下执行以下命令,下载MySQL的repo源: wget -P /tools/ https://dev.mysql.…...
【PTA Advanced】1142 Maximal Clique(C++)
目录 题目 Input Specification: Output Specification: Sample Input: Sample Output: 思路 代码 题目 A clique is a subset of vertices of an undirected graph such that every two distinct vertices in the clique are adjacent. A maximal clique is a clique …...
1. MySQL在金融互联网行业的企业级安装部署
这里写目录标题 1. 版本介绍示例2.安装MySQL规范(建议二进制)2.1 安装方式2.2 安装用户2.3 目录规范3.二进制安装3.1 操作系统配置3.2 MySQL 5.7.33 安装部署2.3 MySQL8.0.27安装2.4 源码安装(了解 )3.多实例部署及注意事项3.1 多实例概念3.2 多实例安装3.3 多实例第二种方式…...
【C++修炼之路】19.AVL树
每一个不曾起舞的日子都是对生命的辜负 AVL树前言:一.AVL树的概念二.AVL树的结构2.1 AVL树节点的定义2.2 AVL树的结构2.3 AVL树的插入2.4 AVL树的验证2.5 AVL树的删除(了解)三.AVL树的旋转(重要)3.1 左单旋3.2 右单旋3.3 左右双旋3.4 右左双旋…...
项目管理工具dhtmlxGantt甘特图入门教程(十):服务器端数据集成(下)
这篇文章给大家讲解如何利用dhtmlxGantt在服务器端集成数据。 dhtmlxGantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表,可满足应用程序的所有需求,是完善的甘特图图表库 DhtmlxGantt正版试用下载(qun 764149912)http…...
LeetCode 793. 阶乘函数后 K 个零
f(x) 是 x! 末尾是 0 的数量。回想一下 x! 1 * 2 * 3 * ... * x,且 0! 1 。 例如, f(3) 0 ,因为 3! 6 的末尾没有 0 ;而 f(11) 2 ,因为 11! 39916800 末端有 2 个 0 。 给定 k,找出返回能满足 f(x) …...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
CVPR 2025 MIMO: 支持视觉指代和像素grounding 的医学视觉语言模型
CVPR 2025 | MIMO:支持视觉指代和像素对齐的医学视觉语言模型 论文信息 标题:MIMO: A medical vision language model with visual referring multimodal input and pixel grounding multimodal output作者:Yanyuan Chen, Dexuan Xu, Yu Hu…...
相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
