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 processprintf("Child process, PID: %d\n", getpid());} else {// This is the parent processprintf("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);// 如何该子进程对应的客户端退出了,该函数返回,应该退出子进程,防止子进程acceptexit(1);} else {// 在父进程中可以关闭掉子进程的套接字,继续下一次while循环执行acceptclose(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.rhkb.cn/news/192296.html

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

相关文章

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

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

Intellij IDEA 内存设置的问题 及解决

解决方案&#xff1a; 在IDEA上运行较大项目时&#xff0c;编译量很大&#xff0c;可能会报出 Error:java: java.lang.OutOfMemoryError: Java heap space 的错误&#xff0c;解决方法如下&#xff1a; java.lang.OutOfMemoryError是内存不足导致的&#xff0c;因此需要修改Id…

PSP - 蛋白质复合物结构预测 Template 的 Multichain Mask 2D (二维多链掩码)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/134406459 在 蛋白质复合物结构预测 中&#xff0c;AlphaFold2 Multimer 的 Multichain Mask 2D 对于 模版特征 (Template) 的影响较大&#xff0…

1688往微信小程序自营商城铺货商品采集API接口

一、背景介绍 随着移动互联网的快速发展&#xff0c;微信小程序作为一种新型的电商形态&#xff0c;正逐渐成为广大商家拓展销售渠道、提升品牌影响力的重要平台。然而&#xff0c;对于许多传统企业而言&#xff0c;如何将商品信息快速、准确地铺货到微信小程序自营商城是一个…

网络编程 —— TCP 和 UDP 编程详解

目录 网络编程主要函数介绍 1. socket 函数 2. bind 函数 3. listen 函数 4. accept 函数 5. connect 函数 6. send 函数 7. recv 函数 8. recvfrom 函数 9. sendto 函数 TCP 和 UDP 原理上的区别 TCP 编程 服务端代码&#xff1a; 客户端代码&#xff1a; UDP 编…

Android设计模式--工厂模式

一&#xff0c;定义 工厂模式与Android 设计模式--单例模式-CSDN博客&#xff0c;Android设计模式--Builder建造者模式-CSDN博客&#xff0c;Android设计模式--原型模式-CSDN博客 一样&#xff0c;都是创建型设计模式。 工厂模式就是定义一个用于创建对象的接口&#xff0c;让…

225.用队列实现栈(LeetCode)

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

uni-app报错“本应用使用HBuilderX x.x.x 或对应的cli版本编译,而手机端SDK版本是x.x.x不匹配的版本可能造成应用异常”

uniapp开发的一个跨平台软件&#xff0c;在安卓模拟器上启动的时候报警告&#xff1a; 官方给的解释&#xff1a;uni-app运行环境版本和编译器版本不一致的问题 - DCloud问答 解决办法有两个 方法一&#xff1a;添加忽略警告的配置 项目根目录下找到 manifest.json&#xf…

LeetCode算法心得——高级访客(模拟枚举+小窗口)

大家好&#xff0c;我是晴天学长&#xff0c;今天的周赛第二题&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .高级访客 给你一个长度为 n 、下标从 0 开始的二维字符串数组 access_times 。对于每个 …

Spark SQL编程

1. Spark SQL概述 1.1 什么是Spark SQL Spark SQL是用于结构化数据处理的Spark模块。与基本的Spark RDD API不同&#xff0c;Spark SQL提供的接口为Spark提供了有关数据结构和正在执行的计算的更多信息。在内部&#xff0c;Spark SQL使用这些额外的信息来执行额外的优化。与Spa…

一文讲清生产质量场景的数据分析思路及案例实战

今天&#xff0c;顺着制造业数据分析这个大主题&#xff0c;我们来讲讲质量管理数据分析。   说起质量管理&#xff0c;就是对所生产的产品质量进行管理&#xff0c;其最终目的就是保证客户收到的产品质量&#xff0c;提高客户满意度&#xff0c;减少退货和维修的数量。质量管…

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

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

【计算机网络笔记】CIDR与路由聚合

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

日历应用程序 BusyCal mac中文版软件特点

BusyCal mac是一款日历应用程序&#xff0c;它可以帮助用户轻松地管理日程安排、事件提醒、会议安排等。BusyCal 支持 macOS 和 iOS 平台&#xff0c;并且可以与 iCloud、Google 日历、Exchange 等多种日历服务进行同步。 BusyCal mac软件特点 强大的日历功能&#xff1a;Busy…

单链表经典OJ题(三)

目录 1、反转链表 2、合并两个有序链表 3、链表的中间结点 4、环形链表的约瑟夫问题 5、移除链表元素 6、移除元素 1、反转链表 206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 翻转链表的实质就是更改当前结点的前驱结点和后继结点 假设原链表为:1->2->…

【nlp】2.4 GRU模型

GRU模型 1 GRU介绍2 GRU的内部结构图2.1 GRU结构分析2.2 Bi-GRU介绍2.3 使用Pytorch构建GRU模型2.4 GRU优缺点3 RNN及其变体1 GRU介绍 GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆…

Day29力扣打卡

打卡记录 美丽塔 II&#xff08;前后缀分解 单调栈&#xff09; 链接 大佬的题解 class Solution:def maximumSumOfHeights(self, a: List[int]) -> int:n len(a)suf [0] * (n 1)st [n] # 哨兵s 0for i in range(n - 1, -1, -1):x a[i]while len(st) > 1 and …

​TechSmith Camtasia 2024破解版功能介绍及使用教程

在现在的网络互联网时代&#xff0c;越来越多的人走上了自媒体的道路。有些自媒体人会自己在网络上录制精彩视频&#xff0c;也有一些人会将精彩、热门的电影剪辑出来再加上自己给它的配音&#xff0c;做成大家喜欢看的电影剪辑片段。相信不管大家是自己平时有独特的爱好也好、…

Django(五、视图层)

文章目录 一、视图层1.视图函数返回值的问题2.三板斧的使用结论&#xff1a;在视图文件中写视图函数的时候不能没有返回值&#xff0c;默认返回的是None&#xff0c;但是页面上会报错&#xff0c;用来处理请求的视图函数都必须返回httpResponse对象。 二、JsonReponse序列化类的…

[单片机课程设计报告汇总] 单片机设计报告常用硬件元器件描述

[单片机课程设计必看] 单片机设计报告常用描述 硬件设计 AT89C51最小系统 AT89C51是美国ATMEL公司生产的低电压&#xff0c;高性能CMOS16位单片机&#xff0c;片内含4k bytes的可反复擦写的只读程序存储器和128 bytes的随机存取数据存储器&#xff0c;期间采用ATMEL公司的高…