从0实现基于Linux socket聊天室-多线程服务器模型-1
前言
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为例,总结了几种常见的网络服务器模型的实现方式,并最终实现一个简单的命令行聊天室。
单进程循环
单线进程循环原理就是主进程没和客户端通信,客户端都要先连接服务器,服务器接受一个客户端连接后从客户端读取数据,然后处理并将处理的结果返还给客户端,然后再接受下一个客户端的连接请求。
优点
单线程循环模型优点是简单、易于实现,没有同步、加锁这些麻烦事,也没有这些开销。
缺点
阻塞模型,网络请求串行处理; 没有利用多核cpu的优势,网络请求串行处理; 无法支持同时多个客户端连接; 程序串行操作,服务器无法实现同时收发数据。
单线程IO复用
linux高并发服务器中常用epoll作为IO复用机制。线程将需要处理的socket读写事件都注册到epoll中,当有网络IO发生时,epoll_wait返回,线程检查并处理到来socket上的请求。
优点
实现简单, 减少锁开销,减少线程切换开销。
缺点
只能使用单核cpu,handle时间过长会导致整个服务挂死; 当有客户端数量超过一定数量后,性能会显著下降; 只适用高IO、低计算,handle处理时间短的场景。
多线程/多进程
多线程、多进程模型主要特点是每个网络请求由一个进程/线程处理,线程内部使用阻塞式系统调用,在线程的职能划分上,可以由一个单独的线程处理accept连接,其余线程处理具体的网络请求(收包,处理,发包);还可以多个进程单独listen、accept网络连接。
优点:
1、实现相对简单;
2、利用到CPU多核资源。
缺点:
1、线程内部还是阻塞的,举个极端的例子,如果一个线程在handle的业务逻辑中sleep了,这个线程也就挂住了。
多线程/多进程IO复用
多线程、多进程IO服用模型,每个子进程都监听服务,并且都使用epoll机制来处理进程的网络请求,子进程 accept() 后将创建已连接描述符,然后通过已连接描述符来与客户端通信。该机制适用于高并发的场景。
优点:
支撑较高并发。
缺点:
异步编程不直观、容易出错
多线程划分IO角色
多线程划分IO角色主要功能有:一个accept thread处理新连接建立;一个IO thread pool处理网络IO;一个handle thread pool处理业务逻辑。使用场景如:电销应用,thrift TThreadedSelectorServer。
优点:
按不同功能划分线程,各线程处理固定功能,效率更高 可以根据业务特点配置线程数量来性能调优
缺点:
线程间通信需要引入锁开销 逻辑较复杂,实现难度大
小结
上面介绍了常见的网络服务器模型,还有AIO、协程,甚至还有其他的变型,在这里不再讨论。重要的是理解每种场景中所面临的问题和每种模型的特点,设计出符合应用场景的方案才是好方案。
多线程并发服务器模型
下面我们主要讨论多线程并发服务器模型。
代码结构
并发服务器代码结构如下:
thread_func()
{
while(1) {
recv(...);
process(...);
send(...);
}
close(...);
}
main(
socket(...);
bind(...);
listen(...);
while(1) {
accept(...);
pthread_create();
}
}
由上可以看出,服务器分为两部分:主线程、子线程。
主线程
main函数即主线程,它的主要任务如下:
socket()创建监听套字; bind()绑定端口号和地址; listen()开启监听; accept()等待客户端的连接, 当有客户端连接时,accept()会创建一个新的套接字new_fd; 主线程会创建子线程,并将new_fd传递给子线程。
子线程
子线程函数为thread_func(),他通过new_fd处理和客户端所有的通信任务。
客户端连接服务器详细步骤
下面我们分步骤来看客户端连接服务器的分步说明。
1. 客户端连接服务器
服务器建立起监听套接字listen_fd,并初始化; 客户端创建套接字fd1; 客户端client1通过套接字fd1连接服务器的listen_fd;
2. 主线程创建子线程thread1
server收到client1的连接请求后,accpet函数会返回一个新的套接字newfd1; 后面server与client1的通信就依赖newfd1,监听套接字listen_fd会继续监听其他客户端的连接; 主线程通过pthead_create()创建一个子线程thread1,并把newfd1传递给thread1; server与client1的通信就分别依赖newfd1、fd1。 client1为了能够实时收到server发送的信息,同时还要能够从键盘上读取数据,这两个操作都是阻塞的,没有数据的时候进程会休眠,所以必须创建子线程read_thread; client1的主线负责从键盘上读取数据并发送给,子线程read_thread负责从server接受信息。
3. client2连接服务器
客户端client2创建套接字fd2; 通过connect函数连接server的listen_fd;
4. 主线程创建子线程thread2
server收到client2的连接请求后,accpet函数会返回一个新的套接字newfd2; 后面server与client2的通信就依赖newfd2,监听套接字listen_fd会继续监听其他客户端的连接; 主线程通过pthead_create()创建一个子线程thread2,并把newfd2传递给thread2; server与client1的通信就分别依赖newfd2、fd2。 同样client2为了能够实时收到server发送的信息,同时还要能够从键盘上读取数据必须创建子线程read_thread; client1的主线负责从键盘上读取数据并发送给,子线程read_thread负责从server接受信息。
由上图可见,每一个客户端连接server后,server都要创建一个专门的thread负责和该客户端的通信;每一个客户端和server都有一对固定的fd组合用于连接。
实例
好了,理论讲完了,根据一口君的惯例,也继承祖师爷的教诲:talk is cheap,show you my code.不上代码,只写理论的文章都是在耍流氓。
本例的主要功能描述如下:
实现多个客户端可以同时连接服务器; 客户端可以实现独立的收发数据; 客户端发送数据给服务器后,服务器会将数据原封不动返回给客户端。
服务器端
/*********************************************
服务器程序 TCPServer.c
公众号:一口Linux
*********************************************/
#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)
{//客户端出错了 返回值-1
close(new_fd);
break;
}
if(nbytes == 0)
{//客户端主动断开连接,返回值是0
close(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
公众号:一口Linux
*********************************************/
#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 1024
void *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 1
printf("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库,编译命令如下:
gcc s.c -o s -lpthread gcc cli.c -o c -lpthread
先本机测试开启一个终端 ./s 8888 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"qqqqqqq" 再开一个终端 ./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一块内存专门存放这个新的套接字呢?
这个是一个很隐蔽,很多新手都容易犯的错误。下一章,我会专门给大家讲解。
本系列文章预计会更新4-5篇。最终目的是写出一个带登录注册公聊私聊等功能的聊天室。喜欢的话请收藏关注。
图片参考网络文章:https://cloud.tencent.com/developer/article/1376352
获取更多关于Linux的资料,请关注公众号「一口Linux」,回复"进群",带你加入大咖云集的技术讨论群。
从0实现基于Linux socket聊天室-多线程服务器模型-1的更多相关文章
- 基于WebSocket实现聊天室(Node)
基于WebSocket实现聊天室(Node) WebSocket是基于TCP的长连接通信协议,服务端可以主动向前端传递数据,相比比AJAX轮询服务器,WebSocket采用监听的方式,减轻了服务器压力 ...
- Ext JS学习第十六天 事件机制event(一) DotNet进阶系列(持续更新) 第一节:.Net版基于WebSocket的聊天室样例 第十五节:深入理解async和await的作用及各种适用场景和用法 第十五节:深入理解async和await的作用及各种适用场景和用法 前端自动化准备和详细配置(NVM、NPM/CNPM、NodeJs、NRM、WebPack、Gulp/Grunt、G
code&monkey Ext JS学习第十六天 事件机制event(一) 此文用来记录学习笔记: 休息了好几天,从今天开始继续保持更新,鞭策自己学习 今天我们来说一说什么是事件,对于事件 ...
- ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(十二) 代码重构使用反射工厂解耦(一)缓存切换
前言 上一篇中,我们用了反射工厂来解除BLL和UI层耦合的问题.当然那是最简单的解决方法,再复杂一点的程序可能思路相同,但是在编程细节中需要考虑的就更多了,比如今天我在重构过程中遇到的问题.也是接下来 ...
- ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(四) 之 用户搜索(Elasticsearch),加好友流程(1)。
前面几篇基本已经实现了大部分即时通讯功能:聊天,群聊,发送文件,图片,消息.不过这些业务都是比较粗犷的.下面我们就把业务细化,之前用的是死数据,那我们就从加好友开始吧.加好友,首先你得知道你要加谁.L ...
- ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(七) 之 历史记录查询(时间,关键字,图片,文件),关键字高亮显示。
前言 上一篇讲解了如何自定义右键菜单,都是前端的内容,本篇内容就一个:查询.聊天历史纪录查询,在之前介绍查找好友的那篇博客里已经提到过 Elasticsearch,今天它又要上场了.对于Elastic ...
- Java Socket聊天室编程(一)之利用socket实现聊天之消息推送
这篇文章主要介绍了Java Socket聊天室编程(一)之利用socket实现聊天之消息推送的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下 网上已经有很多利用socket实现聊天的例子了 ...
- python socket 聊天室
socket 发送的时候,使用的是全双工的形式,不是半双工的形式.全双工就是类似于电话,可以一直通信.并且,在发送后,如果又接受数据,那么在这个接受到数据之前,整个过程是不会停止的.会进行堵塞,堵塞就 ...
- TCP/IP以及Socket聊天室带类库源码分享
TCP/IP以及Socket聊天室带类库源码分享 最近遇到个设备,需要去和客户的软件做一个网络通信交互,一般的我们的上位机都是作为客户端来和设备通信的,这次要作为服务端来监听客户端,在这个背景下,我查 ...
- ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列
ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言 http://www.cnblogs.com/panzi/p/5742089.html ASP.NET S ...
- ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言
前端时间听一个技术朋友说 LayIM 2.0 发布了,听到这个消息抓紧去官网看了一下.(http://layim.layui.com/)哎呀呀,还要购买授权[大家支持一下哦],果断买了企业版,喜欢钻研 ...
随机推荐
- 在 VSCode 中编写 Markdown 的进阶指南
最新版的 Visual Studio Code 对 Markdown 的支持已显著提升,其在预览方面的体验甚至可以与 Markdown Preview Enhanced 插件相比.本文将介绍一些优化方 ...
- 韦东山freeRTOS系列教程之【第八章】事件组(event group)
目录 系列教程总目录 概述 8.1 事件组概念与操作 8.1.1 事件组的概念 8.1.2 事件组的操作 8.2 事件组函数 8.2.1 创建 8.2.2 删除 8.2.3 设置事件 8.2.4 等待 ...
- windows中运行nicegui官网内容
内容受到bilibili视频nicegui官网访问不了?教你本地部署文档,我写python终于有界面启发,其余部分由chatgpt协助生成. 1. 在本地新建目录如 nicegui_web 2. 从g ...
- yb课堂 前端项目通用底部选项卡 CommonsFooter 《三十六》
学会看cube-UI文档,并掌握cube-tab-bar开发 前端需求分析 底部导航 首页Banner 首页视频列表 视频详情模块 注册模块 登陆模块 个人信息模块 下单模块 订单列表模块 文档地址: ...
- Vite5+Electron聊天室|electron31跨平台仿微信EXE客户端|vue3聊天程序
基于electron31+vite5+pinia2跨端仿微信Exe聊天应用ViteElectronChat. electron31-vite5-chat原创研发vite5+electron31+pin ...
- Maven Helper插件——实现一键Maven依赖冲突问题
业余在一个SpringBoot项目集成Swagger2时,启动过程一直出现以下报错信息-- An attempt was made to call a method that does not exi ...
- Apache Hudi X Apache Kyuubi,中国移动云湖仓一体的探索与实践
分享嘉宾:孙方彬 中国移动云能力中心 软件开发工程师 编辑整理:Hoh Xil 出品平台:DataFunTalk 导读:在云原生 + 大数据的时代,随着业务数据量的爆炸式增长以及对高时效性的要求,云原 ...
- webpack4.15.1 学习笔记(七) — 懒加载(Lazy Loading)
懒加载或者按需加载,是一种很好的优化网页或应用的方式.实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或引用另外一些新的代码块.这样加快了应用的初始加载速度,减轻了它 ...
- 网络基础 Modbus协议学习总结
协议简介 Modbus协议,首先从字面理解它包括Mod和Bus两部分,首先它是一种bus,即总线协议,总线就意味着有主机,有从机,这些设备在同一条总线上. Modbus支持单主机,多个从机,最多支持2 ...
- odoo 为form表单视图添加chatter功能
实践环境 Odoo 14.0-20221212 (Community Edition) 需求描述 如图,给表单新增一个类似聊天的窗口,当记录一些表单活动(本例为自动记录当前记录状态变化) 需求实现 模 ...