Linux socket编程(3):利用fork实现服务端与多个客户端建立连接

上一节,我们实现了一个客户端/服务端的Socket通信的代码,在这个例子中,客户端连接上服务端后发送一个字符串,而服务端接收到字符串并打印出来后就关闭所有套接字并退出了。

上一节的代码较为简单,在实际的应用中,客户端和服务端需要像一个聊天室一样能够收发信息,但这样就引出了一些问题:

1、服务端程序需要既能accept新的客户端请求,又能实时获与已经建立连接的客户端发来的消息

2、客户端程序需要既能从stdin获取用户输入,又能实时获取从服务端发来的消息

下面就来解决这两个问题。

文章目录

  • 1 代码改进
    • 1.1 fork函数
    • 1.2 服务端代码
    • 1.3 客户端代码
    • 1.4 实验结果
  • 2 代码优化:信号机制之kill和signal函数
    • 2.1 kill函数
    • 2.2 signal函数
    • 2.3 客户端代码改进
    • 2.4 实验结果
  • 3 完整代码
    • 3.1 服务端
    • 3.2 客户端

1 代码改进

以服务端的问题为例,在accept了一个客户端之后,就在循环中无限检测这个套接字是否有新的数据到来。此时如果又有一个客户端想要进行连接,由于服务端没有在accept,那么就无法建立连接。在这里介绍其中一种解决方法:fork函数。

1.1 fork函数

fork函数是Linux系统中用于创建新进程的系统调用。它在调用进程内部创建一个与父进程几乎完全相同的子进程。这个子进程是父进程的副本,包括代码段、数据段、堆栈等,但具有不同的进程ID(PID)。

pid_t fork(void);

返回值fork函数在父进程中返回两次,在父进程中返回的值为子进程的PID,而在子进程中返回的值为0。若系统资源不足,则返回-1。

例子:创建一个父进程,然后使用fork函数创建了一个子进程,分别输出它们的PID。父进程和子进程可以并发执行,并且各自具有不同的PID。

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t child_pid = fork();

    if (child_pid == -1) {
        perror("fork");
        return 1;
    }

    if (child_pid == 0) {
        // This is the child process
        printf("Child process, PID: %d\n", getpid());
    } else {
        // This is the parent process
        printf("Parent process, PID: %d, Child PID: %d\n", getpid(), child_pid);
    }

    return 0;
}

运行结果:

在这里插入图片描述

可以看到if的两个分支并发执行,并输出PID。

1.2 服务端代码

现在就将accept放入while循环中,来监听多个客户端的请求,对于不同客户端fork一个子进程对数据进行处理,更改代码如下:

在这里插入图片描述

其中socket_read函数如下:

在这里插入图片描述

  • 这里服务端在收到客户端消息后,固定回复server gets message successfully

这里有几个点需要注意:

(1)在父进程中关闭子进程的socket,在子进程中关闭父进程的socket?

关闭套接字只会影响当前进程的套接字,不会直接影响其他进程的套接字,包括父进程的套接字。这是因为每个进程都有其独立的文件描述符表。在子进程中关闭套接字后,父进程的套接字仍然保持打开状态,不受影响。

(2)recv()函数

  • 返回0时,一般表示客户端断开了连接
  • 该函数不会在结尾添加\0,需要自行添加

1.3 客户端代码

客户端现在需要实现一遍通过stdin输入数据并发给服务端,一边接收服务端的数据。同样地,这也可以使用fork来解决:

在这里插入图片描述

这里在子进程中接收服务端的消息,在父进程中接收标准输入的数据并发送给服务端。

1.4 实验结果

在这里插入图片描述

2 代码优化:信号机制之kill和signal函数

在前面的代码中,send最好和recv一样判断返回值,在返回0的时候表示对端断开了连接,此时要关闭套接字。

现在以客户端为例再来理一下这里面的逻辑,在客户端中,子进程用来接收服务端的消息,父进程用来接收标准输入。如果服务端断开了连接,客户端的子进程的recv将返回0,从而退出;但是从逻辑上来看,服务端都退出了,此时父进程监听stdin也没有什么意义了。

**所以我们需要在客户端中实现:在子进程退出的同时,主动关闭父进程。**我们可以利用Linux中的信号机制来实现这个功能,这里简单地介绍一下killsignal函数的使用:

2.1 kill函数

kill是一个用于发送信号给指定进程的系统调用。它的原型如下:

int kill(pid_t pid, int sig);
  • pid参数指定目标进程的PID。可以使用不同的PID值来选择不同的目标:正整数表示具体的进程,0表示向与调用进程属于同一个进程组的所有进程发送信号,-1表示向所有具有权限的进程发送信号,-2表示向与调用进程属于同一个会话的所有进程发送信号。
  • sig参数是要发送的信号的编号
    • 通常我们可以使用常见的信号宏(如SIGTERMSIGKILLSIGINT等)作为参数,这些特殊的信号宏的回调函数由内核实现。比如进程接收到SIGKILL信号时,它会被立即终止。
    • 我们也可以使用一些自定义的宏(如SIGUSR1),然后自定义回调函数

2.2 signal函数

signal 函数用于注册信号处理函数,即在收到特定信号时执行的用户定义的函数。它的原型如下:

void (*signal(int signum, void (*handler)(int)))(int);
  • signum 参数是要处理的信号的编号。
  • handler 参数是一个函数指针,指向信号处理函数。通常,你可以使用一个函数来处理特定信号,或者使用 SIG_IGN表示忽略信号,使用SIG_DFL表示使用默认的信号处理方式。

2.3 客户端代码改进

我们需要实现在子进程退出的同时,主动关闭父进程:

1、在父进程代码开始处注册信号处理函数

在这里插入图片描述

其中handler函数如下:

在这里插入图片描述

也就是说,在父进程收到SIGUSR1信号的时候,会调用exit退出。

2、在子进程退出时调用kill函数向父进程发送SIGUSR1信号

在这里插入图片描述

  • 这里的getppid()用于获取父进程的pid

当然这里只是举一个例子,实际上可以不用在父进程中注册信号和回调函数,直接使用内核提供的SIGKILL信号,在子进程最后调用kill(getppid(), SIGUSR1),效果也是一样的。

2.4 实验结果

在这里插入图片描述

3 完整代码

3.1 服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

void socket_read(int s, struct sockaddr_in *addr)
{
    char buffer[1024];
    while (1)
    {
        ssize_t bytes_received = recv(s, buffer, sizeof(buffer), 0);
        if (bytes_received == -1) {
            perror("Receive failed");
        } else if(bytes_received == 0)
		{
			printf("peer closed\n");
			break;
		} else {
			buffer[bytes_received] = '\0';
            printf("recv from %s:%d = %s\n",inet_ntoa(addr->sin_addr), ntohs(addr->sin_port), buffer);
			send(s, "server gets message successfully\n", strlen("server gets message successfully\n"), 0);
        }
    }
}

int main() {
    // 创建套接字
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(1);
    }
    int reuse = 1;
    setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    // 绑定套接字到IP地址和端口
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8080);
    server_address.sin_addr.s_addr = INADDR_ANY;
    if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("Bind failed");
        close(server_socket);
        exit(1);
    }

    // 设置服务器套接字为监听状态
    if (listen(server_socket, 5) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(1);
    }

    printf("Server listening on port 8080...\n");

    // 接受客户端连接
    struct sockaddr_in client_address;
    socklen_t client_len = sizeof(client_address);
	
	pid_t child_pid;
    while(1)
    {
		int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_len);
		if (client_socket == -1) {
		    perror("Accept failed");
		    close(server_socket);
		    exit(1);
		}
		// 打印客户端的IP和端口号
		printf("accept new:ip=%s port=%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
		
		// 创建子进程
		child_pid = fork();
		if (child_pid == -1) {
		    perror("fork");
		    return 1;
		}
		
		if (child_pid == 0) {
			// 在子进程中可以关闭掉父进程的套接字
		    close(server_socket);
			// 数据接收函数,在while中循环读socket是否有数据并输出
			socket_read(client_socket, &client_address);
			// 如何该子进程对应的客户端退出了,该函数返回,应该退出子进程,防止子进程accept
			exit(1);
		} else {
			// 在父进程中可以关闭掉子进程的套接字,继续下一次while循环执行accept
			close(client_socket);
		}
    }

    return 0;
}

3.2 客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(1);
}

int main() {
    // 创建套接字
    int client_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (client_socket == -1) {
        perror("Socket creation failed");
        exit(1);
    }

    // 设置服务器地址和端口
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(8080);
    server_address.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 连接到服务器
    if (connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
        perror("Connection failed");
        close(client_socket);
        exit(1);
    }
	// 创建子进程
	pid_t child_pid;
	child_pid = fork();
	if (child_pid == -1) {
	    perror("fork");
	    return 1;
	}
	if (child_pid == 0) {// 子进程用来接收服务端发来的消息
		char recv_buf[1024];
		while(1)
		{
			ssize_t bytes_received = recv(client_socket, recv_buf, sizeof(recv_buf), 0);
			if (bytes_received == -1) {
		        perror("Receive failed");
		    } else if(bytes_received == 0)
			{
				printf("peer closed\n");
				break;
			} else
			{
				recv_buf[bytes_received] = '\0';
				printf("recv %ld bytes from server:%s\n", bytes_received, recv_buf);
			}
		}
		close(client_socket);
		kill(getppid(), SIGUSR1);
	} else {// 父进程用来从stdin中接收数据
		signal(SIGUSR1, handler);
		char stdin_buf[1024];
		while(fgets(stdin_buf, sizeof(stdin_buf), stdin) != NULL)
		{
			send(client_socket, stdin_buf, strlen(stdin_buf), 0);
		}
		close(client_socket);
	}
    return 0;
}

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

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

相关文章

识别伪装IP的网络攻击方法

识别伪装IP的网络攻击可以通过以下几种方法&#xff1a; 观察IP地址的异常现象。攻击者在使用伪装IP地址进行攻击时&#xff0c;往往会存在一些异常现象&#xff0c;如突然出现的未知IP地址、异常的流量等。这些现象可能是攻击的痕迹&#xff0c;需要对此加以留意。 检查网络通…

详解 KEIL C51 软件的使用·设置工程·编绎与连接程序

详解 KEIL C51 软件的使用建立工程-CSDN博客 2. 设置工程 (1)在图 2-15 的画面中点击 会弹出如图 2-16 的对话框.其中有 10 个选择页.选择“Target” 项,也就是图 2-16 的画面. 图 2-16 在图 2-16 中,箭头所指的是晶振的频率值,默认是所选单片机最高的可用频率值.该设置值与单…

大数据Doris(二十二):数据查看导入

文章目录 数据查看导入 数据查看导入 Broker load 导入方式由于是异步的,所以用户必须将创建导入的 Label 记录,并且在查看导入命令中使用 Label 来查看导入结果。查看导入命令在所有导入方式中是通用的,具体语法可执行 HELP SHOW LOAD 查看。 show load order by create…

IEEE--DSConv: Efficient Convolution Operator 论文翻译

论文地址:https://arxiv.org/pdf/1901.01928v1.pdf 目录 摘要 1 介绍 2 相关工作 3 DSConv层 4 量化过程 5 分布偏移 6 优化推断 7 训练 8 结果 8.1 ImageNet 8.2 内存和计算负载 8.3 转移性 9 结论 摘要 我们引入了一种卷积层的变体&#xff0c;称为DSConv&…

【LLM】0x00 大模型简介

0x00 大模型简介 个人问题学习笔记大模型简介LLM 的能力&#xff1a;LLM 的特点&#xff1a; LangChain 简介LangChain 核心组件 小结参考资料 个人问题 1、大模型是什么&#xff1f; 2、ChatGPT 在大模型里是什么&#xff1f; 3、大模型怎么用&#xff1f; 带着问题去学习&a…

【分布式】CAP理论详解

一、CAP理论概述 在分布式系统中&#xff0c;CAP是指一组原则&#xff0c;它们描述了在网络分区&#xff08;Partition&#xff09;时&#xff0c;分布式系统能够提供的保证。CAP代表Consistency&#xff08;一致性&#xff09;、Availability&#xff08;可用性&#xff09;和…

【Java 进阶篇】JQuery 案例:全选全不选,为选择添彩

在前端的舞台上&#xff0c;用户交互是一场精彩的表演&#xff0c;而全选全不选的功能则是其中一段引人入胜的剧情。通过巧妙运用 JQuery&#xff0c;我们可以为用户提供便捷的全选和全不选操作&#xff0c;让页面更富交互性。本篇博客将深入探讨 JQuery 中全选全不选的实现原理…

<MySQL> 查询数据进阶操作 -- 聚合查询

目录 一、聚合查询概述 二、聚合函数查询 2.1 常用函数 2.2 使用函数演示 2.3 聚合函数参数为*或列名的查询区别 2.4 字符串不能参与数学运算 2.5 具有误导性的结果集 三、分组查询 group by 四、分组后条件表达式查询 五、MySQL 中各个关键字的执行顺序 一、聚合查询…

232.用栈实现队列(LeetCode)

思路 思路&#xff1a;利用两个栈实现队列先进先出的特性&#xff0c;先将元素导入一个栈内 模拟出队时&#xff0c;则将所有元素导入另一个栈内&#xff0c;此时元素顺序被反转过来&#xff0c;只需要取栈顶数据即可 那我们就可以将两个栈的功能分开&#xff0c;一个专门入pus…

多机器人群体的任务状态与机器人状态同步设计思路

背景技术 近年来&#xff0c;随着科学技术的发展需要&#xff0c;机器人技术不断进步。面临任务的日益复杂化&#xff0c;单机器人在很多环境下已经无法满足生产要求&#xff0c;于是国内外科研工作者对多机器人技术投入了大量关注&#xff0c;提出了利用多机器人协作来代替单机…

Karmada更高效地实现故障转移

随着云原生技术的发展&#xff0c;其应用场景不断扩大。越来越多的企业开始将应用程序部署在 Kubernetes 集群中&#xff0c;随着 Kubernetes 集群规模的不断扩大&#xff0c;也带来了许多管理挑战&#xff0c;例如多集群间负载均衡、资源调度、故障转移等问题。为了解决这些问…

【Python】上市公司数据进行经典OLS回归实操

一、题目二、数据合并、清洗、描述性统计1、数据获取2、数据合并3、选择董监高薪酬作为解释变量的理论逻辑分析 三、多元回归模型的参数估计、结果展示与分析1、描述性统计分析2、剔除金融类上市公司3、对所有变量进行1%缩尾处理4、0-1标准化&#xff0c;所有解释变量5、绘制热…

网络运维Day16

文章目录 Docker简介什么是容器命名空间&#xff1a; Docker 的优缺点 Docker安装Docker镜像管理什么是镜像镜像管理 Docker容器管理运行容器容器启动、停止、重启拷贝文件进入容器容器与应用 DockerfileDockerfile 语法案例 总结 Docker简介 什么是容器 容器是用来装东西的&a…

磁带标签设计:Tape Label Studio 2023.11.0.7 Crack

Tape Label Studio&#xff08;磁带标签设计&#xff09; 为标签创建颜色样式。修改标签中使用的每种颜色&#xff0c;包括背景、条形码、边框、文本和字符颜色。自定义边框样式以适合您正在使用的标签。从实心、虚线或虚线边框中进行选择。轻松调整宽度和宽度。Tape Label St…

【网络奇缘】- 计算机网络|网络类型|性能指标

&#x1f308;个人主页: Aileen_0v0&#x1f525;系列专栏: 一见倾心,再见倾城 --- 计算机网络~&#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 目录 计算机网络分类 1.根据范围分类 ​编辑 2.按使用者分​编辑 3.按交换技术分 ​编辑4.按拓扑结构分 ​…

react中间件的理解

一、是什么&#xff1f; 中间件&#xff08;Middleware&#xff09;在计算机中&#xff0c;是介于应用系统和系统软件之间的一类软件&#xff0c;它使用系统软件所提供的基础服务&#xff08;功能&#xff09;&#xff0c;衔接网络应用上的各个部分或不同的应用&#xff0c;能…

Netty Review - 从BIO到NIO的进化推演

文章目录 BIODEMO 1DEMO 2小结论单线程BIO的缺陷BIO如何处理并发多线程BIO服务器的弊端 NIONIO要解决的问题模拟NIO方案一&#xff1a; &#xff08;等待连接时和等待数据时不阻塞&#xff09;方案二&#xff08;缓存Socket&#xff0c;轮询数据是否准备好&#xff09;方案二存…

225.用队列实现栈(LeetCode)

思路 思路&#xff1a;用两个队列实现栈后进先出的特性 &#xff0c;两个队列为空时&#xff0c;先将数据都导向其中一个队列。 当要模拟出栈时&#xff0c;将前面的元素都导入另一个空队列&#xff0c;再将最后一个元素移出队列 实现 实现&#xff1a; 因为C语言没有库可以…

网络运维Day17

文章目录 什么是数据库MySQL介绍实验环境准备构建MySQL服务连接数据库修改root密码 数据库基础常用的SQL命令分类SQL命令使用规则MySQL基本操作创建库创建表查看表结构 记录管理命令 数据类型数值类型 数据类型日期时间类型时间函数案例枚举类型 约束条件案例修改表结构添加新字…

C++实现ransac

目录 一、ransac算法原理 1.1、算法概念 1.2、图解 二、c实现ransac 2.1、设置随机样本和离群点 2.2、随机抽取样本 2.3、内点计算 2.4、更新参数 2.2、完整代码 一、ransac算法原理 1.1、算法概念 随机抽样一致性 (RANSAC) 是一种迭代方法&#xff0c;用于根据一组包…