基于linux下的高并发服务器开发(第四章)- 多线程实现并发服务器

>>了解文件描述符

文件描述符分为两类,一类是用于监听的,一类是用于通信的,在服务器端既有监听的,又有通信的。而且在服务器端只有一个用于监听的文件描述符,用于通信的文件描述符是有n个。和多少个客户端建立了连接,在服务器端就有多少个用于通信的文件描述符。在客户端只有一类文件描述符,就是用于通信的文件描述符。有了这些文件描述符,我们就可以进行网络io操作了。

网络io其实就是网络数据得到读或写操作。那么这个读或写操作的主体是谁呢?其实是内核里边的一块内存。在进行文件IO操作的时候,操作的是磁盘上的某一块内存。我们通过文件描述符就可以把数据从磁盘里边读出来,或者说把数据写入到磁盘中。我们在进行套接字通信的时候,每一个文件描述符它对应的是内核里边的两块内存,一块内存我们称之为读缓冲区,另一块内存我们称之为写缓冲区。读缓冲区是用来接收数据的,写缓冲区是用来发送数据的。

>>总结:在进行套接字通信的时候,每一个文件描述符都在内核的内存里边对应两块内存,一块内存我们称之为读缓冲区,用来接收数据的;另一块内存我们称之为写缓冲区,用来发送数据的。

>>pthread_create 

基于linux下的高并发服务器开发(第三章)-(3.1-3.2)线程概述和创建_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/131878249?spm=1001.2014.3001.5501

一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程
称之为子线程。
程序中默认只有一个进程,fork()函数调用,2进行
程序中默认只有一个线程,pthread_create()函数调用,2个线程。
 
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
void *(*start_routine) (void *), void *arg);
 
	- 功能:创建一个子线程
	- 参数:
		- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
		- attr : 设置线程的属性,一般使用默认值,NULL
		- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
		- arg : 给第三个参数使用,传参
	- 返回值:
		成功:0
		失败:返回错误号。这个错误号和之前errno不太一样。
		获取错误号的信息:  char * strerror(int errnum);

>>pthread_detach

基于linux下的高并发服务器开发(第三章)- 3.5 线程的分离_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/131880285?spm=1001.2014.3001.5501

/*
    #include <pthread.h>
    int pthread_detach(pthread_t thread);
        - 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
          1.不能多次分离,会产生不可预料的行为。
          2.不能去连接一个已经分离的线程,会报错。
        - 参数:需要分离的线程的ID
        - 返回值:
            成功:0
            失败:返回错误号
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
 
void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    return NULL;
}
 
int main() {
 
    // 创建一个子线程
    pthread_t tid;
 
    int ret = pthread_create(&tid, NULL, callback, NULL);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error1 : %s\n", errstr);
    }
 
    // 输出主线程和子线程的id
    printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());
 
    // 设置子线程分离,子线程分离后,子线程结束时对应的资源就不需要主线程释放
    ret = pthread_detach(tid);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error2 : %s\n", errstr);
    }
 
    // 设置分离后,对分离的子线程进行连接 pthread_join()
    // ret = pthread_join(tid, NULL);
    // if(ret != 0) {
    //     char * errstr = strerror(ret);
    //     printf("error3 : %s\n", errstr);
    // }
 
    pthread_exit(NULL);
 
    return 0;
}

网友评论lecranek:老师这集关于`pthread_create()`这里讲得蛮好的,是逐步引入了为什么`sockInfos`需要这样定义,而不是敲好代码直接带过。设置成全局变量是避免在栈上变量被销毁

多线程实现并发服务器

以下思路和文字总结来自爱编程的大丙:服务器并发 | 爱编程的大丙 (subingwen.cn)

大丙老师的课程也讲得非常棒(๑•̀ㅂ•́)و✧

多线程中的线程有两大类:主线程(父线程)和子线程,他们分别要在服务器端处理监听和通信流程。根据多进程的处理思路,就可以这样设计了:

  • 主线程:
    • 负责监听,处理客户端的连接请求,循环调用accept()函数
    • 创建子线程:建立一个新的连接,就创建一个新的子线程,让这个子线程和对应的客户端通信
    • 回收子线程资源:由于回收需要调用阻塞函数,这样就会影响accept()直接做线程分离即可。
  • 子线程:负责通信,基于主线程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
    • 发送数据:send() / write()
    • 接收数据:recv() / read()

在多线程版的服务器端程序中,多个线程共用同一个地址空间,有些数据是共享的,有些数据的独占的,下面来分析一些其中的一些细节:

  • 同一地址空间中的多个线程的栈空间是独占的
  • 多个线程共享全局数据区,堆区,以及内核区的文件描述符等资源,因此需要注意数据覆盖问题,并且在多个线程访问共享资源的时候,还需要进行线程同步。

思路:首先accept是有一个线程的,另外只要这个accept成功的和一个客户端建立了连接,那么我们就需要创建一个对应的线程,用这个线程和客户端进行网络通信。每建立一个连接,通信的线程就需要创建出来一个。这样的话,能够保证通信的线程和客户端是一个一一对应的关系,也就是说用于通信的线程一共是有n个,用于建立连接的线程只有一个。在线程里边一共分为两类,一类是主线程,一类是子线程,只要是建立了新连接,主线程创建一个子线程,让子线程和对应建立连接的那个客户端去通信就行了。 

这个图的思路和分析:我们需要在主线程里面不停的进行accept操作,如果说有新的客户端连接就建立连接。如果说没有新的客户端连接,主线程就阻塞在accept这个函数上。在主线程里边每创建一个新连接,就需要调用pthread_create创建一个子线程让这个子线程和对应的那个客户端进行网络通信。

考虑细节:多线程之间有哪些资源是共享的?哪些资源是不共享的?

全局和堆区是共享的,他们可以共同访问全局数据区里面的某一块内存或者说堆区里边的某一块内存。如果说有三个线程,那么这个栈区会被分成三份,每个线程都有一块属于自己的独立的栈空间,因此对于多个线程来说,他们并不是共享的。

 1.sockInfo结构

struct sockInfo {
    int fd;// 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;// 线程号
};
struct sockInfo sockInfos[128];

需要注意父子线程共用同一个地址空间中的文件描述符,因此每当在主线程中建立一个新的连接,都需要将得到文件描述符值保存起来,不能在同一变量上进行覆盖,这样做丢失了之前的文件描述符值也就不知道怎么和客户端通信了。

在上面示例代码中是将成功建立连接之后得到的用于通信的文件描述符值保存到了一个全局数组中,每个子线程需要和不同的客户端通信,需要的文件描述符值也就不一样,只要保证存储每个有效文件描述符值的变量对应不同的内存地址,在使用的时候就不会发生数据覆盖的现象,造成通信数据的混乱了。

把结构体数组里边的每一个元素中的文件描述符设置为-1,这样的话,可以通过这个服务器来判断当前的数组元素是不是被占用的。如果这个数组元素被占用了,它的文件描述符的值应该是一个有效值。如果是-1,是无效值。也就意味着这个元素是空闲的,是可用的

 2.初始化数据

// 初始化数据
int max = sizeof(sockInfos) / sizeof(sockInfos[0]);
for(int i = 0;i < max; i++) {
	bzero(&sockInfos[i],sizeof(sockInfos[i]));
	sockInfos[i].fd = -1;
	sockInfos[i].tid = -1;
} 

3.创建子线程

struct sockInfo* pinfo;
for(int i = 0;i < max;i++) {
	// 从这个数组中找到一个可用的sockInfo元素
	if(sockInfos[i].fd == -1) {
		pinfo = &sockInfos[i];
		break;
	}
	if(i == max - 1) {
		sleep(1);
		//i--;
		i=-1;
	}
}

pinfo->fd = cfd;
memcpy(&pinfo->addr,&clientaddr,len);

// 创建子线程
pthread_create(&pinfo->tid,NULL,working,pinfo);

4.线程分离

pthread_detach(pinfo->tid);

server_thread.c

#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    客户端的信息    线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo*)arg;

    char clientIP[16];
    inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,clientIP,sizeof(clientIP));
    unsigned short clientPort = ntohs(9999);
    printf("client ip is : %s, port is %d\n",clientIP,clientPort);

    // 接收客户端发来的数据
    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);
    pinfo->fd=-1;
    return NULL;    
}

int main() {

    // 创建socket
    int lfd = socket(AF_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++) {
        bzero(&sockInfos[i],sizeof(sockInfos[i]));
        sockInfos[i].fd = -1;
        sockInfos[i].tid = -1;
    }   

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while (1)
    {
        struct sockaddr_in clientaddr;
        int len = sizeof(clientaddr);
        // 接受连接
        int cfd = accept(lfd,(struct sockaddr*)&clientaddr,&len);
        
        struct sockInfo* pinfo;
        for(int i = 0;i < max;i++) {
            // 从这个数组中找到一个可用的sockInfo元素
            if(sockInfos[i].fd == -1) {
                pinfo = &sockInfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                //i--;
                i=-1;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr,&clientaddr,len);

        // 创建子线程
        pthread_create(&pinfo->tid,NULL,working,pinfo);

        pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
}

client.c

// TCP 通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.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,"192.168.88.129",&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] = {0};
    int i = 0;
    while(1) {
        sprintf(recvBuf,"data : %d\n",i++);
        // 给服务端发送数据
        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....\n");
            break;
        }
        sleep(1);
    }
    return 0;
}

相关socket的api可以看这一篇,有介绍到: 

基于linux下的高并发服务器开发(第四章)- 多进程实现并发服务器(回射服务器)_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/132021287?spm=1001.2014.3001.5501

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spring Boot】请求参数传json对象,后端采用(map)CRUD案例(101)

请求参数传json对象&#xff0c;后端采用&#xff08;map&#xff09;接收的前提条件&#xff1a; 1.Spring Boot 的Controller接受参数采用&#xff1a;RequestBody 2.需要一个Json工具类&#xff0c;将json数据转成Map&#xff1b; 工具类&#xff1a;Json转Map import com…

Linux部署jar包,隐藏命令行参数

Linux部署jar包&#xff0c;隐藏命令行参数 一、背景需求二、查阅资料三、实现隐藏库3.1、测试test.c3.2、设置隐藏库3.3、验证 四、应用jar启动命令五、直接应用结果 最新项目安全检测&#xff0c;发现配置文件中数据库密码&#xff0c;redis密码仍处理明文状态 于是整理了一篇…

linux快速安装tomcat

linux快速安装tomcat 前提安装好jdk 下载Tomcat安装包 wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.0.27/bin/apache-tomcat-10.0.27.tar.gz如果出现颁发的证书已经过期的错误提示,用下面命令 wget --no-check-certificate https://dlcdn.apache.org/tomcat/tomcat-1…

链表的总体涵盖以及无哨兵位单链表实现——【数据结构】

&#x1f60a;W…Y&#xff1a;个人主页 在学习之前看一下美丽的夕阳&#xff0c;也是很不错的。 如果觉得博主的美景不错&#xff0c;博客也不错的话&#xff0c;关注一下博主吧&#x1f495; 在上一期中&#xff0c;我们说完了顺序表&#xff0c;并且提出顺序表中的问题 1. 中…

AWS——02篇(AWS之服务存储EFS在Amazon EC2上的挂载——针对EC2进行托管文件存储)

AWS——02篇&#xff08;AWS之服务存储EFS在Amazon EC2上的挂载——针对EC2进行托管文件存储&#xff09; 1. 前言2. 关于Amazon EFS2.1 Amazon EFS全称2.2 什么是Amazon EFS2.3 优点和功能2.4 参考官网 3. 创建文件系统3.1 创建 EC2 实例3.2 创建文件系统 4. 在Linux实例上挂载…

Pytorch深度学习-----神经网络之Sequential的详细使用及实战详解

系列文章目录 PyTorch深度学习——Anaconda和PyTorch安装 Pytorch深度学习-----数据模块Dataset类 Pytorch深度学习------TensorBoard的使用 Pytorch深度学习------Torchvision中Transforms的使用&#xff08;ToTensor&#xff0c;Normalize&#xff0c;Resize &#xff0c;Co…

Sui主网升级至V1.6.3版本

Sui主网现已升级至V1.6.3版本&#xff0c;此升级包含了多项修复和优化。升级要点如下所示&#xff1a; #13029 在构建Move代码时&#xff0c;可能会出现与实现自定义transfer/share/freeze函数相关的额外linter警告。这些函数是为了实施自定义的transfer/share/freeze策略而…

Vue3 基础知识点汇总 自学笔记,记录难点 和 新知识点

1.vue3 基础 1.1vue3基础及创建 npm init vue@latest1.2.熟悉项目目录及关键文字 1.3 组合式API-setup 1.4.组合式 API reactive 和ref 函数 (都是为了生成响应式数据) 1.5.组合式API-computed 计算属性函数 1.6.watch 函数 1.7.组合式API-生命周期函数 1.8.组合式 API-父子…

记录 Vue3 + Ts 类型使用

阅读时长: 10 分钟 本文内容&#xff1a;记录在 Vue3 中使用 ts 时的各种写法. 类型大小写 vue3 ts 项目中&#xff0c;类型一会儿大写一会儿小写。 怎么区分与基础类型使用? String、string、Number、number、Boolean、boolean … 在 js 中&#xff0c; 以 string 与 String…

【多线程初阶】多线程案例之单例模式

文章目录 前言1. 什么是单例模式2. 饿汉模式3. 懒汉模式 --- 单线程版4. 懒汉模式 --- 多线程版5. 懒汉模式 --- 多线程改进版总结 前言 本文主要给大家讲解多线程的一个重要案例 — 单例模式. 关注收藏, 开始学习吧&#x1f9d0; 1. 什么是单例模式 单例模式是一种很经典的…

Prometheus-各种exporter

一、 nginx-prometheus-exporter 1 nginx 配置 1.1 Nginx 模块支持 nginx 安装的时候需要有 nginx 的状态模块: stub_status 可通过如下命令检查 nginx -V 2>&1 | grep -o with-http_stub_status_module1.2 Nginx 配置文件配置 添加如下配置到自己 nginx 的配置文…

落地数字化管理,提升企业市场竞争力

数字化企业管理方案是一种利用数字技术和信息系统来提升企业管理效率和运营效果的策略。 潜在的数字化企业管理方案 1、企业资源规划&#xff08;ERP&#xff09;系统&#xff1a;建立一个集成的ERP系统来统一管理企业的各项业务流程&#xff0c;包括采购、销售、库存管理、财…

Java超级玛丽小游戏制作过程讲解 第一天 创建窗口

package com.sxt;import javax.swing.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener;public class MyFrame extends JFrame implements KeyListener {//设置窗口的大小为800*600public MyFrame() {this.setSize(800, 600);//设置窗口中显示this.setLo…

Cpp9 — map和set

map和set STL分为序列式容器&#xff08;vector、list、deque&#xff09;和关联式容器&#xff08;map、set&#xff09; 序列式容器&#xff1a;数据与数据之间没有很强的联系。&#xff08;各个数据之间没什么关联&#xff09;。底层为线性序列的数据结构&#xff0c;里面…

【云原生K8s】二进制部署单master K8s+etcd集群

一、实验设计 mater节点master01192.168.190.10kube-apiserver kube-controller-manager kube-scheduler etcd node节点node01192.168.190.20kubelet kube-proxy docker (容…

elementUI全屏loading的使用(白屏的解决方案)

官网中有使用方法&#xff0c;但是我实际上手之后会出现白屏&#xff0c;解决办法如下&#xff1a; <el-button type"text" size"small" click"delRow(scope)"> 删除</el-button>loading: false, // loading 动画loadingInstance…

ubuntu下,在vscode中使用platformio出现 Can not find working Python 3.6+ Interpreter的问题

有一段时间没有使用platformio了&#xff0c;今天突然使用的时候&#xff0c;发现用不了&#xff0c;报错&#xff1a; Ubuntu PlatformIO: Can not find working Python 3.6 Interpreter. Please install the latest Python 3 and restart VSCode。 上网一查&#xff0c;发现…

【NLP概念源和流】 06-编码器-解码器模型(6/20 部分)

一、说明 在机器翻译等任务中,我们必须从一系列输入词映射到一系列输出词。读者必须注意,这与“序列标记”不同,在“序列标记”中,该任务是将序列中的每个单词映射到预定义的类,如词性或命名实体任务。 作者生成 在上面的

基于回溯算法实现八皇后问题

八皇后问题是一个经典的计算机科学问题&#xff0c;它的目标是将8个皇后放置在一个大小为88的棋盘上&#xff0c;使得每个皇后都不会攻击到其他的皇后。皇后可以攻击同一行、同一列和同一对角线上的棋子。 一、八皇后问题介绍 八皇后问题最早由国际西洋棋大师马克斯贝瑟尔在18…

计算机视觉与图形学-神经渲染专题-第一个基于NeRF的自动驾驶仿真平台

如今&#xff0c;自动驾驶汽车可以在普通情况下平稳行驶&#xff0c;人们普遍认识到&#xff0c;真实的传感器模拟将在通过模拟解决剩余的极端情况方面发挥关键作用。为此&#xff0c;我们提出了一种基于神经辐射场&#xff08;NeRF&#xff09;的自动驾驶模拟器。与现有作品相…