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服务器只能处理单连接,在代码实现时,多进程并发服务器与非并发服务器在创建监听套接字、绑定、监听这几个步骤是一样的,但是在接收连接请求的时候,多进程并发服务器是这样实现的:父进程负责接受连接请求,一旦连接成功,将会创建一个子进程与客户端通信。示意图如下:
(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)读时共享写时复制详解
首先看图
如果父子进程都只是读数据,那么他们都通过虚拟地址去访问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. 多线程并发服务器
多线程并发服务器示意图如下:
在多进程模型中,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) …...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...

招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...