Linux 并发服务器编程(多进程)
说明
在Linux中通过流式套接字编程(TCP),实现一个并发服务器的访问回显,适合刚学完Linux套接字编程的朋友进行巩固训练
具体功能:
- 服务器能够同时连接、处理多个客户端的信息
- 客户端向服务器发送数据之后,服务器收到数据,然后反手发送给客户端
- 服务器能够对客户端的退出做出反应,并在客户端退出连接的时候给出提示
- 服务器能够识别每个客户端发送的信息,在显示的时候加上客户端的IP地址
- 服务器中能够对已经退出的服务进程作回收处理
- 客户端能够对服务器的退出作出反应,检测到服务器退出后客户端也退出
注意事项
- 多进程并发服务器编程中,每次建立一个套接字连接,都会fork一个进程来处理
- accept是自带阻塞的,所以fork返回父进程之后,父进程就会阻塞等待下一个已连接套接字
- 客户端的关闭通过ctrl-c发出的信号(SIGINT)来终止客户端
- 当客户端终止之后,服务器上对应的服务进程通过exit结束,此时由于服务器的主进程还阻塞在accept中,所以无法及时回收子服务进程,所以通过注册一个信号SIGCHLD处理函数,在信号处理的时候回收僵尸子进程。SIGCHLD是子进程结束的时候发送给父进程的信号,默认忽略。
- 服务器进程如何检测客户端退出呢?通过
recv()
函数,当返回值为0的时候,表示客户端已经关闭套接字,即客户端退出。 - 当服务线程主动关闭的时候,客户端也会通过
recv()
收到服务器关闭的信息,然后客户端主动退出 - 关于套接字描述符,因为描述符也算是进程的资源,当套接字描述符的引用值为0的时候,才会关闭套接字,或者是进程退出的时候释放套接字描述符资源。
- 每次fork的时候,都会产生一个对于已经打开的套接字描述符的引用,所以要在进入子服务进程后关闭监听套接字描述符、在主服务进程中关闭已连接套接字描述符、在子服务进程退出的时候关闭已连接套接字描述符、在退出主服务器进程的时候关闭监听套接字描述符,这样才做到了有始有终(fork之后已连接套接字描述符的引用就有两份)
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>
#define SERVER_ADDR "172.17.44.154"
#define BUFSIZE 100
void sigchld_handler(int arg);
int main(int argc, const char *argv[])
{
int socket_fd, new_fd;
struct sockaddr_in server_addr, cli_addr;
char buf[BUFSIZE];
int pid;
struct sigaction sig;
/* 注册中断信号处理函数 */
sig.sa_handler = sigchld_handler;
sigaction(SIGCHLD, &sig, NULL);
/* 创建套接字,并获取套接字描述符 */
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == socket_fd) {
perror("socket");
exit(-1);
}
/* 绑定地址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(5001);
inet_pton(AF_INET, SERVER_ADDR, (void*)&server_addr.sin_addr.s_addr); //地址转换
if (-1 == bind(socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) {
perror("bind");
exit(-1);
}
/* 转换为被动连接套接字 */
if (-1 == listen(socket_fd, 5)) {
perror("listen");
exit(-1);
}
#if 0 //单进程服务器
/* 获取已连接套接字 */
socklen_t len = 0;
new_fd = accept(socket_fd, (struct sockaddr*)&cli_addr, (socklen_t *)&len);
if (-1 == new_fd) {
perror("accept");
exit(-1);
}
printf("accept socket!\nclient ip :%s port:%d\n", inet_ntoa(cli_addr.sin_addr), cli_addr.sin_port);
while (1) {
memset(buf, 0, BUFSIZE);
if (0 == recv(new_fd, buf, BUFSIZE, 0)) { //接受数据
printf("the client is closed\n");
break;
}
printf("read:%s\n", buf);
send(new_fd, buf, BUFSIZE, 0); //回应客户端
}
close(new_fd);
#else //多进程并发服务器
while (1) {
socklen_t len = 0;
new_fd = accept(socket_fd, (struct sockaddr*)&cli_addr, (socklen_t *)&len);
if (-1 == new_fd) {
if (errno == EINTR) continue; //accept可能会被信号中断
perror("accept");
exit(-1);
}
/* 并发服务器:子进程中进行TCP通信 */
pid = fork();
if (pid == -1) {
perror("fork");
exit(0);
} else if (pid == 0) {
close(socket_fd); //关闭监听套接字
while (1) {
memset(buf, 0, BUFSIZE);
if (0 == recv(new_fd, buf, BUFSIZE, 0)) { //接受数据,只有当客户端主动关闭的时候,才退出线程,还要对关闭之后的子进程
printf("the client socket %s is closed\n", inet_ntoa((struct in_addr)cli_addr.sin_addr));
close(new_fd); //退出之前记得关闭 已连接套接字
exit(0); //通过信号处理函数进行回收
}
getsockname(new_fd, (struct sockaddr*)&cli_addr, (socklen_t *)&len); //获取连接套接字信息
printf("recv client IP:%s data:%s\n", inet_ntoa((struct in_addr)cli_addr.sin_addr), buf);
send(new_fd, buf, BUFSIZE, 0); //回应客户端
}
} else {
close(new_fd); //父进程关闭 已连接套接字
}
}
#endif
close(socket_fd);
return 0;
}
void sigchld_handler(int arg)
{
int child_pid;
if (SIGCHLD == arg) {
if ((child_pid = waitpid(-1, NULL, WNOHANG)) == -1) {
perror("sigchld");
}
printf("a client %d is end\n", child_pid);
}
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#define SERVER_ADDR "172.17.44.154"
#define BUFSIZE 100
int main(int argc, const char *argv[])
{
int new_fd;
struct sockaddr_in server_addr;
char buf[BUFSIZE];
/* 创建套接字,并获取套接字描述符 */
new_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == new_fd) {
perror("socket");
exit(-1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(5001);
inet_pton(AF_INET, SERVER_ADDR, (void*)&server_addr.sin_addr.s_addr);
if (-1 == connect(new_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) {
perror("connect");
exit(-1);
}
while (1) {
printf("input:");
fgets(buf, BUFSIZE, stdin); //获取数据
if (-1 == send(new_fd, buf, BUFSIZE, 0)) { //发送数据
perror("send");
close(new_fd);
exit(-1);
}
if (0 == recv(new_fd, buf, BUFSIZE, 0)) { //收到数据
printf("server closed\n");
break;
}
printf("recv:%s\n", buf);
}
close(new_fd);
return 0;
}
运行截图
PS:这里是在同一主机下做实验的,所以各个客户端的IP地址都是一样的
正常运行的状态如下:
当有一个客户端退出时,服务器会显示信息,但是对其他客户端的服务正常进行:
当服务器主动关闭之后,所有客户端都会收到服务器关闭的信息,并且主动退出:
Linux 并发服务器编程(多进程)的更多相关文章
- Linux 高性能服务器编程——多进程编程
问题聚焦: 进程是Linux操作系统环境的基础. 本篇讨论以下几个内容,同时也是面试经常被问到的一些问题: 1 复制进程映像的fork系统调用和替换进程映像的exec系列系统调 ...
- Linux 高性能服务器编程——高性能服务器程序框架
问题聚焦: 核心章节. 服务器一般分为如下三个主要模块:I/O处理单元(四种I/O模型,两种高效事件处理模块),逻辑单元(两种高效并发模式,有效状态机)和存储单元(不讨论). 服务器模 ...
- Linux 高性能服务器编程——Linux网络编程基础API
问题聚焦: 这节介绍的不仅是网络编程的几个API 更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系. 这节主要介绍三个方面的内容:套接字(so ...
- 第15章 高并发服务器编程(2)_I/O多路复用
3. I/O多路复用:select函数 3.1 I/O多路复用简介 (1)通信领域的时分多路复用 (2)I/O多路复用(I/O multiplexing) ①同一线程,通过“拨开关”方式,来同时处理多 ...
- linux高性能服务器编程
<Linux高性能服务器编程>:当当网.亚马逊 目录: 第一章:tcp/ip协议族 第二章:ip协议族 第三章:tcp协议详解 第四章:tcp/ip通信案例:访问Internet 第五章: ...
- linux高性能服务器编程 (一) --Tcp/Ip协议族
前言: 在学习swoole入门基础的过程中,遇到了很多知识瓶颈,比方说多进程.多线程.以及进程池和线程池等都有诸多的疑惑.之前也有学习相关知识,但只是单纯的知识面了解.而没有真正的学习他们的来龙去脉. ...
- Linux 高性能服务器编程——多线程编程
问题聚焦: 在简单地介绍线程的基本知识之后,主要讨论三个方面的内容: 1 创建线程和结束线程: 2 读取和设置线程属性: 3 线程同步方式:POSIX信号量,互斥锁和条件变量 ...
- Linux 高性能服务器编程——I/O复用
问题聚焦: 前篇提到了I/O处理单元的四种I/O模型. 本篇详细介绍实现这些I/O模型所用到的相关技术. 核心思想:I/O复用 使用情景: 客户端程序要同时处理多个socket ...
- Linux 高性能服务器编程——Linux服务器程序规范
问题聚焦: 除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范. 工欲善其事,必先利其器,这篇主要来探 ...
随机推荐
- 从三道题目入门frida
偶然从看雪看到了一篇入门frida的题目,正好苦于没练手的东西,直接上手一波 1.第一题jadx打开,也没有壳和混淆,整体非常清晰,判断的逻辑也很简单 发现其实就是两个输入框,一个用户名一个密码,先拼 ...
- CF1539D PriceFixed[题解]
PriceFixed 题目大意 市场上又 \(n\) 种商品,每种商品的价格都是 \(2\) .对于第 \(i\) 种商品 \(a_i\) 件.对于商品 \(i\) 给出一个值 \(b_i\) ,如果 ...
- 「CF547D」 Mike and Fish
「CF547D」 Mike and Fish 传送门 介绍三种做法. \(\texttt{Solution 1}\) 上下界网络流 我们将每一行.每一列看成一个点. 两种颜色的数量最多相差 \(1\) ...
- shell编程之循环语句for / while / until
shell编程之循环语句与函数 一.条件测试 二.循环语句 ① for循环语句结构(遍历) 示例1 示例2 ② while循环语句结构(迭代) 示例1 示例2 ③ until 循环语句结构 示例1 一 ...
- 短视频:用快影制作3D音乐视频
用快影制作3D音乐视频1打开快影点击开始剪辑导入一张风景照片,按住照片向后拉到自己需要的时长2点击画中画,点击新增,画中画,导入一张照片3点击模板,选择圆形,调整圆形的大小,摆放到上面合适的位置,按照 ...
- 数据库:随机显示n条记录
1.sqlite3数据库select * from QG order by random() limit 6 以下显示前10条记录 2.SQL Server数据库select top 10 * f ...
- vue3 封装简单的 tabs 切换组件
背景:公司项目要求全部换成 vue3 ,而且也没有应用像 element-ui 一类的UI组件,用到的公共组件都是根据项目需求封装的,下面是使用vue3实现简单的tabs组件,我只是把代码分享出来,实 ...
- Kubernetes部署-RKE自动化部署
一.简介 RKE:Rancher Kubernetes Engine 一个极其简单,闪电般快速的Kubernetes安装程序,可在任何地方使用. 二.准备工作 I.配置系统 系统:CentOS 7 / ...
- grub2引导各种ISO系统镜像
1 grub2引导winpe 1下载winpe.iso镜像,放置在U盘的根目录下 2下载syslinux的memdisk,放置在U盘的根目录下 3 set root=(hd0,msdos1) 设置U ...
- Pytest单元测试框架之FixTure基本使用
前言: 在单元测试框架中,主要分为:测试固件,测试用例,测试套件,测试执行及测试报告: 测试固件不难理解,也就是我们在执行测试用例前需要做的动作和测试执行后的需要做的事情: 比如在UI自动化测试中,我 ...