在Linux系统实现服务器端和客户端的多线程并发通信

news2024/9/28 15:31:32

 先导知识:

在Linux系统实现服务器端和客户端的套接字通信_小梁今天敲代码了吗的博客-CSDN博客

线程同步(一)_小梁今天敲代码了吗的博客-CSDN博客

线程同步(二)_小梁今天敲代码了吗的博客-CSDN博客

线程同步(三)_小梁今天敲代码了吗的博客-CSDN博客

        如果要编写多进程版的并发服务器程序,首先要考虑,创建出的多个进程都是什么角色,这样就可以在程序中对号入座了。在Tcp服务器端一共有两个角色,分别是:监听和通信,监听是一个持续的动作,如果有新连接就建立连接,如果没有新连接就阻塞。关于通信是需要和多个客户端同时进行的,因此需要多个进程,这样才能达到互不影响的效果。进程也有两大类:父进程和子进程,通过分析我们可以这样分配进程:

  • 父进程:
    • 负责监听,处理客户端的连接请求,也就是在父进程中循环调用accept()函数
    • 创建子进程:建立一个新的连接,就创建一个新的子进程,让这个子进程和对应的客户端通信
    • 回收子进程资源:子进程退出回收其内核PCB资源,防止出现僵尸进程
  • 子进程:负责通信,基于父进程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
    • 发送数据:send() / write()
    • 接收数据:recv() / read()

在多进程版的服务器端程序中,多个进程是有血缘关系,对应有血缘关系的进程来说,还需要想明白他们有哪些资源是可以被继承的,哪些资源是独占的,以及一些其他细节:

  • 子进程是父进程的拷贝,在子进程的内核区PCB中,文件描述符也是可以被拷贝的,因此在父进程可以使用的文件描述符在子进程中也有一份,并且可以使用它们做和父进程一样的事情。

  • 父子进程有用各自的独立的虚拟地址空间,因此所有的资源都是独占的

  • 为了节省系统资源,对于只有在父进程才能用到的资源,可以在子进程中将其释放掉,父进程亦如此。

  • 由于需要在父进程中做accept()操作,并且要释放子进程资源,如果想要更高效一下可以使用信号的方式处理

 服务器端代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//提供IP地址转换函数
#include <pthread.h>

//信息结构体
struct SockInfo
{
    struct sockaddr_in addr;
    int fd;
};
struct SockInfo infos[512];//用来存储客户端信息
//声明子线程任务函数
void* working(void* arg);
int main()
{
    //1.创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");//perror(s) 用来将上一个函数发生错误的原因输出到标准设备,参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
        return -1;
    }

    //2.绑定本地的IP port
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;//地址族协议
    saddr.sin_port = htons(9999);//端口必须转换成大端
    saddr.sin_addr.s_addr = INADDR_ANY;//指定IP地址(INADDR_ANY泛指本机的意思,也就是表示本机的所有IP)
    int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if (ret == -1)//绑定失败 返回-1
    {
        perror("bind");
        return -1;
    }
    //3设置监听
    ret = listen(fd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }
    //初始化结构体数组
    int max = sizeof(infos) / sizeof(infos[0]);
    for (int i = 0; i < max; ++i)
    {
        bzero(&infos[i], sizeof(infos[i]));
        infos[i].fd = -1;
    }
    //4阻塞并等待客户端的连接
    int addrlen = sizeof(struct sockaddr_in);
    while (1)
    {
        struct SockInfo* pinfo;
        for (int i = 0; i < max; ++i)
        {
            if (infos[i].fd == -1)
            {
                pinfo = &infos[i];
                break;
            }
        }
        int cfd = accept(fd, (struct sockaddr*)&pinfo->addr, &addrlen);
        pinfo->fd = cfd;
        if (cfd == -1)
        {
            perror("accept");
            break;
        }
        //创建子线程
        pthread_t tid;
        pthread_create(&tid, NULL, working, pinfo);
        pthread_detach(tid);
    }
    close(fd);
    return 0;

}

void* working(void* arg)
{
    struct SockInfo* pinfo = (struct SockInfo*)arg;//将传进来的arg进行类型转换
    //连接建立成功,打印客户端的IP和端口信息
    char ip[32];
    printf("客户端的ip:%s,端口:%d\n",
        inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(pinfo->addr.sin_port));//网络字节序转换为主机字节序

    //5.通信
    while (1)
    {
        //接收数据
        char buff[1024];
        int len = recv(pinfo->fd, buff, sizeof(buff), 0);
        if (len > 0)
        {
            printf("client say: %s\n", buff);
            send(pinfo->fd, buff, len, 0);
        }
        else if (len == 0)
        {
            printf("客户端已经断开了连接...\n");
            break;
        }
        else
        {
            perror("recv");
            break;
        }
    }
    //关闭文件描述符
    close(pinfo->fd); 
    pinfo->fd = -1;
    return NULL;
}

客户端代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//提供IP地址转换函数
 
int main()
{
    //1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");//perror(s) 用来将上一个函数发生错误的原因输出到标准设备,参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
        return -1;
    }
 
    //2.连接服务器IP port
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;//地址族协议
    saddr.sin_port = htons(9999);//端口必须转换成大端
    inet_pton(AF_INET, "192.168.122.1 ", &saddr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if (ret == -1)//连接失败 返回-1
    {
        perror("connect");
        return -1;
    }
    
    int number = 0;
    //3.通信
    while (1)
    {
        //发送数据
        char buff[1024];
        sprintf(buff, "你好,hello ,world ,%d...\n", number++);
        send(fd, buff, strlen(buff) + 1, 0);
        //接收数据
        memset(buff, 0, sizeof(buff));
        int len = recv(fd, buff, sizeof(buff), 0);
        if (len > 0)
        {
            printf("server say: %\n", buff);
 
        }
        else if (len == 0)
        {
            printf("服务器端已经断开了连接...\n");
            break;
        }
        else
        {
            perror("recv");
            break;
        }
        sleep(1);
    }
    //关闭文件描述符
    close(fd);
    return 0;
}

运行结果:

 从图中可以看到server服务器端可以收到两个client客户端的通信信息,即多线程并发通信成功

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

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

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

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

相关文章

通付盾入围《2023年度中国数字安全能力图谱(行业版)》

近日&#xff0c;数世咨询发布《2023年度中国数字安全能力图谱&#xff08;行业版&#xff09;》。通付盾作为以分布式数字身份和大数据决策智能技术为核心的数字化高端软件与服务提供商&#xff0c;凭借在数字安全领域的实力和影响力&#xff0c;入选政府、互联网两大行业细分…

实战react+ts+antd遇见的问题之自定义树形结构

目录 自定义编辑树搜索树形结构搜索算法原理 实时更改数据界面不随之发生变化 自定义编辑树 需求要求在每个节点的后面加上新增&#xff0c;编辑&#xff0c;删除按钮&#xff0c;并且能够点击编辑title的显示变成input输入框&#xff0c;antd的案例中没有这种情况&#xff0c…

逍遥自在学C语言 | 指针函数与函数指针

前言 在C语言中&#xff0c;指针函数和函数指针是强大且常用的工具。它们允许我们以更灵活的方式处理函数和数据&#xff0c;进而扩展程序的功能。 本文将介绍指针函数和函数指针的概念&#xff0c;并讲解一些常见的应用示例。 一、人物简介 第一位闪亮登场&#xff0c;有请…

金士顿U盘无法识别的修复软件,方便好用

一、PD V1.16 先打开“PDx16.exe”这个软件&#xff0c;插入U盘。就会在“DEVICE 1”那里检测到U盘&#xff08;如果没有&#xff0c;就用另外的软件&#xff09;。然后按“全部开始”。当完成好&#xff0c;再重新插入U盘。 二、2090&2090E_V1.6.9_普通版070628 1、插入…

工业机器人运动学与Matlab正逆解算法学习笔记(用心总结一文全会)(二)

文章目录 机器人逆运动学※ 代数解、几何解&#xff0c;解析解&#xff08;封闭解&#xff09;、数值解的含义与联系○ 代数解求 θ 1 \theta_1 θ1​、 θ 2 \theta_2 θ2​、 θ 3 \theta_3 θ3​※参考资料 求解 θ 1 \theta_1 θ1​ 求解 θ 3 \theta_3 θ3​ 求解 θ 2 \t…

JUC高级-0620

8. CAS 原子类&#xff1a;Atomic没有CAS之前&#xff1a;多线程环境不使用原子类保证线程安全i&#xff08;基本数据类型&#xff09;&#xff0c;可以使用synchronized&#xff0c;但是很重有CAS之后&#xff1a; 使用AtomicInteger.getAndIncrement这样的API&#xff0c;保…

ARM的半主机模式(Semihosting)

本文介绍ARM的半主机模式&#xff0c;并介绍在MCU进行调试时其他的调试方法和手段。 1.ARM半主机模式(Semihosting) ARM Semihosting是ARM平台的一个独特功能&#xff0c;它允许使用主机上的输入和输出函数&#xff0c;通过硬件调试器转发到微控制器&#xff0c;通过挂接到I/…

网络解析----faster rcnn

Faster R-CNN&#xff08;Region-based Convolutional Neural Network&#xff09;是一种基于区域的卷积神经网络用于目标检测任务的模型。它是一种两阶段的目标检测方法&#xff0c;主要包含以下几个步骤&#xff1a; Region Proposal Network&#xff08;RPN&#xff09;: F…

c++ vector的扩容机制

1、当向vector push_back一个元素时&#xff0c;如果此时元素个数超过了vector的容量&#xff0c;会触发扩容 2、扩容的过程是&#xff1a;开辟新空间->拷贝旧空间的元素->释放旧空间 3、扩容过程中开辟新空间的大小影响着往vector插入元素的效率&#xff1a; 如果新空…

软件系统三基座之三:用户管理

软件系统三基座包含&#xff1a;权限管理、组织架构、用户管理。 基于权限控制、组织搭建&#xff0c;用户可以批量入场。 一、用户管理 在系统构建中&#xff0c;权限控制、组织搭建&#xff0c;对于普通用户都是不可见的。 权限控制&#xff0c;在系统搭建时&#xff0c;就会…

电商网站Web自动化测试实战( 编写京东搜索脚本python+selenium框架)

电商网站Web自动化测试实战&#xff08; 编写京东搜索脚本&#xff09; 1&#xff0c;打开京东页 京东首页地址&#xff1a;京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物&#xff01;&#xff0c;故进入京东首页如下&#xff1a; 2&#xff0c;打开浏览器开发者模式…

Doris-简介、架构、编译、安装和数据表的基本使用

目录 1、Doris简介2、Doris网址3、Doris架构3、编译和安装 3.1、软硬件需求3.2、编译 3.2.1、安装Docker环境3.2.2、使用Docker 开发镜像编译3.3、集群部署 3.3.1、创建目录并拷贝编译后的文件3.3.2、部署 FE 节点3.3.3、配置 BE 节点3.3.4、在 FE 中添加所有 BE 节点3.3.5、启…

最小生成树的拓展应用

1.新的开始 信息学奥赛一本通&#xff08;C版&#xff09;在线评测系统 (ssoier.cn)http://ybt.ssoier.cn:8088/problem_show.php?pid1488 假如自己建个发电站相当于从一个虚拟原点向他有条边&#xff0c;然后做跑一遍最小生成树即可 #include<bits/stdc.h> using nam…

【云原生】Docker部署/容器加速器(最新版)

目录 初时Docker和部署 1.什么是Docker 2.容器和虚拟化的区别 3.部署Docker 1.卸载历史版本 2.设置存储库 3.安装Docker最新引擎 4.安装Docker特定安装引擎 1.先查看当前docker-ce都有那些版本 2.替换为所需版本&#xff0c;然后运行以下命令 要安装的命令&#xff1a; 5.启动D…

Python入门教程:掌握for循环、while循环、字符串操作、文件读写与异常处理等基础知识

文章目录 for循环while循环字符串操作访问字符串中的字符切片总结字符串拼接 文件读写try...except 异常处理函数模块和包类和面向对象编程完结 for循环 在 Python 中&#xff0c;for 循环用于遍历序列&#xff08;list、tuple、range 对象等&#xff09;或其他可迭代对象。for…

AI建模可以智能到什么程度?

2023年年初&#xff0c;我们被AIGC&#xff08;人工智能生产内容&#xff09;撞了个满怀&#xff0c;从AI绘画、AI写作、AI配音&#xff0c;到AI建模&#xff0c;似乎每个行业的内容创作者都被AI“击中了膝盖”。AI技术发展迅速&#xff0c;前段时间&#xff0c;国内外各大公司…

使用esp32+micropython+microdot搭建web(http+websocket)服务器(超详细)第三部分

使用esp32micropythonmicrodot搭建web(httpwebsocket)服务器&#xff08;超详细&#xff09;第三部分 microdot文档速查 什么是Microdot?Microdot是一个可以在micropython中搭建物联网web服务器的框架micropyton文档api速查 Quick reference for the ESP32 实现websocket服务…

优雅组合,高效交互:Gradio Combining Interfaces模块解析

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

Jdk9版本以上如何查看java对象所占内存大小

想要查看java对象在运行时的实际占用内存大小。网上大部分方法都是雷同&#xff0c;都是出自 查看java对象所占内存大小-云社区-华为云 这里面的提供的4种方法仅仅适合jdk8及以下版本。 如果项目使用的是dk11、jdk18等高级版本就无法使用&#xff0c;上面帖子中第一种和第二…

蓝奥声核心技术—— 用电异常监控技术

1.技术背景 用电异常监控技术主要通过电能监测节点作为目标监测节点对其关联绑定的用电负载对象的异常状态进行快速响应与准确监控&#xff0c;以解决用电监控的安全性问题。该项技术涉及无线物联网边缘智能与测控的技术领域&#xff0c;主要涉及面向电能监测及安全监控的边缘…