【C++高并发服务器WebServer】-13:多线程服务器开发

在这里插入图片描述

本文目录

  • 一、多线程服务器开发
  • 二、TCP状态转换
  • 三、端口复用

一、多线程服务器开发

服务端代码如下。

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>struct sockInfo {int fd; // 通信的文件描述符struct sockaddr_in addr; //客户端的信息pthread_t tid;  // 线程号
};// 先定义好能够同时支持的客户端数量
struct sockInfo sockinfos[128];void * working(void * arg) {// 子线程和客户端通信   cfd 客户端的信息 线程号// 获取客户端的信息// 参数是void * 类型的,所以需要进行强转struct sockInfo * pinfo = (struct sockInfo *)arg;char cliIp[16];inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));unsigned short cliPort = ntohs(pinfo->addr.sin_port);printf("client ip is : %s, prot is %d\n", cliIp, cliPort);// 接收客户端发来的数据char recvBuf[1024];while(1) {int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));if(len == -1) {perror("read");exit(-1);}else if(len > 0) {printf("recv client : %s\n", recvBuf);} else if(len == 0) {printf("client closed....\n");break;}write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);}close(pinfo->fd);return NULL;
}int main() {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);if(lfd == -1){perror("socket");exit(-1);}struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);saddr.sin_addr.s_addr = INADDR_ANY;// 绑定int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));if(ret == -1) {perror("bind");exit(-1);}// 监听ret = listen(lfd, 128);if(ret == -1) {perror("listen");exit(-1);}// 初始化数据,用整个数组的所占字节除以单个元素的大小,得到数组中的总数int max = sizeof(sockinfos) / sizeof(sockinfos[0]);for(int i = 0; i < max; i++) {//将sockinfos[i]这个地址中的所有内存大小都置为0bzero(&sockinfos[i], sizeof(sockinfos[i]));sockinfos[i].fd = -1; //-1表示是可用的文件描述符sockinfos[i].tid = -1;}// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信while(1) {struct sockaddr_in cliaddr;int len = sizeof(cliaddr);// 接受连接int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);// 局部变量当循环结束,就会释放,所以可以通过堆malloc来保存数据,但是子线程需要对应的去释放这个堆// 定义好结构体指针struct sockInfo * pinfo;for(int i = 0; i < max; i++) {// 从这个数组中找到一个可以用的sockInfo元素if(sockinfos[i].fd == -1) {pinfo = &sockinfos[i];break;}if(i == max - 1) {//也就是i=127的时候,sleep1秒,不然会继续下去创建子线程了sleep(1);// i--;i =  -1;}}pinfo->fd = cfd;//不可以用pinfo.addr = cliaddr进行赋值,可以通过里面对应的元素进行赋值memcpy(&pinfo->addr, &cliaddr, len);// 创建子线程,第四个参数是子线程需要的参数,且类型是 (void *) 类型,但是需要cfd、客户端信息、线程号// 所以可把需要的参数封装成一个结构体,然后把结构体传进去,这里就是第四个参数pinfo// pthread_t tid得等到pthread_create之后才会有对应的值,所以可以直接用&pinfo->tid来代替,这样可以直接给结构体中的tid进行赋值pthread_create(&pinfo->tid, NULL, working, pinfo);// void* 类型的指针是一种特殊的指针类型,可以指向任何类型的对象。// 也就是可以存储任何类型的指针值,但是不能直接对他进行解引用操作。// 在进行使用的时候,必须进行强转,将其转换为正确的类型。// 不可以用pthread_join();因为是阻塞的,这样就不能等待下一个客户端进来循环了。// 设置线程分离,让当前线程结束之后自己去释放资源,不需要父线程回收。pthread_detach(pinfo->tid);}close(lfd);return 0;
}

客户端代码如下:

// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>int main() {// 1.创建套接字int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");exit(-1);}// 2.连接服务器端struct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr.s_addr);serveraddr.sin_port = htons(9999);int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));if(ret == -1) {perror("connect");exit(-1);}// 3. 通信char recvBuf[1024];int i = 0;while(1) {sprintf(recvBuf, "data : %d\n", i++);// 给服务器端发送数据//这里+1 是因为要算进去字符换行的结束符,不然会有问题。write(fd, recvBuf, strlen(recvBuf)+1);int len = read(fd, recvBuf, sizeof(recvBuf));if(len == -1) {perror("read");exit(-1);} else if(len > 0) {printf("recv server : %s\n", recvBuf);} else if(len == 0) {// 表示服务器端断开连接printf("server closed...");break;}sleep(1);}// 关闭连接close(fd);return 0;
}

运行下面代码可以看到效果如图:

在这里插入图片描述

二、TCP状态转换

在这里插入图片描述
主动断开连接的一方,最后进入一个TIME_WAIT状态,这个状态是定时经过两个报文段寿命(2MSL,Maximum Segment Lifetime)之后才会结束。

这里需要搞清楚一个点,假设客户端主动断开连接,当客户端发送FIN报文之后,服务端回一个ACK,然后客户端会进入FIN_WAIT_2状态,这个时候服务端可以继续向客户端发送数据,直到发送完该发送的数据之后,才会向客户端发送FIN报文,然后客户端会进入TIME_WAIT状态,从而经过2MSL断开。

这也就是为什么是四次挥手,而不是像三次握手一样,把ACK和FIN结合起来从而整体变成三次挥手。三次握手的时候是因为双方都希望能够建立连接,所以ACK和SYN可以结合。但是断开连接可能会有某一方“不愿意”,还有需要发送的一个数据。可以理解成单方面的概念。

2MSL是为了保证安全和可靠性,因为有可能客户端回的最后一个ACK可能服务端会没收到,如果客户端立马断开,那么服务端会没断开,那么结束的状态是不完整的。没有接收到ACK,那么服务端会再次发送一个FIN,然后客户端再发一次ACK。

2MSL就是确保另外一方能够接收到ACK。Linux中msl一般是30s。

当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号被动关闭方也不会重传),而是因为被动关闭方重传了它的FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。

有些程序就是有单方向发送的需求,所以可以用半关闭状态。

当 TCP 链接中A 向B发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN WAIT 2状态),并没有立即发送 FIN 给 A,A方处于半连接状态(半开关),此时A可以接收B发送的数据,但是 A已经不能再向B发送数据。

可以通过API来实现半连接半关闭状态。

#include <sys/socket.h>
int shutdown(int sockfd, int how);

sockfd: 需要关闭的socket的描述符。

how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后是SHUT_WR。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。(在使用 fork 时,子进程会继承父进程的文件描述符,因此需要在父子进程中分别关闭不需要的文件描述符,以避免资源泄漏。)

如果有多个进程共享一个套接字,close 每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了 close,套接字将被释放。

在多进程中如果一个进程调用了 shutdown(sfd,SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程 close(sfd)将不会影响到其它进程。

三、端口复用

在Linux中,有一些查看网络相关信息的命令。

netstat:
netstat -a :显示所有的socket
netstat -p :显示正在使用socket的程序的名称
netstat -n :直接使用IP地址,而不通过域名服务器
netstat -t :显示TCP的socket
netstat -u :显示UDP的socket

首先运行下面的server 代码。

#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main(int argc, char *argv[]) {// 创建socketint lfd = socket(PF_INET, SOCK_STREAM, 0);if(lfd == -1) {perror("socket");return -1;}struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY;saddr.sin_port = htons(9999);//int optval = 1;//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));//int optval = 1;//setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));// 绑定int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));if(ret == -1) {perror("bind");return -1;}// 监听ret = listen(lfd, 8);if(ret == -1) {perror("listen");return -1;}// 接收客户端连接struct sockaddr_in cliaddr;socklen_t len = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);if(cfd == -1) {perror("accpet");return -1;}// 获取客户端信息char cliIp[16];inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));unsigned short cliPort = ntohs(cliaddr.sin_port);// 输出客户端的信息printf("client's ip is %s, and port is %d\n", cliIp, cliPort );// 接收客户端发来的数据char recvBuf[1024] = {0};while(1) {int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);if(len == -1) {perror("recv");return -1;} else if(len == 0) {printf("客户端已经断开连接...\n");break;} else if(len > 0) {printf("read buf = %s\n", recvBuf);}// 小写转大写for(int i = 0; i < len; ++i) {recvBuf[i] = toupper(recvBuf[i]);}printf("after buf = %s\n", recvBuf);// 大写字符串发给客户端ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);if(ret == -1) {perror("send");return -1;}}close(cfd);close(lfd);return 0;
}

下面是client代码。

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {// 创建socketint fd = socket(PF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);seraddr.sin_family = AF_INET;seraddr.sin_port = htons(9999);// 连接服务器int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret == -1){perror("connect");return -1;}while(1) {char sendBuf[1024] = {0};fgets(sendBuf, sizeof(sendBuf), stdin);write(fd, sendBuf, strlen(sendBuf) + 1);// 接收int len = read(fd, sendBuf, sizeof(sendBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n", sendBuf);} else {printf("服务器已经断开连接...\n");break;}}close(fd);return 0;
}

查看对应的端口占用情况,可以看到是server程序正在占用9999端口。

在这里插入图片描述

然后再运行客户端,再查看一次情况,可以看到下面的情况,有两个server。一个server是用来监听的,一个server是用来通信的(也就是状态是Established的)。

在这里插入图片描述

当我们主动断开服务器之后,再查看一次状态,可以看到服务器的状态还在,但是不会显示server,状态是FIN_WAIT2。并且client的状态变成了Close_WAIT。

在这里插入图片描述
在这里插入图片描述
然后再过一段时间,服务端的信息也会没有了。

在这里插入图片描述
那么继续刚刚的过程,运行server和client,然后退出server,再立即启动server,会发现显示端口已占用,此时查看netstat情况,会发现处于FIN_WAIT_2的一个状态。

在这里插入图片描述

如果继续退出client,这个时候server会从FIN_WAIT_2变成TIME_WAIT状态,然后等待2msl就会退出。

这个时候就需要进行端口复用的设置,把server端中的下面两行代码的注释取消,然后再进行尝试,就可以发现不会显示端口绑定了。

端口复用就是为了解决防止程序服务器突然重启时,之前绑定的端口还没有释放,或者程序突然退出但是没有释放端口。

int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

来看看下面这个函数的作用。

#include <sys/types.h>
#include <sys/socket.h>// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t
optlen);

setsockopt 函数用于设置套接字的选项,它允许程序员对套接字的行为进行细粒度的控制。通过指定文件描述符 sockfd,可以针对特定的套接字进行操作。level 参数指定了选项所在的协议级别,例如 SOL_SOCKET 表示在套接字层面上的选项,这通常用于设置通用的套接字行为,如端口复用等。optname 参数指定了要设置的具体选项名称,比如 SO_REUSEADDR 或 SO_REUSEPORT,这些选项分别用于控制地址和端口的复用行为,允许在某些情况下多个套接字绑定到同一个地址和端口,这对于提高服务器的并发处理能力和快速重启服务非常有用。

optval 参数是一个指向值的指针,它指定了选项的具体值,通常是一个整型值,例如 1 表示启用某个选项(如允许复用),而 0 表示禁用该选项。optlen 参数则指定了 optval 参数所指向的值的大小,这在某些情况下用于确保数据的正确传递和解析。通过这些参数的组合,setsockopt 函数能够灵活地调整套接字的行为,以满足应用程序在不同场景下的需求,比如在开发高性能网络服务器时,合理设置这些选项可以显著提升系统的性能和可靠性。

具体可以看看UNIX网络编程这本书对端口复用的解释。

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

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

相关文章

「vue3-element-admin」告别 vite-plugin-svg-icons!用 @unocss/preset-icons 加载本地 SVG 图标

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template &#x1f33a; 仓库主页&#xff1a; GitCode︱ Gitee ︱ Github &#x1f496; 欢迎点赞 &#x1f44d; 收藏 ⭐评论 …

C#中深度解析BinaryFormatter序列化生成的二进制文件

C#中深度解析BinaryFormatter序列化生成的二进制文件 BinaryFormatter序列化时,对象必须有 可序列化特性[Serializable] 一.新建窗体测试程序BinaryDeepAnalysisDemo,将默认的Form1重命名为FormBinaryDeepAnalysis 二.新建测试类Test Test.cs源程序如下: using System; us…

深度学习在医疗影像分析中的应用

引言 随着人工智能技术的快速发展&#xff0c;深度学习在各个领域都展现出了巨大的潜力。特别是在医疗影像分析中&#xff0c;深度学习的应用不仅提高了诊断的准确性&#xff0c;还大大缩短了医生的工作时间&#xff0c;提升了医疗服务的质量。本文将详细介绍深度学习在医疗影像…

计算机领域QPM、TPM分别是什么并发指标,还有其他类似指标吗?

在计算机领域&#xff0c;QPM和TPM是两种不同的并发指标&#xff0c;它们分别用于衡量系统处理请求的能力和吞吐量。 QPM&#xff08;每分钟请求数&#xff09; QPM&#xff08;Query Per Minute&#xff09;表示每分钟系统能够处理的请求数量。它通常用于衡量系统在单位时间…

【安当产品应用案例100集】036-视频监控机房权限管理新突破:安当windows操作系统登录双因素认证解决方案

一、机房管理痛点&#xff1a;权限失控下的数据泄露风险 在智慧城市与数字化转型浪潮下&#xff0c;视频监控系统已成为能源、金融、司法等行业的核心安防设施。然而&#xff0c;传统机房管理模式中&#xff0c;值班人员通过单一密码即可解锁监控画面的操作漏洞&#xff0c;正…

Unity抖音云启动测试:如何用cmd命令行启动exe

相关资料&#xff1a;弹幕云启动&#xff08;原“玩法云启动能力”&#xff09;_直播小玩法_抖音开放平台 1&#xff0c;操作方法 在做云启动的时候&#xff0c;接完发现需要命令行模拟云环境测试启动&#xff0c;所以研究了下。 首先进入cmd命令&#xff0c;CD进入对应包的文件…

< OS 有关 > 利用 google-drive-ocamlfuse 工具,在 Ubuntu 24 系统上 加载 Google DRIVE 网盘

Created by Dave On 8Feb.2025 起因&#xff1a; 想下载 StableDiffusion&#xff0c;清理系统文件时把 i/o 搞到 100%&#xff0c;已经删除到 apt 缓存&#xff0c;还差 89MB&#xff0c;只能另想办法。 在网上找能不能挂在 Google 网盘&#xff0c;百度网盘&#xff0c;或 …

【LITS游戏——暴力DFS+剪枝优化】

题目 代码 #include <bits/stdc.h> using namespace std; using pll pair<int, int>; #define x first #define y second const int N 51; pll d[4][4][4] {{{{0, 0}, {1, 0}, {2, 0}, {2, 1}}, {{0, 0}, {1, 0}, {1, -1}, {1, -2}}, {{0, 0}, {0, 1}, {1, 1},…

Redisson全面解析:从使用方法到工作原理的深度探索

文章目录 写在文章开头详解Redisson基本数据类型基础配置字符串操作列表操作映射集阻塞队列延迟队列更多关于Redisson详解Redisson 中的原子类详解redisson中的发布订阅模型小结参考写在文章开头 Redisson是基于原生redis操作指令上进一步的封装,屏蔽了redis数据结构的实现细…

用Python进行websocket接口测试

这篇文章主要介绍了用Python进行websocket接口测试&#xff0c;帮助大家更好的理解和使用python&#xff0c;感兴趣的朋友可以了解下 我们在做接口测试时&#xff0c;除了常见的http接口&#xff0c;还有一种比较多见&#xff0c;就是socket接口&#xff0c;今天讲解下怎么用P…

【RocketMQ 存储】- 同步刷盘服务 GroupCommitService

文章目录 1. 前言2. 参数3. 队列相关4. 核心逻辑 run4.1 waitForRunning4.2 doCommit4.3 flush 5. 小结 本文章基于 RocketMQ 4.9.3 1. 前言 RocketMQ 存储部分系列文章&#xff1a; 【RocketMQ 存储】- RocketMQ存储类 MappedFile【RocketMQ 存储】- 一文总结 RocketMQ 的存…

基于yolov11的阿尔兹海默症严重程度检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv11的阿尔兹海默症严重程度检测系统是一种创新的医疗辅助工具&#xff0c;旨在通过先进的计算机视觉技术提高阿尔兹海默症的早期诊断和病情监测效率。阿尔兹海默症是一种渐进性的神经退行性疾病&#xff0c;通常表现为认知障碍、记忆丧失和语言障碍等症状…

IDEA编写SpringBoot项目时使用Lombok报错“找不到符号”的原因和解决

目录 概述|背景 报错解析 解决方法 IDEA配置解决 Pom配置插件解决 概述|背景 报错发生背景&#xff1a;在SpringBoot项目中引入Lombok依赖并使用后出现"找不到符号"的问题。 本文讨论在上述背景下发生的报错原因和解决办法&#xff0c;如果仅为了解决BUG不论原…

【异常解决】在idea中提示 hutool 提示 HttpResponse used withoud try-with-resources statement

博主介绍&#xff1a;✌全网粉丝22W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…

搜维尔科技:提供人形机器人传感器的应用案例分析

视觉传感器 • 家庭服务场景&#xff1a;在家庭清洁机器人中&#xff0c;视觉传感器可以识别家具、障碍物的位置和形状&#xff0c;规划清洁路径&#xff0c;避开桌椅、宠物玩具等。如小米扫地机器人&#xff0c;通过视觉传感器与算法结合&#xff0c;能构建房间地图&#xff…

虹科波形小课堂 | 三分钟掌握车辆相对压缩测试!不拆发动机、不测缸压就能判断故障缸!

不拆发动机、不测缸压&#xff0c;只测个电流也能知道哪个缸压缩有问题&#xff1f;没错&#xff01;做个相对压缩测试&#xff0c;测下起动电流就行&#xff0c;简单又实用&#xff01;今天&#xff0c;从原理到方法&#xff0c;几分钟教会你&#xff01; 我们都知道&#xf…

自然语言处理NLP_[1]-NLP入门

文章目录 1.自然语言处理入门1. 什么是自然语言处理2.自然语言处理的发展简史3 自然语言处理的应用场景1. **机器翻译**2. **文本分类**3. **情感分析**4. **问答系统**5. **文本生成**6. **信息抽取**7. **语音识别与合成**8. **文本摘要**9. **搜索引擎优化**10. **聊天机器人…

无限使用Cursor

原理&#xff1a;运行程序获得15天的免费试用期&#xff0c;重新运行程序重置试用期&#xff0c;实现无限使用。免费的pro账号&#xff0c;一个月有250的高级模型提问次数。 前提&#xff1a;已安装cursor cursor-vip工具&#xff1a;https://cursor.jeter.eu.org?p95d60efe…

LIMO:少即是多的推理

25年2月来自上海交大、SII 和 GAIR 的论文“LIMO: Less is More for Reasoning”。 一个挑战是在大语言模型&#xff08;LLM&#xff09;中的复杂推理。虽然传统观点认为复杂的推理任务需要大量的训练数据&#xff08;通常超过 100,000 个示例&#xff09;&#xff0c;但本文展…

一种基于Leaflet.Legend的图例动态更新方法

目录 前言 一、场景再现 1、需求描述 2、核心方法介绍 3、存在的问题 二、问题解决 1、重复解决办法 2、图例不展示解决办法 3、成果展示 三、总结 前言 在当今数字化时代&#xff0c;地理信息系统&#xff08;GIS&#xff09;技术已经广泛应用于各个领域&#xff0c;…