[C++ 网络协议] 多线程服务器端

具有代表性的并发服务器端实现模型和方法:

多进程服务器:通过创建多个进程提供服务。

多路复用服务器:通过捆绑并统一管理I/O对象提供服务。

多线程服务器:通过生成与客户端等量的线程提供服务。✔

目录

1. 线程的概念

1.1 为什么要引入线程

1.2 线程和进程的差异

2.线程函数

2.1 线程的创建

2.1 分离线程

3. 线程存在的问题和临界区

4. 线程安全

5. 线程同步

5.1 互斥量(Mutual Exclusion)

5.1.1 概念

5.1.2 互斥量的创建

5.1.3 互斥量的销毁

5.1.4 上锁和解锁

5.2 信号量

5.2.1 概念

5.2.2 创建信号量

5.2.3 销毁信号量

5.2.4 post和wait

6. 多线程服务器端的实现


1. 线程的概念

1.1 为什么要引入线程

之前学习的内容中,讲解了多进程服务器端的实现方法,明确了其缺点:

  1. 创建进程的过程会带来一定的开销
  2. 为了完成进程间的数据交换,要进行特殊的IPC技术(管道通信等)
  3. 每秒多次的上下文切换(进程A和进程B之间切换运行,操作系统要先将进程A的相关信息移出内存,再读入进程B的相关信息),带来的巨大开销

所以,为了保持多进程的优点,同时在一定程度上客服其缺点,就引入了线程,也被称为“轻量级进程”,其相比于进程有如下优点:

  1. 线程的创建和上下文切换比进程的创建和上下文切换更快。
  2. 线程间的通信,无需特殊技术。

1.2 线程和进程的差异

对于进程来说,每次创建新进程,都要复制旧进程的整个内存区域,包括:全局数据区、堆区、栈区。但如果创建进程只是为了获得多个代码执行流,那么就不应该复制整个进程的内存区域。如图:

所以,线程共享数据区、堆区,而分离栈区,进程是分离整个内存区。

进程和线程可以定义为如下形式:

进程:在操作系统构成单独执行流的单位

线程:在进程中构成单独执行流的单位

线程和进程的关系:

进程就像是一个装有线程的篮子,里面的运行注意都是线程,main函数的执行也是由线程来执行的,一般被称为主线程。如图:

2.线程函数

2.1 线程的创建

#include<pthread.h>int pthread_create(
pthread_t* restrict thread,            //保存新创建线程ID的变量地址值
const pthread_attr_t* restrict attr,   //传递线程属性的参数,传递NULL,创建默认属性的线程
void* (*start_routine)(void*),         //线程的main函数,单独执行流中执行的函数地址值
void* restrict arg                     //第三个参数调用函数时要传入的参数信息的变量地址值
);
成功返回0
失败返回其他值

restrict关键字:它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改。这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码。

线程的代码在编译时命令行需要添加-lpthread的声明来连接线程库,如:

gcc thread.c -o thr -lpthread

传递多个参数的方法:定义一个结构体,存放参数,然后进行指针的转换。

struct thread_param
{int fd;sockaddr_in addr;
};int main()
{......thread_param params;params.fd=clientfd;params.addr=clientAddr;pthread_create(&clientthread,NULL,thread_client_handle,(void*)&params);
}void* thread_client_handle(void* args)
{thread_param params=*(thread_param*)args;int clientfd=params.fd;sockaddr_in clientAddr=params.addr;......
}

2.1 分离线程

#include<pthread.h>int pthread_join(
pthread_t thread,    //要分离的线程ID
void** statrus       //保存线程的main函数的返回值的指针变量地址值
);
成功返回0
失败返回其他值

函数功能:阻塞住主线程的运行,直到这个子线程运行结束,被销毁后,主线程才会继续执行。

#include<pthread.h>int pthread_detach(
pthread_t thread      //要分离的线程ID
);
成功返回0
失败返回其他值

函数功能:不会阻塞主线程的运行,子线程自己运行结束后,自己进行销毁。调用该函数后,不能再调用pthread_join。

3. 线程存在的问题和临界区

当有多个线程同时访问一个共享数据时,就很大概率导致意想不到的错误发生。例如:

当有2个线程访问同一个全局变量num初始值为100,并都要对其进行+1操作,理想情况下得到的值应该是102,但可能会有如下情况的发生:

        1.线程A首先访问变量num,要给num进行+1的操作,其会做以下事情

                A.首先从全局数据区中读取num的值。

                B.再将其传递到CPU,让CPU计算后,得到其结果。

                C.再把值写入到num变量中。

        2.在线程A执行到C之前,此时线程B就读取了num的值,然后当线程B执行完以上步骤时,此时num的值已经变为了101,但是线程A此时也同时进行了C步,那么num最终的结果将仍然是101,这并不是理想的结果。

所以可能会出现各种不同的问题。这样所有线程都执行的函数,这类函数内部就存在临界区

临界区的位置是在:函数内同时运行多个线程时引起问题的多条语句构成的代码块

我们要如何避免这种线程共享数据的问题的发生?

        1.使用线程安全的函数

        2.实现线程同步

4. 线程安全

根据临界区是否引起问题,函数可以分为两类:

        1.线程安全函数:被多个线程同时调用也不会引发问题的函数。一般来说,大部分标准函数都是线程安全函数。

        2.非线程安全函数:与线程安全函数相反,多个线程调用可能会引发问题。

怎么使用线程安全函数?

        1.平台在定义非线程安全函数的同时,就提供了具有相同功能的线程安全函数。具有线程安全函数的名称后缀通常为_r。例如:gethostbyname(非线程安全函数),gethostbyname_r(线程安全函数)。

        2.或者可以在头文件处,声明_REENTRANT宏,声明了此宏,就会将程序内的函数自动改为线程安全函数调用。

或者在编译时加上gcc -D_REEDTRANT thread.c -o thread -lpthread

5. 线程同步

线程同步一般涉及如下两个情况:

        1.同时访问统一内存空间时的情况。(互斥量)

        2.指定访问统一内存空间的线程执行顺序的情况。(信号量)

5.1 互斥量(Mutual Exclusion)

5.1.1 概念

互斥量是一种同步技术,其提供锁机制,当某一个线程在调用临界区代码时,可以使用互斥量将临界区保护起来,即将其锁住,其他线程将会等待此线程解锁之后,才进入调用临界区代码。

5.1.2 互斥量的创建

方式一:

#include<pthread.h>int pthread_mutex_init(
pthread_mutex_t* mutex,            //创建互斥量时传递保护互斥量的变量地址
const pthread_mutexattr_t* attr    //传递创建的互斥量的属性,传递NULL则默认
);
成功返回0
失败返回其他值

方式二:

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

不推荐第二种方式,因为不容易发现错误。

5.1.3 互斥量的销毁

#include<pthread.h>int pthread_mutex_destroy(
pthread_mutex_t* mutex,            //销毁互斥量时传递的互斥量地址值
);
成功返回0
失败返回其他值

5.1.4 上锁和解锁

#include<pthread.h>int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
成功返回0
失败返回其他值

注意:上锁(lock)后一定要记得解锁(unlock),不然其他线程将一直处于阻塞状态,造成死锁。

5.2 信号量

5.2.1 概念

信号量也是一种同步技术,利用二进制信号量(只用0和1)来完成控制线程顺序为中心的同步方法。

5.2.2 创建信号量

#include<semaphore.h>int sem_init(
sem_t* sem,            //创建信号量时传递保存的信号量的变量地址值
int pshared,           //传递其他值时,创建可以多个进程共享的信号量//传递0时,创建只允许一个进程使用的信号量
unsigned int value     //创建的信号量的初始值
);
成功返回0
失败返回其他值

5.2.3 销毁信号量

#include<semaphore.h>int sem_destroy(sem_t* sem);    //销毁时传递需要销毁的信号量变量地址值
成功返回0
失败返回其他值

5.2.4 post和wait

#include<semaphore.h>//sem是传递保存信号量读取值的变量地址值
int sem_wait(sem_t* sem);    //信号量-1,类似于互斥量的lock
int sem_post(sem_t* sem);    //信号量+1,类似于互斥量的unlock
成功返回0
失败返回其他值

调用sem_wait时,因为信号量的值不能<0,所以当线程执行sem_wait时sem的值是0时,那么线程此时就会阻塞住,当sem的值是1,线程会继续执行,同时会将sem减为0。如:

sem_t signal=1;
sem_wait(&signal);    //signal变为0,且继续执行
...
sem_post(&signal);    //signal变为1sem_t signal2=0;
sem_wait(&signal2);    //signal为0,阻塞状态
...
sem_post(&signal2);    //signal变为1

保证线程的访问顺序,需要两个信号量,如下面的代码,需要保证线程A先处理,线程B再取值。

static sem_t sem_one;
static sem_t sem_two;int main()
{sem_init(&sem_one,0,0);    //sem_one初始化为0sem_init(&sem_two,0,1);    //sem_two初始化为1......//线程A先create和join
}void* Handle(void* arg)      //线程A
{sem_wait(&sem_two);      //先进入执行,将sem_two置为0,等待线程B执行结束,将sem_two置为1,再第二次执行......sem_post(&sem_one);
}void* accu(void* arg)        //线程B
{sem_wait(&sem_one);      //等待线程A执行到sem_post(&sem_one)将其置为1......sem_post(&sem_two);
}

6. 多线程服务器端的实现(聊天室)

实现思路:

服务器端作为一个中转站,将一个客户端发送的消息,发送给另一个客户端。

map容器:key是ip地址,value是文件描述符,每连接一个客户端把这个客户端的ip地址和文件描述符以键值对的形式存入map中。

message结构体:客户端之间接收和发送的数据,里面存有要发送的客户端的ip地址,以及聊天消息内容。

mutex互斥量:保护对map进行插入的代码段的临界区。

服务器端代码:

#define _REENTRANT
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<semaphore.h>
#include<iterator>
#include<map>struct thread_param
{int fd;sockaddr_in addr;
};struct message
{char ip[17];char content[100];
};void* thread_client_handle(void* args);std::map<std::string,int> mapClient;pthread_mutex_t mutex;int main()
{int serverfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);if(serverfd==-1){std::cout<<"创建套接字错误!"<<std::endl;return 0;}int soreuse=true;socklen_t soreuselen=sizeof(soreuse);setsockopt(serverfd,SOL_SOCKET,SO_REUSEADDR,(void*)&soreuse,soreuselen);sockaddr_in serverAddr;memset(&serverAddr,0,sizeof(serverAddr));serverAddr.sin_family=AF_INET;serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);serverAddr.sin_port=htons(9130);if(-1==bind(serverfd,(sockaddr*)&serverAddr,sizeof(serverAddr))){std::cout<<"绑定套接字失败!"<<std::endl;return 0;}if(-1==listen(serverfd,2)){std::cout<<"监听失败!"<<std::endl;return 0;}while(1){sockaddr_in clientAddr;memset(&clientAddr,0,sizeof(clientAddr));socklen_t clientAddrLen=sizeof(clientAddr);int clientfd=accept(serverfd,(sockaddr*)&clientAddr,&clientAddrLen);pthread_mutex_init(&mutex,NULL);pthread_t clientthread;thread_param params;params.fd=clientfd;params.addr=clientAddr;pthread_create(&clientthread,NULL,thread_client_handle,(void*)&params);pthread_detach(clientthread);}pthread_mutex_destroy(&mutex);close(serverfd);
}void* thread_client_handle(void* args)
{thread_param params=*(thread_param*)args;int clientfd=params.fd;sockaddr_in clientAddr=params.addr;if(clientfd==-1){std::cout<<"客户端连接失败!"<<std::endl;return NULL;}else{pthread_mutex_lock(&mutex);std::string ip=std::string(inet_ntoa(clientAddr.sin_addr));mapClient.insert(std::make_pair(ip,clientfd));std::cout<<"客户端:"<<ip<<"已连接!"<<std::endl;pthread_mutex_unlock(&mutex);}char buff[1024];int readLen;while((readLen=read(clientfd,buff,sizeof(buff)))){message* msg=(message*)buff;if(msg){std::string peerIp=std::string(msg->ip);std::map<std::string,int>::iterator it=mapClient.find(peerIp);if(it!=mapClient.end()){int peerfd=it->second;write(peerfd,(const char*)msg,readLen);}else{std::cout<<"找不到您要交流的对象或您交流的对象已退出"<<std::endl;break;}}}close(clientfd);
}

客户端实现思路:

先输入要交流的对象的IP地址,然后创建一个子线程,主线程写,子线程读。

客户端代码:

#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<pthread.h>struct message
{char ip[17];char content[100];
};void* thread_read(void* arg);std::string strIp;int main()
{int clientfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);if(clientfd==-1){std::cout<<"socket fail!"<<std::endl;return 0;}sockaddr_in clientAddr;memset(&clientAddr, 0, sizeof(clientAddr));clientAddr.sin_family = AF_INET;clientAddr.sin_port = htons(9130);clientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");if (connect(clientfd, (sockaddr*)&clientAddr, sizeof(clientAddr)) == -1){std::cout << "连接失败!" << std::endl;return 0;}std::cout<<"请输入要交流的对方的IP地址:"<<std::endl;std::cin>>strIp;pthread_t readthread;pthread_create(&readthread,NULL,thread_read,(void*)&clientfd);pthread_detach(readthread);std::string chatcontent;while(1){std::cout<<"您(输入'Q'退出聊天):";std::cin>>chatcontent;if(chatcontent.compare("Q")==0){break;}message msg;strcpy(msg.ip, strIp.c_str());strcpy(msg.content, chatcontent.c_str());write(clientfd,(const char*)&msg,sizeof(msg));}close(clientfd);
}void* thread_read(void* arg)
{int clientfd=*(int*)arg;char buff[1024];int readLen;while((readLen=read(clientfd,buff,sizeof(buff)))){message* msg=(message*)buff;std::cout<<"对方(IP:"<<strIp<<"):"<<msg->content<<std::endl;}
}

执行结果:

服务器:

客户端1:

客户端2:

其实这种通过IP地址来确定要发送的客户端的实现是有问题的:每个客户端作为主机在连接到因特网上时,电信/联通网会分配动态IP地址,所以这种向服务器端传IP地址从而建立沟通通道的效果行不通。建议是每个客户端主机都能有一个唯一标识,来进行判断。

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

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

相关文章

静态链接与动态链接

目录 静态链接 地址空间分配 静态链接的详细过程 静态链接库 动态链接 位置无关代码 延迟绑定机制 本篇会重点介绍静态链接&#xff0c;动态链接&#xff0c;延迟绑定机制 问&#xff1a;两个或者多个不同的目标文件是如何组成一个可执行文件的呢? 答&#xff1a;这就…

数据结构 - 线段树的运用

数据结构 - 线段树的运用 前言一. 线段树的运用1.1 区间和 - 线段树节点的成员变量1.2 线段树的构建1.3 线段树的区间和查询1.4 线段树的区间和更新1.5 完整代码 二. 线段树的动态扩建2.1 向下递推2.2 向上递推2.3 更新操作2.4 查询操作2.5 完整代码 三. 线段树的使用案例3.1 定…

Unity之NetCode多人网络游戏联机对战教程(3)--NetworkObject组件讲解

文章目录 NetworkObjectAlways Replicate As RootSynchronization TransformActive Scene SynchronizationScene Migration SynchronizationSpawn With ObserversDont Destroy With OwnerAuto Object Parent Sync 后话 NetworkObject 为了复制任何Netcode感知属性或发送/接收R…

Python大数据之pandas快速入门(一)

文章目录 pandas快速入门学习目标1. DataFrame 和 Series 简介2. 加载数据集(csv和tsv)2.1 csv和tsv文件格式简介2.2 加载数据集(tsv和csv) pandas快速入门 学习目标 能够知道 DataFrame 和 Series 数据结构能够加载 csv 和 tsv 数据集能够区分 DataFrame 的行列标签和行列位…

FPGA project : uart232_ram_vga

重点学习&#xff1a; 本实验重点学习了双口ram解决多bit跨时钟域同步处理的问题。 其实signal port ram&#xff0c;它的输入口和输出口分别用不同的时钟&#xff0c;也可以解决这个问题。 让我意识到的比较重要的事情&#xff1a; 1&#xff0c;代码设计中&#xff0c;一…

经典题记录 字符串相加/相乘

1. LeetCode 415 字符串相加 代码一&#xff1a;代码简短&#xff0c;但需要借助额外的一个string来保存结果&#xff0c;更占用内存。 class Solution { public:string addStrings(string num1, string num2) {string ans"";int size1num1.size();int size2num2.si…

Qt_C++读写NFC标签Ntag支持windows国产linux操作系统

本示例使用的发卡器&#xff1a;Android Linux RFID读写器NFC发卡器WEB可编程NDEF文本/智能海报/-淘宝网 (taobao.com) ntag2标签存储结构说明 #include "mainwindow.h" #include "./ui_mainwindow.h" #include <QDebug> #include "QLibrary&…

Django REST Farmowork初探

1.简介 Django REST framework &#xff08;简称&#xff1a;DRF&#xff09;是一个强大而灵活的 Web API 工具。 遵循RESTFullAPI风格&#xff0c;功能完善&#xff0c;可快速开发API平台。 官网文档&#xff1a;https://www.django-rest-framework.org 2. framwork的安装 …

界面组件DevExpress WPF v23.2新功能预览 - 更轻量级的主题

本文主要描述了DevExpress WPF即将在几个月之后发布的v23.2中包含的新功能&#xff0c;持续关注我们获取更多最新资讯哦~ P.S&#xff1a;DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强…

2023蓝帽杯半决赛取证复现

1.检材数据开始提取是今年什么时候&#xff1f;&#xff08;答案格式&#xff1a;04-12 13:26&#xff09; 09-11 17:21 这题做错了 其实当时盘古石手机取证里面就有的&#xff0c;想多了去看了日志文件 是真的有点歧义&#xff0c;20分就开始提取任务了 2.嫌疑人手机SD卡存…

精通git,没用过git cherry-pick?

前言 git cherry-pick是git中非常有用的一个命令&#xff0c;cherry是樱桃的意思&#xff0c;cherry-pick就是挑樱桃&#xff0c;从一堆樱桃中挑选自己喜欢的樱桃&#xff0c;在git中就是多次commit中挑选一个或者几个commit出来&#xff0c;也可以理解为把特定的commit复制到…

【实验记录】AGW | Visible-Infrared Re-ID

【RT】Visible Thermal Re-IDDeep Learning for Person Re-identification: A Survey and Outlook中提出了一个针对单/跨模态行人重识别的baseline&#xff1a;AGW 做过两次&#xff0c;在测试阶段有问题&#xff0c;现在再重做一次&#x1f914;Code RTX3090 修改数据集路…

手机相机系统介绍

目录 一张照片是如何生成的? 相机的成像原理 相机硬件 颜色四要素 相机硬件三大块 模组结构 镜头 镜头光路 镜头常见参数 镜头-FOV&EFL 镜头-焦距 镜头-光圈 图像传感器 图像传感器-像素-底 RGB排布 图像传感器-Pattern & PDAF Sensor CMOS sensor …

计算机类软件方向适合参加的比赛

前言 博主是一名计算机专业的大三学生&#xff0c;在校时候参加了很多比赛和训练营&#xff0c;现在给大家博主参加过的几个的比赛&#xff0c;希望能给大一大二的学生提供一点建议。 正文 最近也有比赛的&#xff0c;我会从时间线上来给大家推荐一些比赛&#xff0c;并且给…

雷柏mv20鼠标使用体验

用了1年多&#xff0c;第一次用竖着的鼠标&#xff0c;现在已经很习惯了&#xff0c;感觉还不错。说说使用感受&#xff1a; 1、 仍然是长时间使用鼠标&#xff0c;但是很少出现手腕痛的情况&#xff0c;确实是有一定效果的。 2、使用场景是有限制的&#xff0c;我是配合笔记…

解决kali beef启动失败问题及实战

文章目录 一、解决方法二、靶场实战应用1.首先打开dvwa这个靶场&#xff0c;设置难度为low2.打开xss-stored3.准备payload4.提交payload5.利用 一、解决方法 首先需卸载 ruby apt remove ruby 卸载 beef apt remove beef-xss 重新安装ruby apt-get install ruby apt-get insta…

安卓修改ROM 修改固件中的一些基本常识 自己做rom注意事项

修改rom 制作rom 解包rom的一些问题解析 安卓系列机型如何内置app 如何选择so文件内置 修改设置里 添加选项 添加文字 修改图标 修改版本号等等 实例解析 最近有几个粉丝对修改rom有兴趣。今天主要给这些友友提供一些自己初学修改rom的一些建议和思路&#xff0c;可以供大家…

uni-app:多方法实现两个view在同一行展示

效果 方法一&#xff1a;flex 布局 使用 display: flex 后&#xff0c;默认的 flex-direction 值就是 row&#xff0c;即水平排列。 <template><view class"container"><view class"left-view">123</view><view class"r…

SpringBoot的excel模板导出

Word的模板导出(参考&#xff1a;https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill) 创建有两个sheet的excel文件模板 将模板文件放入resource\templates/doc下使用 public void exportUavInfoExcel(HttpServletResponse response, CaseExportRPO cas…

GB28181协议-SDP详解

SDP协议 SDP全称是Session Description Protocol&#xff0c;翻译过来就是描述会话的协议。主要用于两个会话实体之间的媒体协商。 SDP描述由许多文本行组成&#xff0c;文本行的格式为<类型><值>&#xff0c;表示为keyvalue; SIP负责建立和释放会话&#xff0c…