基于Linux socket聊天室-多线程服务器模型(01)

​前言

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  
*********************************************/
#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){//客户端出错了 返回值-1close(new_fd);break;   }if(nbytes == 0){//客户端主动断开连接,返回值是0close(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  
*********************************************/
#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 1024void *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 1printf("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一块内存专门存放这个新的套接字呢? 

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

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

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

相关文章

什么是推挽电路?

推挽电路原理&#xff1a; 可以简单理解为推和拉&#xff1b; 此电路总共用到两个元器件&#xff0c;对应图中的Q1----NPN三极管&#xff0c;Q2----PNP三极管&#xff0c;两个电阻R1和R2起到限流的作用&#xff1b;两个三极管的中间对应信号的输出。 下面就举例说明是如何工作的…

基于JAVA,SpringBoot和Vue的前后端分离的求职招聘系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 这个系统的研发背景是…

时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测

时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测 目录 时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 MATLAB实现POA-CNN-BiLSTM鹈鹕算…

使用SPY++查看窗口信息去排查客户端UI软件问题

目录 1、使用SPY查看窗口的信息 2、使用SPY查看某些软件UI窗口用什么UI组件实现的 2.1、查看海康视频监控客户端安装包程序 2.2、查看华为协同办公软件WeLink 2.3、查看字节协同办公软件飞书 2.4、查看最新版本的Chrome浏览器 2.5、查看小鱼易连视频会议客户端软件 2.6…

第十四届蓝桥杯大赛软件赛决赛 C/C++ 大学 B 组 试题 A: 子 2023

[蓝桥杯 2023 国 B] 子 2023 试题 A: 子 2023 【问题描述】 小蓝在黑板上连续写下从 1 1 1 到 2023 2023 2023 之间所有的整数&#xff0c;得到了一个数字序列&#xff1a; S 12345678910111213 ⋯ 20222023 S 12345678910111213\cdots 20222023 S12345678910111213⋯2…

讯飞星火认知大模型Java后端接口

文章目录 1.免费申请星火大模型套餐2.Java后端接口说明2.1 项目地址2.2 项目说明2.3 项目结构2.4 项目代码&#x1f340; maven 依赖&#x1f340; application.yml 配置文件&#x1f340; config 包&#x1f4cc; XfXhConfig &#x1f340; dto 包&#x1f4cc; MsgDTO&#x…

element中使用el-steps 进度条效果demo(整理)

<template><div class"margin-top20"><!-- align-center 不要居中就去掉 --><!-- process-status 这几个参数值&#xff1a;改变颜色 wait / process / finish / error / --><!-- active 到第几个是绿色 --><el-steps :space&qu…

深度解读F5:从企业级负载均衡到云原生应用服务

上世纪九十年代&#xff0c;Internet 的快速发展催生了大量在线网站&#xff0c;Web 访问量迅速提升。在互联网泡沫破灭以前&#xff0c;这个领域基本是围绕如何对 Web 网站进行负载均衡与优化。因而在早期&#xff0c;也会有“Web 交换机”的说法。从1997年 F5 发布了 BIG-IP …

黑马JVM总结(二十五)

&#xff08;1&#xff09;字节码指令-cinit 构造方法可以分为两类&#xff0c;一类是cinit 一类init cinit是整个类的构造方法 putstatic&#xff1a;进行static变量的赋值&#xff0c;是到常量池里找到名字一个叫做i的变量 &#xff08;2&#xff09;字节码指令-init in…

JimuReport积木报表 v1.6.2 版本正式发布—开源免费的低代码报表

项目介绍 一款免费的数据可视化报表&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完成报…

mysql面试题2:说一说MySQL的架构设计?一条 MySQL 语句执行的步骤?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:说一说MySQL的架构设计? MySQL的架构设计主要包括以下几个组件: 连接器(Connector):负责与客户端建立连接,并进行身份验证和授权。 查询缓存…

BFS专题7 多终点迷宫问题

题目&#xff1a; 样例&#xff1a; 输入 3 3 0 0 0 1 0 0 0 1 0 输出 0 1 2 -1 2 3 -1 -1 4 思路&#xff1a; 单纯的 BFS 迷宫问题&#xff0c;只是标记一下每个点的 step&#xff0c;注意初始化答案数组都为 -1. 代码详解如下&#xff1a; #include <iostream> #…

常用接口测试工具

首先&#xff0c;什么是接口呢&#xff1f; 接口一般来说有两种&#xff0c;一种是程序内部的接口&#xff0c;一种是系统对外的接口。 系统对外的接口&#xff1a;比如你要从别的网站或服务器上获取资源或信息&#xff0c;别人肯定不会把数据库共享给你&#xff0c;他只能给你…

五、3d场景的卡片展示的创建

在我们3d的开发中&#xff0c;对某一些建筑和物体进行解释说明是非常常见的现象&#xff0c;那么就不得不说卡片的展示了&#xff0c;卡片展示很友好的说明了当前物体的状态&#xff0c;一目了然&#xff0c;下面就是效果图。 它主要有两个方法来实现&#xff0c;大量的图片建议…

最近很火的AIGC人工智能之AI赋能运营(巧用ChatGPT轻松上手新媒体)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

阿里云服务器技术创新、网络技术和数据中心技术说明

阿里云服务器技术创新、网络技术创新、数据中心技术创新和智能运维&#xff1a;云服务器方升架构、自研硬件、自研存储硬件AliFlash和异构计算加速平台&#xff0c;以及全自研网络系统技术创新和数据中心巴拿马电源、液冷技术等技术创新说明&#xff0c;阿里云百科分享阿里云服…

算法-堆/多路归并-查找和最小的 K 对数字

算法-堆/多路归并-查找和最小的 K 对数字 1 题目概述 1.1 题目出处 https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/description/?envTypestudy-plan-v2&envIdtop-interview-150 1.2 题目描述 2 优先级队列构建大顶堆 2.1 思路 将两个数字的和放入大…

Spring面试题23:Spring支持哪些事务管理类型?Spring框架的事务管理有哪些优点?你更倾向用哪种事务管理类型?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:Spring支持哪些事务管理类型? Spring 支持以下几种事务管理类型: 编程式事务管理:通过在代码中显式地使用事务管理 API(如 TransactionTempla…

英飞凌 Tricore 架构中断系统详解

本文以TC3系列MCU为例&#xff0c;先来了解中断源是如何产生的&#xff0c;再看一下CPU是如何处理中断源的。 AURIX TC3XX的中断路由模块 Interrupt Router (IR) 在TC3中&#xff0c;中断既可以被CPU处理&#xff0c;也可以被DMA处理&#xff0c;所以手册中不再把中断称为中断…

Spring 学习(九)整合 Mybatis

1. 整合 Mybatis 步骤 导入相关 jar 包 <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency>…