从0实现基于Linux socket聊天室-多线程服务器模型(一)

news2024/9/27 15:30:35

    前言Socket在实际系统程序开发当中,应用非常广泛,也非常重要。实际应用中服务器经常需要支持多个客户端连接,实现高并发服务器模型显得尤为重要。高并发服务器从简单的循环服务器模型处理少量网络并发请求,演进到解决C10K,C10M问题的高并发服务器模型。本文通过一个简单的多线程模型,带领大家学习如何自己实现一个简单的并发服务器。

C/S架构

服务器-客户机,即Client-Server(C/S)结构。C/S结构通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。

在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;客户机部分为用户所专有,负责执行前台功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。

如上图所示:这是基于套接字实现客户端和服务器相连的函数调用关系,socket API资料比较多,本文不再过多叙述。

pthread线程库:(POSIX)

pthread线程库是Linux下比较常用的一个线程库,关于他的用法和特性大家可以自行搜索相关文章,下面只简单介绍他的用法和编译。

线程标识

线程有ID, 但不是系统唯一, 而是进程环境中唯一有效. 线程的句柄是pthread_t类型, 该类型不能作为整数处理, 而是一个结构. 下面介绍两个函数:

头文件: <pthread.h>
原型: int pthread_equal(pthread_t tid1, pthread_t tid2);
返回值: 相等返回非0, 不相等返回0.
说明: 比较两个线程ID是否相等.

头文件: <pthread.h>
原型: pthread_t pthread_self();
返回值: 返回调用线程的线程ID.

线程创建

在执行中创建一个线程, 可以为该线程分配它需要做的工作(线程执行函数), 该线程共享进程的资源. 创建线程的函数pthread_create()

头文件: <pthread.h>
原型: int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(start_rtn)(void), void *restrict arg);
返回值: 成功则返回0, 否则返回错误编号.
参数:
tidp: 指向新创建线程ID的变量, 作为函数的输出.
attr: 用于定制各种不同的线程属性, NULL为默认属性(见下).
start_rtn: 函数指针, 为线程开始执行的函数名.该函数可以返回一个void *类型的返回值,
而这个返回值也可以是其他类型,并由 pthread_join()获取
arg: 函数的唯一无类型(void)指针参数, 如要传多个参数, 可以用结构封装.

编译

因为pthread的库不是linux系统的库,所以在进行编译的时候要加上     -lpthread
# gcc filename -lpthread  //默认情况下gcc使用c库,要使用额外的库要这样选择使用的库

常见的网络服务器模型

本文结合自己的理解,主要以TCP为例,总结了几种常见的网络服务器模型的实现方式,并最终实现一个简单的命令行聊天室。

单进程循环

单线进程循环原理就是主进程没和客户端通信,客户端都要先连接服务器,服务器接受一个客户端连接后从客户端读取数据,然后处理并将处理的结果返还给客户端,然后再接受下一个客户端的连接请求。

优点单线程循环模型优点是简单、易于实现,没有同步、加锁这些麻烦事,也没有这些开销。

缺点

  1. 阻塞模型,网络请求串行处理;

  2. 没有利用多核cpu的优势,网络请求串行处理;

  3. 无法支持同时多个客户端连接;

  4. 程序串行操作,服务器无法实现同时收发数据。

单线程IO复用

linux高并发服务器中常用epoll作为IO复用机制。线程将需要处理的socket读写事件都注册到epoll中,当有网络IO发生时,epoll_wait返回,线程检查并处理到来socket上的请求。

优点

  1. 实现简单, 减少锁开销,减少线程切换开销。

缺点

  1. 只能使用单核cpu,handle时间过长会导致整个服务挂死;

  2. 当有客户端数量超过一定数量后,性能会显著下降;

  3. 只适用高IO、低计算,handle处理时间短的场景。

 

多线程/多进程

多线程、多进程模型主要特点是每个网络请求由一个进程/线程处理,线程内部使用阻塞式系统调用,在线程的职能划分上,可以由一个单独的线程处理accept连接,其余线程处理具体的网络请求(收包,处理,发包);还可以多个进程单独listen、accept网络连接。

优点:

1、实现相对简单;2、利用到CPU多核资源。

缺点:

1、线程内部还是阻塞的,举个极端的例子,如果一个线程在handle的业务逻辑中sleep了,这个线程也就挂住了。

多线程/多进程IO复用

多线程、多进程IO服用模型,每个子进程都监听服务,并且都使用epoll机制来处理进程的网络请求,子进程 accept() 后将创建已连接描述符,然后通过已连接描述符来与客户端通信。该机制适用于高并发的场景。

优点:

  1. 支撑较高并发。

缺点:

  1. 异步编程不直观、容易出错

 

多线程划分IO角色

多线程划分IO角色主要功能有:一个accept thread处理新连接建立;一个IO thread pool处理网络IO;一个handle thread pool处理业务逻辑。使用场景如:电销应用,thrift TThreadedSelectorServer。

优点:

  1. 按不同功能划分线程,各线程处理固定功能,效率更高

  2. 可以根据业务特点配置线程数量来性能调优

缺点:

  1. 线程间通信需要引入锁开销

  2. 逻辑较复杂,实现难度大

小结

上面介绍了常见的网络服务器模型,还有AIO、协程,甚至还有其他的变型,在这里不再讨论。重要的是理解每种场景中所面临的问题和每种模型的特点,设计出符合应用场景的方案才是好方案。

多线程并发服务器模型

下面我们主要讨论多线程并发服务器模型。

代码结构

并发服务器代码结构如下:

thread_func()
{
  while(1) {
    recv(...);
    process(...);
    send(...);
  }
  close(...);
}
main(
 socket(...); 
 bind(...);
 listen(...);
 while(1) { 
  accept(...);
  pthread_create();
 }
}

由上可以看出,服务器分为两部分:主线程、子线程。

主线程

main函数即主线程,它的主要任务如下:

  1. socket()创建监听套字;

  2. bind()绑定端口号和地址;

  3. listen()开启监听;

  4. accept()等待客户端的连接,

  5. 当有客户端连接时,accept()会创建一个新的套接字new_fd;

  6. 主线程会创建子线程,并将new_fd传递给子线程。

子线程

  1. 子线程函数为thread_func(),他通过new_fd处理和客户端所有的通信任务。

客户端连接服务器详细步骤

下面我们分步骤来看客户端连接服务器的分步说明。

1. 客户端连接服务器

  1. 服务器建立起监听套接字listen_fd,并初始化;

  2. 客户端创建套接字fd1;

  3. 客户端client1通过套接字fd1连接服务器的listen_fd;

 

 

2. 主线程创建子线程thread1

  1. server收到client1的连接请求后,accpet函数会返回一个新的套接字newfd1;

  2. 后面server与client1的通信就依赖newfd1,监听套接字listen_fd会继续监听其他客户端的连接;

  3. 主线程通过pthead_create()创建一个子线程thread1,并把newfd1传递给thread1;

  4. server与client1的通信就分别依赖newfd1、fd1。

  5. client1为了能够实时收到server发送的信息,同时还要能够从键盘上读取数据,这两个操作都是阻塞的,没有数据的时候进程会休眠,所以必须创建子线程read_thread;

  6. client1的主线负责从键盘上读取数据并发送给,子线程read_thread负责从server接受信息。

 

3. client2连接服务器

  1. 客户端client2创建套接字fd2;

  2. 通过connect函数连接server的listen_fd;

4. 主线程创建子线程thread2

  1. server收到client2的连接请求后,accpet函数会返回一个新的套接字newfd2;

  2. 后面server与client2的通信就依赖newfd2,监听套接字listen_fd会继续监听其他客户端的连接;

  3. 主线程通过pthead_create()创建一个子线程thread2,并把newfd2传递给thread2;

  4. server与client1的通信就分别依赖newfd2、fd2。

  5. 同样client2为了能够实时收到server发送的信息,同时还要能够从键盘上读取数据必须创建子线程read_thread;

  6. client1的主线负责从键盘上读取数据并发送给,子线程read_thread负责从server接受信息。

 

由上图可见,每一个客户端连接server后,server都要创建一个专门的thread负责和该客户端的通信;每一个客户端和server都有一对固定的fd组合用于连接。

实例

好了,理论讲完了,根据一口君的惯例,也继承祖师爷的教诲:talk is cheap,show you my code.不上代码,只写理论的文章都是在耍流氓。

本例的主要功能描述如下:

  1. 实现多个客户端可以同时连接服务器;

  2. 客户端可以实现独立的收发数据;

  3. 客户端发送数据给服务器后,服务器会将数据原封不动返回给客户端。

服务器端

/*********************************************
           服务器程序  TCPServer.c  
           公众号:一口Linux
*********************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>

#define RECVBUFSIZE 2048
void *rec_func(void *arg)
{
 int sockfd,new_fd,nbytes;
 char buffer[RECVBUFSIZE];
 int i;
 new_fd = *((int *) arg);
 free(arg); 
 
 while(1)
 {
  if((nbytes=recv(new_fd,buffer, RECVBUFSIZE,0))==-1)
  {
   fprintf(stderr,"Read Error:%s\n",strerror(errno));
   exit(1);
  }
  if(nbytes == -1)
  {//客户端出错了 返回值-1
   close(new_fd);
   break;   
  }
  if(nbytes == 0)
  {//客户端主动断开连接,返回值是0
   close(new_fd);
   break;
  }
  buffer[nbytes]='\0'; 
  printf("I have received:%s\n",buffer); 
  
  
  if(send(new_fd,buffer,strlen(buffer),0)==-1)
  {
   fprintf(stderr,"Write Error:%s\n",strerror(errno));
   exit(1);
  }
   
 }

}

int main(int argc, char *argv[])
{
 char buffer[RECVBUFSIZE];
 int sockfd,new_fd,nbytes;
 struct sockaddr_in server_addr;
 struct sockaddr_in client_addr;
 int sin_size,portnumber;
 char hello[]="Hello! Socket communication world!\n";
 pthread_t tid;
 int *pconnsocke = NULL;
 int ret,i;
 
 if(argc!=2)
 {
  fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
  exit(1);
 }
 /*端口号不对,退出*/
 if((portnumber=atoi(argv[1]))<0)
 {
  fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
  exit(1);
 }

 /*服务器端开始建立socket描述符  sockfd用于监听*/
 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)  
 {
  fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
  exit(1);
 }
 
 /*服务器端填充 sockaddr结构*/ 
 bzero(&server_addr,sizeof(struct sockaddr_in));
 server_addr.sin_family     =AF_INET;
 /*自动填充主机IP*/
 server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//自动获取网卡地址
 server_addr.sin_port       =htons(portnumber);
 
 /*捆绑sockfd描述符*/ 
 if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
 {
  fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
  exit(1);
 }
 
 /*监听sockfd描述符*/
 if(listen(sockfd, 10)==-1)
 {
  fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
  exit(1);
 }

 while(1)
 {
  /*服务器阻塞,直到客户程序建立连接*/
  sin_size=sizeof(struct sockaddr_in);
  if((new_fd = accept(sockfd,(struct sockaddr *)&client_addr,&sin_size))==-1)
  {
   fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
   exit(1);
  }
  
  pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 
 }
 //close(sockfd);
 exit(0);
}

客户端

/*********************************************
           服务器程序  TCPServer.c  
           公众号:一口Linux
*********************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#define RECVBUFSIZE 1024

void *func(void *arg)
{
 int sockfd,new_fd,nbytes;
 char buffer[RECVBUFSIZE];
 
 new_fd = *((int *) arg);
 free(arg);
 
 while(1)
 {
  if((nbytes=recv(new_fd,buffer, RECVBUFSIZE,0))==-1)
  {
   fprintf(stderr,"Read Error:%s\n",strerror(errno));
   exit(1);
  }
  buffer[nbytes]='\0';
  printf("I have received:%s\n",buffer); 
 }

}

int main(int argc, char *argv[])
{
 int sockfd;
 char buffer[RECVBUFSIZE];
 struct sockaddr_in server_addr;
 struct hostent *host;
 int portnumber,nbytes; 
 pthread_t tid;
 int *pconnsocke = NULL;
 int ret;
 
 //检测参数个数
 if(argc!=3)
 {
  fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
  exit(1);
 }
 //argv2 存放的是端口号 ,读取该端口,转换成整型变量
 if((portnumber=atoi(argv[2]))<0)
 {
  fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
  exit(1);
 }
 //创建一个 套接子
 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
 {
  fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
  exit(1);
 }

 //填充结构体,ip和port必须是服务器的
 bzero(&server_addr,sizeof(server_addr));
 server_addr.sin_family=AF_INET;
 server_addr.sin_port=htons(portnumber);
 server_addr.sin_addr.s_addr = inet_addr(argv[1]);//argv【1】 是server ip地址

 /*¿Í»§³ÌÐò·¢ÆðÁ¬œÓÇëÇó*/ 
 if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
 {
  fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
  exit(1);
 }
 
 //创建线程
 pconnsocke = (int *) malloc(sizeof(int));
 *pconnsocke = sockfd;
 
 ret = pthread_create(&tid, NULL, func, (void *) pconnsocke);
 if (ret < 0) 
 {
  perror("pthread_create err");
  return -1;
 } 
 while(1)
 {
 #if 1
  printf("input msg:");
  scanf("%s",buffer);
  if(send(sockfd,buffer,strlen(buffer),0)==-1)
  {
   fprintf(stderr,"Write Error:%s\n",strerror(errno));
   exit(1);
  }
  #endif
 }
 close(sockfd);
 exit(0);
}

编译编译线程,需要用到pthread库,编译命令如下:

  1. gcc s.c -o s -lpthread

  2. gcc cli.c -o c -lpthread 先本机测试

  3. 开启一个终端 ./s 8888

  4. 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"qqqqqqq"

  5. 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"yikoulinux"

有读者可能会注意到,server创建子线程的时候用的是以下代码:

 pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 

为什么必须要malloc一块内存专门存放这个新的套接字呢? 

这个是一个很隐蔽,很多新手都容易犯的错误。下一章,我会专门给大家讲解。

本系列文章预计会更新4-5篇。最终目的是写出一个带登录注册公聊私聊等功能的聊天室。喜欢的话请收藏关注。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/712355.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AntDB数据库将携创新性解决方案亮相2023可信数据库发展大会

由中国通信标准化协会指导&#xff0c;中国通信标准化协会大数据技术标准推进委员会&#xff08;CCSA TC601&#xff09;主办的“2023可信数据库发展大会”将于2023年7月4日——5日在北京国际会议中心召开。作为深耕通信行业15年的国产数据库产品&#xff0c;AntDB受邀参会&…

记录一下kibana启动链接报错问题(kibana server is not ready yet)

记录一下kibana启动链接报错问题(kibana server is not ready yet) 今天启动kibana出现该问题 先去看了看是否是elasticsearch连接出错 启动了容器 docker start elasticsearch docker start kibana进入了kibana容器 docker exec -it kibana bash进行了下面的操作&#xf…

No suitable driver found for

在学习Mbatis时候遇到的奇怪的问题&#xff0c;报错提示如图所示&#xff0c;提示找不到数据库驱动 检查db.properties文件,一开始认为没问题 drivercom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/mybatis?useSSLfalse&useUnicodetrue&characterEncodingUTF…

华为OD机试真题2023Q1 100分 + 2023 B卷(JavaPythonJavaScript)

目录 2023 5月 B卷 “新加题”&#xff08;100分值&#xff09;2023Q1 100分下面分享一道“2023Q1 200分 机器人活动区域”的解题思路一、题目描述二、输入描述三、输出描述四、解题思路五、Python算法源码六、效果展示1、输入2、输出 大家好&#xff0c;我是哪吒。 五月份之前…

react基础-生命周期render props模式高阶组件原理揭秘

组件生命周期&#xff08;★★★&#xff09; 目标 说出组件生命周期对应的钩子函数钩子函数调用的时机 概述 意义&#xff1a;组件的生命周期有助于理解组件的运行方式&#xff0c;完成更复杂的组件功能、分析组件错误原因等 组件的生命周期&#xff1a; 组件从被创建到挂…

通用机器人里程碑?谷歌展示全球首个多任务AI智能体

目录 两大硬核科技支撑通用机器人研发&#xff08;1&#xff09;自生成训练数据&#xff08;2&#xff09;基于多模态模型 科技巨头同台比拼 中国产业链凸显性价比优势发展初期硬件先行 运动模块价值量最高 已学会套圈、搭积木、抓水果…… 人工智能和机器人&#xff0c;总是不…

MES生产管理系统与ERP系统的集成以及优势

导言&#xff1a; 在当今数字化转型的浪潮中&#xff0c;企业越来越意识到整合各个部门的数据和流程的重要性。MES生产管理系统和ERP系统是两个关键的管理工具&#xff0c;它们在企业中发挥着不可或缺的作用。本文将探讨企业MES管理系统与ERP系统进行集成&#xff0c;以及这种…

它如何做到让我们持久且不感疲劳

写在前面 随着科技的进步和数字化生活的兴起&#xff0c;人们长时间使用显示器的需求增加&#xff0c;越来越多的人戴眼镜并且面临眼睛问题。显示器屏幕灯在当今社会也逐渐扮演着不可或缺的角色。 首先&#xff0c;显示器屏幕灯能够提供必要的亮度&#xff0c;确保我们在各种…

pyhton-docx表格合并单元格

合并单元格需要指定两个单元格&#xff0c; from docx_utils import set_table_singleBoard from docx import Documentdocument Document() table document.add_table(rows3, cols3) # 创建一个包含 3 行 3 列的表格 table.cell(0, 0).merge(table.cell(0, 1)) # 合并第一…

用正则表达式进行input框的限制输入

vue项目可以用input事件输入 1.限制input输入框只能输入大小写字母、数字、下划线的正则表达式&#xff1a; 用户名< input type"text" placeholder"只包含数字字母下划线" onkeyup"this.valuethis.value.replace(/[^\w_]/g,);"> 2.限…

linux如何修改sudoers文件,将非root用户加入到 sudoers 文件中

需求 由于在非 root 用户下执行 sudo 命令会报错 cc 不在 sudoers 文件中。此事将被报告。所以需要将 cc 这个用户加入到 sudoers 文件中进行授权 解决 要修改 sudoers 文件&#xff0c;您需要以 root 用户身份进行操作。以下是一种常见的方法&#xff1a; 1、使用 root 用…

Linux文件管理(创建 删除 复制 剪切 打包 压缩 解压缩)全总结

目录 一、Linux下文件命名规则 1、可以使用哪些字符&#xff1f; 2、文件名的长度 3、文件名的大小写 4、Linux文件扩展名 二、Linux下的文件管理 1、文件夹创建 ① mkdir创建文件夹 ② mkdir -p递归创建文件夹&#xff08;目录&#xff09; ③ 使用mkdir同时创建多个…

nacos批量信息获取-GitNacosConfig

声明&#xff1a;文中涉及到的技术和工具&#xff0c;仅供学习使用&#xff0c;禁止从事任何非法活动&#xff0c;如因此造成的直接或间接损失&#xff0c;均由使用者自行承担责任。 点点关注不迷路&#xff0c;每周不定时持续分享各种干货。 原文链接&#xff1a;众亦信安&a…

Leetcode-每日一题【234.回文链表】

题目 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为回文链表。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,2,1]输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;head…

【Golang | runtime】runtime.Caller和runtime.Callers的使用和区别

环境&#xff1a; go version go1.18.2 1、runtime.Caller 函数func runtime.Caller(skip int) (pc uintptr, file string, line int, ok bool) 作用 获取函数Caller的调用信息 参数 skip: 0时&#xff0c;返回调用Caller的函数A的pc(program counter)、所在文件名以及Cal…

SI12T触摸按键芯片兼容TMS12资料

Si12T 是一款具有自动灵敏度校准功能的 12 通道电容传感器&#xff0c;其工作电压 范围为 1.8 ~ 5.0 V 。 Si12T 设置 IDLE 模式来节省功耗&#xff0c;此时&#xff0c;功耗电流为 3.5 A3.3V 。 Si12T 有三种特殊功能&#xff1a;一种是通道 1 上的嵌入式电源键…

体验Kubernetes(k8s),使用minikube搭建单机k8s

文章目录 一、windows&#xff1a;使用Minikube搭建单节点K8s1、安装VirtualBox2、安装kubectl3、安装minikube4、使用minikube搭建k8s 二、centos&#xff1a;使用minikube搭建单节点k8s1、安装docker2、下载kubectl&minikube与安装3、搭建单机k8s4、体验pod 一、windows&…

【计算机网络】MAC地址、IP地址、ARP协议

1.总概念 2.MAC地址 2.1 MAC地址作用 2.2 网络适配器&#xff08;网卡&#xff09; 2.3 概念 2.4 MAC地址格式 2.5 MAC地址发送顺序 单播MAC地址 广播MAC地址 多播MAC地址 随机MAC地址&#xff08;信息安全和隐私保护&#xff09; 2.6小结 3. IP地址 3.1 IP地址作用 3.2 网络…

【C#】实体类和DataTable之间相互转换,实体反射动态遍历列

在实际项目中&#xff0c;经常会用到数据之间的相互转换&#xff0c;序列化和反序列化就是常见场景。这里我们只简单聊聊实体类和DataTable之间的相互转换&#xff0c;可以用于不同业务场景使用。 目录 1、DataTable转Model2、Model转DataTable3、反射概念3.1、Type 类型3.2、A…

力扣 669. 修剪二叉搜索树

题目来源&#xff1a;https://leetcode.cn/problems/trim-a-binary-search-tree/description/ C题解1&#xff1a;递归法。当前节点为空时返回空&#xff0c;不为空时对其值进行分类讨论。以low为例&#xff0c;当前节点值等于low时&#xff0c;意味着其左子树都要丢弃&#xf…