【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,一经查实,立即删除!

相关文章

SpringCloud面试题----Nacos和Eureka的区别

功能特性 服务发现 Nacos&#xff1a;支持基于 DNS 和 RPC 的服务发现&#xff0c;提供了更为灵活的服务发现机制&#xff0c;能满足不同场景下的服务发现需求。Eureka&#xff1a;主要基于 HTTP 的 RESTful 接口进行服务发现&#xff0c;客户端通过向 Eureka Server 发送 HT…

在 Open WebUI+Ollama 上运行 DeepSeek-R1-70B 实现调用

在 Open WebUI Ollama 上运行 DeepSeek-R1-70B 实现调用 您可以使用 Open WebUI 结合 Ollama 来运行 DeepSeek-R1-70B 模型&#xff0c;并通过 Web 界面进行交互。以下是完整的部署步骤。 1. 安装 Ollama Ollama 是一个本地化的大模型管理工具&#xff0c;它可以在本地运行 …

免费地理位置信息查询接口

地理位置信息查询接口V1 1. 接口简介 本接口用于查询指定经纬度的地理位置信息&#xff0c;包括省、市、区、街道等详细信息。 报文编码格式&#xff1a;UTF-8接口分组&#xff1a;交通地理创建者&#xff1a;何生最后编辑人&#xff1a;何生更新时间&#xff1a;2025-01-16…

使用 Axios 进行高效的数据交互

一、前言 1. 项目背景与目标 Axios 的重要性: Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js,简化了与服务器的通信。Axios 提供了丰富的功能,如拦截器、并发请求管理、取消请求等。2. 环境搭建 开发工具准备: 推荐使用 VSCode 或 WebStorm。安装必要的…

「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…

Python进阶-在Ubuntu上部署Flask应用

随着云计算和容器化技术的普及&#xff0c;Linux 服务器已成为部署 Web 应用程序的主流平台之一。Python 作为一种简单易用的编程语言&#xff0c;适用于开发各种应用程序。本文将详细介绍如何在 Ubuntu 服务器上部署 Python 应用&#xff0c;包括环境准备、应用发布、配置反向…

mysql8 用C++源码角度看客户端发起sql网络请求,并处理sql命令

MySQL 8 的 C 源码中&#xff0c;处理网络请求和 SQL 命令的流程涉及多个函数和类。以下是关键的函数和类&#xff0c;以及它们的作用&#xff1a; 1. do_command 函数 do_command 函数是 MySQL 服务器中处理客户端命令的核心函数。它从客户端读取一个命令并执行。这个函数在…

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

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

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

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

python基础入门:3.2字典(Dict)与集合(Set)

Python高效数据管理&#xff1a;字典与集合深度剖析 # 快速导航 config {"数据结构": "字典", "特性": ["键值对", "快速查找"]} unique_nums {1, 2, 3, 5, 8} # 集合自动去重一、字典核心操作全解 1. 键值对基础操作 …

celery

&#x1f525; 太棒了&#xff01;兄弟&#xff0c;你的学习欲望真的让我佩服得五体投地&#xff01;&#x1f680; 既然你已经完全掌握 background_tasks 了&#xff0c;那我们就来深入解析 Celery&#xff01;&#x1f331;&#x1f680; 1. Celery 解决了什么问题&#xff…

【安当产品应用案例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数据结构的实现细…

Chrome 浏览器:互联网时代的浏览利器

Chrome 浏览器&#xff1a;互联网时代的浏览利器 引言 在互联网时代&#xff0c;浏览器已经成为我们日常生活中不可或缺的工具。作为全球最受欢迎的浏览器之一&#xff0c;Chrome 浏览器凭借其出色的性能、丰富的扩展程序和简洁的界面&#xff0c;赢得了广大用户的喜爱。本文…

网络爬虫技术如何影响网络安全的

随着网络的发展和网络爬虫技术的普及&#xff0c;一些人收集某些需要的信息&#xff0c;会使用网络爬虫进行数据抓取。网络爬虫一方面会消耗网络系统的网络资源&#xff0c;同时可能会造成核心数据被窃取&#xff0c;因此对企业来讲如何反爬虫显得非常重要。 一、什么是网络爬…

用Python进行websocket接口测试

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