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

news2024/11/20 20:20:24

上一节,我们实现了一个客户端/服务端的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.coloradmin.cn/o/1210046.html

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

相关文章

揭秘Vue中的nextTick:异步更新队列背后的技术原理大揭秘!

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、N…

解码方法00

题目链接 解码方法 题目描述 注意点 s 只包含数字&#xff0c;并且可能包含前导零计算并返回 解码 方法的 总数 解答思路 使用动态规划解决本题&#xff0c;其思路为&#xff1a;从后往前遍历字符串&#xff0c;遍历到任一i位置的字符c时&#xff0c;有几种情况&#xff1…

react中间件的理解

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

ubuntu20安装opencv4和opencv_contrib 多版本共存

openCV 卸载 openCV 安装后的源码尽可能保留&#xff0c;因为可以直接从build文件夹下卸载已经安装的openCV. 参考链接&#xff1a;视觉学习笔记10——opencv的卸载、安装与多版本管理 如果已经安装完openCV,后续想重新装&#xff0c;需要先卸载掉安装的openCV. 在ubuntu终端…

离散卡尔曼滤波器算法详解及重要参数(Q、R、P)的讨论

公开数据集中文版详细描述参考前文&#xff1a;https://editor.csdn.net/md/?not_checkout1&spm1011.2124.3001.6192神经元Spike信号分析参考前文&#xff1a;https://blog.csdn.net/qq_43811536/article/details/134359566?spm1001.2014.3001.5501神经元运动调制分析参考…

安防监控EasyCVR视频汇聚平台使用海康SDK播放出现花屏是什么原因?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…

《洛谷深入浅出进阶篇》P3397 地毯————二维差分

上链接&#xff1a;P3397 地毯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P3397 上题干&#xff1a; 题目描述 在 nn 的格子上有 m 个地毯。 给出这些地毯的信息&#xff0c;问每个点被多少个地毯覆盖。 输入格式 第一行&#xff0c;两个…

Git | Git的基本操作以及原理介绍

文章目录 基本操作创建git仓库配置name和email .git目录的结构git add & git commit.git目录结构的变化 git追踪管理的数据git的版本回退回退的原理回退的三种情况 版本库中文件的删除git分支管理分支的删除合并分支时的冲突分支的合并模式分支策略git stash不要在master分…

实用篇-ES-DSL操作文档

一、mapping属性 mapping属性的官方文档: https://elastic.co/guide/en/elasticsearch/reference/current/index.html 下面的表格是介绍elasticsearch中的各个概念以及含义&#xff0c;看的时候重点看第二、三列&#xff0c;第一列是为了让你更理解第二列的意思&#xff0c;所…

嵌入式养成计划-54----ARM--异常处理流程

一百三十五、异常处理流程 135.1 arm处理器工作模式 135.2 异常源和异常模式关系 135.2.1 异常源 异常源就是引发处理器进入相应异常模式 135.2.2 对应关系 异常模式异常源FIQ模式FIQ类型异常源引发处理器进入FIQ模式IRQ模式IRQ类型异常源引发处理器进入IRQ模式SVC模式上电…

三、Eureka注册中心

目录 一、作用及调用方式 二、搭建eureka注册中心 三、注册user-service和order-service 四、新增实例 五、服务拉取 六、总结 一、作用及调用方式 在服务提供者启动时&#xff0c;它会向eureka注册中心提供自己的信息&#xff0c;并每30秒进行一次刷新eureka注册中心保存…

14——2

这道题目前面看不懂可以看比如后面的 这里1/3是因为S100的长度n3&#xff08;100占3位&#xff09;&#xff0c;然后1出现的占比是1/3&#xff08;1在第一位&#xff09;&#xff0c;0出现的占比是2/3&#xff0c;因为0出现了2次&#xff0c;&#xff08;第二位&#xff0c;第…

【文章学习系列之模型】DAGMM

本章内容 文章概况模型结构损失函数实验结果实验分析总结 文章概况 《Deep Autoencoding Gaussian Mixture Model for Unsupervised Anomaly Detection》是2018年发表于ICLR的一篇论文&#xff0c;该论文提出一种端到端的无监督异常检测方法DAGMM&#xff0c;取得了不错的效果…

3.3 Linux 文件管理

1、查看系统信息 tty 命令 描述&#xff1a;查看当前系统在哪个终端语法&#xff1a;tty Linux默认情况下提供6个虚拟终端来让用户登录&#xff0c;系统将F1~F6定义为tty1~tty6。 ctrlalt(F1~F6) &#xff1a;从图形界面切换到命令行界面的第 n 个虚拟终端&#xff08;F1 是…

社区牛奶直供站:创新供应链,满足消费者需求

社区牛奶直供站&#xff1a;创新供应链&#xff0c;满足消费者需求 社区牛奶直供站模式彻底改变了传统的多级经销链条和流通加价环节&#xff0c;通过削减中间环节&#xff0c;以相对低价直接供应牛奶给消费者。这样做实现了从奶企源头直接供应到社区家庭的目标&#xff0c;精准…

字符串旋转结果

文章目录 题目解法1解法2 题目 字符串旋转结果 写一个函数&#xff0c;判断一个字符串是否为另外一个字符串旋转之后的字符串。 例如&#xff1a;给定s1 AABCD和s2 BCDAA&#xff0c;返回1 给定s1abcd和s2ACBD&#xff0c;返回0. AABCD左旋一个字符得到ABCDA AABCD左旋两个字…

人工智能基础_机器学习033_多项式回归升维_多项式回归代码实现_非线性数据预测_升维后的数据对非线性数据预测---人工智能工作笔记0073

然后我们来实际的操作一下看看,多项式升维的作用,其实就是为了,来对,非线性的数据进行拟合. 我们直接看代码 import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression X=np.linspace(-1,11,num=100) 从-1到11中获取100个数…

No203.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

fileread任意文件读取学习笔记

任意文件读取概述 一些网站的需求&#xff0c;可能会提供文件查看与下载的功能。如果对用户查看或下载的文件没有限制或者限制绕过&#xff0c;就可以查看或下载任意文件。这些文件可以是源代码文件&#xff0c;配置文件&#xff0c;敏感文件等等。 任意文件读取会造成&#x…

深入理解JVM虚拟机第二十五篇:详解JVM方法的绑定机制静态绑定和动态绑定,早期绑定晚期绑定,并编写代码从字节码角度证明这件事情

大神链接&#xff1a;作者有幸结识技术大神孙哥为好友&#xff0c;获益匪浅。现在把孙哥视频分享给大家。 孙哥链接&#xff1a;孙哥个人主页 作者简介&#xff1a;一个颜值99分&#xff0c;只比孙哥差一点的程序员 本专栏简介&#xff1a;话不多说&#xff0c;让我们一起干翻J…