我们可以构建按序向第一个客户端到第一百个客户端提供服务的服务器端。当然,第一个客户端不会抱怨服务器端,但如果每个客户端的平均服务时间为0.5秒,则第100个客户端会对服务器端产生相当大的不满。
如果真正为客户端着想,应提高客户端满意度平均标准。如果有下面这种类型的服务器端,应该感到满意了吧???
“第一个连接请求的受理时间为0秒,第50个连接请求的受理时间为50秒,第100个连接请求的受理时间为100秒!但只要受理,服务只需1秒钟。”
如果排在前面的请求数能用一只手数清,客户端当然会对服务器端感到满意。但只要超过这个数,客户端就会开始抱怨。还不如用下面这种方式提供服务。
“所有连接请求的受理时间不超过1秒,但平均服务时间为2~3秒。”
即使有可能延长服务时间,也有必要改进服务器端,使其同时向所有发起请求的客户端提供服务,以提高平均满意度。
而且,网络程序中数据通信时间比CPU运算时间占比更大,因此,向多个客户端提供服务是一种有效利用CPU的方式。接下来讨论同时向多个客户端提供服务的并发服务器端。下面列出的是具有代表性的并发服务器端实现模型和方法。
在这里我们以多进程服务器为主。
ps au指令可以查看当前运行的所有进程。特别需要注意的是,该命令同时可以列出PID(进程ID)。通过指定a和u参数可以列出所有进程详细信息。#include <unistd.h>
//→成功时返回进程 ID,失败时返回-1。
pid_t fork(void);
文件操作中,关闭文件和打开文件同等重要。同样,进程销毁也和进程创建同等重要。如果未认真对待进程销毁,它们将变成僵尸进程困扰大家。
进程的世界同样如此。进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作"僵尸进程",这也是给系统带来负担的原因之一。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid=fork();
if(pid==0) // if Child Process
{
puts("Hi I'am a child process");
}
else
{
printf("Child Process ID: %d \n", pid);
sleep(30); // Sleep 30 sec.
}
if(pid==0)
puts("End child process");
else
puts("End parent process");
return 0;
}
结果显示: Z状态的进程为僵尸进程,S:休眠,R:运行,Z僵尸
上面我们了解了,进程的创建和销毁,父进程往往与子进程一样繁忙,不知道什么时候结束子进程, 因此不能只调用waitpid函数无休止的以等待子进程终止,这就需要信号处理来响应关联。
子进程终止的识别主体是操作系统,因此,若操作系统能把子进程的信息告诉正忙于工作的父进程,将有助于构建高效的程序。
信号处理:特定事件发生时由操作系统向进程发送的消息,另外,为了响应该消息,执行与消息相关的自定义操作的过程。
//→为了在产生信号时调用,返回之前注册的函数指针。
/*
函数名∶signal
参数∶int signo, void(* func)(int)
返回类型∶参数为int型,返回void型函数指针。
*/
#include<signal.h>
void(*signal(int signo, void(*func)(int))(int);
//等价于下面的内容:
typedef void(*signal_handler)(int);
signal_handler signal(int signo,signal_handler func);
编写调用signal函数的语句完成如下请求
1、“子进程终止则调用mychild函数。”
代码:signal(SIGCHLD, mychild);
2、“已到通过alarm函数注册的时间,请调用timeout函数。”
3、“输入CTRL+C时调用keycontrol函数。”
以上就是信号注册过程。注册好信号后,发生注册信号时(注册的情况发生时),操作系统将调用该信号对应的函数。
#include<unistd.h>
//→返回0或以秒为单位的距SIGALRM信号发生所剩时间。
unsigned int alarm(unsigned int seconds);
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生SIGALRM信号。若向该函数传递0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用signal函数)终止进程,不做任何处理。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 定义信号处理函数timeout,返回值为void
void timeout(int sig)
{
if(sig==SIGALRM)
puts("Time out!");
//为了每隔2秒重复产生SIGALRM信号,在信号处理器中调用alarm函数
alarm(2);
}
// 定义信号处理函数keycontrol,返回值为void
void keycontrol(int sig)
{
if(sig==SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char *argv[])
{
int i;
//注册SIGALRM、SIGINT信号及相应处理器
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
//预约2秒后发生SIGALRM信号
alarm(2);
for(i=0; i<3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
这是为什么呢?明明是300秒。。。
(空间与时间的平衡,以时间换取空间,还是以空间换取时间)
每次客户端有访问请求时,服务端在accept阶段,去fork创建子进程处理客户端访问请求,同时父进程回到accept阶段继续等待新的客户端请求。问题点在,若同时在线多个用户的瓶颈,导致内存暴增。
//-----------------------------------------------------多任务并发服务器(进程)---------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> //Linux标准数据类型
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
//------------------------------------服务端-----------------------
void hand_childProc(int sig)
{
pid_t pid;
int status = 0;
waitpid(-1, &status, WNOHANG);//-1:回收僵尸进程,WNOHANG:非挂起方式,立马返回status状态
printf("%s(%d):%s removed sub proc:%d\r\n", __FILE__, __LINE__, __FUNCTION__, pid);
}
//服务器
void ps_moretask_server()
{
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = hand_childProc;
sigaction(SIGCHLD, &act, 0);//当发现有SIGCHLD信号时进入到子进程函数。处理任务和进程回收(防止僵尸进程)
int serv_sock;
struct sockaddr_in server_adr, client_adr;
memset(&server_adr, 0, sizeof(server_adr));
server_adr.sin_family = AF_INET;
server_adr.sin_addr.s_addr = htonl(INADDR_ANY);
server_adr.sin_port = htons(9527);
serv_sock = socket(AF_INET, SOCK_STREAM, 0);
if (bind(serv_sock, (sockaddr*)&server_adr, sizeof(server_adr)) == -1)
error_handling("ps_moretask server bind error");
if (listen(serv_sock, 5) == -1)
{
error_handling("ps_moretask server listen error");
}
int count = 0;
char buffer[1024];
while (true)
{
socklen_t size = sizeof(client_adr);
int client_sock = accept(serv_sock, (sockaddr*)&client_adr, &size);
if (client_sock < 0) {
error_handling("ps_moretask server accept error");
close(serv_sock);
return;
}
pid_t pid = fork();//会复制客户端和服务端的socket
if (pid == 0)
{
close(serv_sock);//子进程关闭服务端的socket,因为子进程为了处理客户端的任务
ssize_t len = 0;
while ((len = read(client_sock, buffer, sizeof(buffer))) > 0)
{
len = write(client_sock, buffer, strlen(buffer));
if (len != (ssize_t)strlen(buffer)) {
//error_handling("write message failed!");
std::cout << "ps_moretask server write message failed!\n";
close(serv_sock);
return;
}
std::cout << "ps_moretask server read & write success!, buffer:" << buffer << "__len:" << len << std::endl;
memset(buffer, 0, len);//清理
close(client_sock);
return;
}
}
else if (pid < 0)
{
close(client_sock);
error_handling("ps_moretask server accept fork error");
break;
}
close(client_sock);//服务端关闭的时候,客户端会自动关闭
}
close(serv_sock);
}
//------------------------------------客户端-----------------------
void ps_moretask_client()
{
int client = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(9527);
int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret == -1) {
std::cout << "ps_moretask client connect failed!\n";
close(client);
return;
}
std::cout << "ps_moretask client connect server is success!\n";
char buffer[256] = "hello ps_moretask server, I am client!";
while (1)
{
//fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
//fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
//if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
// break;
//}
size_t len = strlen(buffer);
size_t send_len = 0;
//当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
while (send_len < len)
{
ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
if (ret <= 0) {//连接出了问题
fputs("may be connect newwork failed,make client write failed!\n", stdout);
close(client);
return;
}
send_len += (size_t)ret;
std::cout << "ps_moretask client write success, msg:" << buffer << std::endl;
}
memset(buffer, 0, sizeof(buffer));
//当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
size_t read_len = 0;
while (read_len < len)
{
size_t ret = read(client, buffer + read_len, len - read_len);
if (ret <= 0) {//连接出了问题
fputs("may be connect newwork failed, make client read failed!\n", stdout);
close(client);
return;
}
read_len += (size_t)ret;
}
std::cout << "from server:" << buffer << std::endl;
};
sleep(2);//延时2秒关闭客户端
close(client);
std::cout << "ps_moretask client done!" << std::endl;
}
//------------------------------------调用函数-----------------------
void ps_moretask_func()
{
pid_t pid = fork();
if (pid > 0)
{
printf("%s(%d):%s wait ps_moretask server invoking!\r\n", __FILE__, __LINE__, __FUNCTION__);
sleep(1);
for (int i = 0; i < 5; i++)
{
pid = fork();
if (pid > 0) {
continue;
}
else if (pid == 0)
{
//子进程启动客户端
ps_moretask_client();
break;//到子进程终止,避免指数级创建进程 n的2次方。
}
}
}
else if (pid == 0) {
//启动服务端
ps_moretask_server();
}
}
IPC(Inter Process Communication):进程间通信,通过内核提供的缓冲区进行数据交换的机制。进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间。
进程A和B之间的如下例子就是一种进程间通信规则。
只要有两个进程可以同时访问的内存空间,就可以通过此空间交换数据。但是进程具有完全的内存结构。就连通过fork函数创建的子进程也不会与父进程共享内存空间。因此,进程间通信只能通过其他特殊方法完成。
进程中,子进程会复制父进程的内存,而父进程不会复制子进程的内存,因此子进程的一些操作父进程是不知道的。
(亲族管道,处理两个不相干的进程时会有问题)
#include <unistd.h>
//→成功时返回 0,失败时返回-1。
int pipe(int filedes[2]);
/*
Filedes[0] 通过管道接收数据时使用的文件描述符,即管道出口。
Fledes[1] 通过管道传输数据时使用的文件描述符,即管道入口。
*/
//进程通信——双管道(PIPE)
void ps_pipe_double_func()
{
int fds_server[2] = { -1, -1 };
int fds_client[2] = { -1, -1 };
pipe(fds_server);//父进程创建管道
pipe(fds_client);
pid_t pid = fork();
if (pid == 0)
{
char buffer[] = "client send by child process!\n";
char readBuf[128] = "";
//子进程数据写入
write(fds_client[1], buffer, sizeof(buffer));
read(fds_server[0], readBuf, sizeof(readBuf));
printf("%s(%d):%s child process read ps_pipe by server :%s\r\n", __FILE__, __LINE__, __FUNCTION__, readBuf);
printf("%s(%d):%s ---pid:%d\r\n", __FILE__, __LINE__, __FUNCTION__, getpid());
}
else
{
char buffer[] = "server send by father process!\n";
char readBuf[128] = "";
//父进程读取数据
read(fds_client[0], readBuf, sizeof(readBuf));
printf("%s(%d):%s father process read ps_pipe by client :%s\r\n", __FILE__, __LINE__, __FUNCTION__, readBuf);
write(fds_server[1], buffer, sizeof(buffer));
}
printf("%s(%d):%s ---pid:%d\r\n", __FILE__, __LINE__, __FUNCTION__, getpid());
}
FIFO:first in first out,先进先出。(每个进程都要有个命名文件)
对比pipe管道,他已经可以完成在两个进程之间通信的任务,不过它似乎完成的不够好,也可以说是不够彻底。它只能在两个有亲戚关系的进程之间进行通信,这就大大了pipe管道的应用范围。我们在很多时候往往希望能够在两个的进程之间进行通信,这样就无法使用pipe管道,所以一种能够满足进程通信的管道应运而生,就是fifo管道。
fifo管道的本质是操作系统中的命名文件
它在操作系统中以命名文件的形式存在,我们可以在操作系统中看见fifo管道,在你有权限的情况下,甚至可以读写他们。
内核会针对fifo文件开辟一个缓冲区,操作FIFO文件,可以操作缓冲区,实现进程通信。一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件IO函数都可以用于FIFO。如:close、read、write、unlink等 .
//进程通信-命名管道(FIFO)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>//创建命名管道头文件
#include <fcntl.h>
#include <string.h>
void ps_fifo_func()
{
mkfifo("./test_fifo.fifo", 0666);//创建FIFO命名管道,并设置mode
pid_t pd = fork();
if (pd == 0)
{
sleep(1);
int fd = open("./test_fifo.fifo", O_RDONLY);//打开创建的fifo文件,并申请读权限
char buffer[] = "";
ssize_t len = read(fd, buffer, sizeof(buffer));
printf("%s(%d):%s read ps_fifo server :%s len: %d\r\n", __FILE__, __LINE__, __FUNCTION__, buffer, len);
close(fd);
}
else
{
int fd = open("./test_fifo.fifo", O_WRONLY);//打开创建的fifo文件, 并申请读写权限
char buffer[128] = "hello, I am fifo server!";
ssize_t len = write(fd, buffer, sizeof(buffer));
printf("%s(%d):%s ps_fifo server wait success!\r\n", __FILE__, __LINE__, __FUNCTION__);
close(fd);
}
}
(数据同步时,具有部分时间差,比较耗时)
int shmget( key_t key, size_t size, int shmflg );
–key:这段共享内存取的名字,系统利用它来区分共享内存,访问同一段共享内存的不同进程需要传入相同的名字。
–size:共享内存的大小
–shmflg:是共享内存的标志,包含9个比特标志位,其内容与创建文件时的mode相同。有一个特殊的标志IPC_CREAT可以和权限标志以或的形式传入
int shmctl( int shm_id, int command, struct shmid_ds* buf );
–shm_id:是共享内存的标示符,也即是shmget()的返回值。
–command:是要采取的动作,它有三个有效值,如下所示:
#include <sys/ipc.h>
#include <sys/shm.h>//共享内存头文件
//共享的结构体
typedef struct {
int id;
char name[128];
int age;
bool sex;
int signal;
}STUDENT, *P_STUDENT;
void ps_sharememory_func()
{
pid_t pid = fork();
if (pid > 0)
{
//shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
if (shm_id == -1) {
printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
return;
}
P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
pStu->id = 666666;
strcpy(pStu->name, "welcome moon");
pStu->age = 19;
pStu->sex = true;
pStu->signal = 99;
while (pStu->signal == 99)//同步
{
usleep(100000);
}
shmdt(pStu);
shmctl(shm_id, IPC_RMID, NULL);
}
else {
usleep(500000);//休眠500ms,等待父进程写入数据, 1000单位为1ms,100000为100ms
//shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
if (shm_id == -1) {
printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
return;
}
P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
while (pStu->signal != 99)//同步
{
usleep(100000);
}
printf("student msg: %d, %s, %d, %s\n", pStu->id, pStu->name, pStu->age, pStu->sex == true ? "male":"famale");
pStu->signal = 0;
shmdt(pStu);
shmctl(shm_id, IPC_RMID, NULL);
}
}
调协进程对共享资源的访问。只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要探索二进制信号量。ipcs -s//进程通信————————————————————共享内存+信号量
#include <sys/ipc.h>
#include <sys/shm.h>//共享内存头文件
#include <sys/sem.h>//信号量头文件
//共享的结构体
typedef struct {
int id;
char name[128];
int age;
bool sex;
int signal;
}STUDENT, *P_STUDENT;
void ps_sharememory_func()
{
pid_t pid = fork();
if (pid > 0)
{
//创建信号量
key_t key = ftok(".", 2);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
int sem_id = semget(key, 2, IPC_CREAT);//信号量id创建,key:代表当前路径,2:创建两个信号量,IPC_CREAT:表示进程通信创建
//pv 生产者 消费者模式(P通过,V释放)
//两个信号量
semctl(sem_id, 0, SETVAL, 0);
semctl(sem_id, 1, SETVAL, 0);
//shmget创建共享文件id
int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
if (shm_id == -1) {
printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
return;
}
P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
pStu->id = 666666;
strcpy(pStu->name, "welcome moon");
pStu->age = 19;
pStu->sex = true;
//第一个信号量 v操作
sembuf sop = {
.sem_num = 0,
.sem_op = 1
};
semop(sem_id, &sop, 1);//v操作,
semctl(sem_id, 0, GETVAL);
//第二个信号量 P操作
sop.sem_num = 1;
sop.sem_op = -1;
semop(sem_id, &sop, 1);//P操作
semctl(sem_id, 1, GETVAL);
//删除共享内存
shmdt(pStu);
shmctl(shm_id, IPC_RMID, NULL);
//主进程删除信号量
semctl(sem_id, 0, IPC_RMID);//释放掉第一个信号量
semctl(sem_id, 1, IPC_RMID);//释放掉第二个信号量
sleep(10);//休眠10秒
}
else {
usleep(500000);//休眠500ms,等待父进程写入数据, 1000单位为1ms,100000为100ms
//创建信号量
key_t key = ftok(".", 2);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
int sem_id = semget(key, 2, IPC_CREAT);//信号量id创建
//shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
if (shm_id == -1) {
printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
return;
}
//第一个信号量 P操作
sembuf sop = {
.sem_num = 0,
.sem_op = -1
};
semop(sem_id, &sop, 1);//P操作,
semctl(sem_id, 0, GETVAL);
P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
//第二个信号量 V操作
sop.sem_num = 1;
sop.sem_op = 1;
semop(sem_id, &sop, 1);//V操作,
semctl(sem_id, 1, GETVAL);
printf("student msg: %d, %s, %d, %s\n", pStu->id, pStu->name, pStu->age, pStu->sex == true ? "male":"famale");
pStu->signal = 0;
shmdt(pStu);
shmctl(shm_id, IPC_RMID, NULL);
sleep(10);//休眠10秒
}
}
该函数用来创建和访问一个消息队列。它的原型为:
int msgget(key_t key, int msgflg);
该函数用来把消息添加到消息队列中。它的原型为:
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
该函数用来从一个消息队列获取消息,它的原型为
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
该函数用来控制消息队列,它与共享内存的shmctl函数相似,它的原型为:
int msgctl(int msgid, int command, struct msgid_ds *buf);
//成功时返回0,失败时返回-1.
struct msgid_ds {
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
//进程通信----------消息队列
#include <sys/msg.h>
typedef struct {
int type;
struct {
int id;
char name[];
int age;
char msg[256];
}data, *pdata;
}MSG, *PMSG;
void ps_msg_func()
{
pid_t pid = fork();
if (pid > 0)
{
int msg_id = msgget(ftok(".", 3), IPC_CREAT | 0666);
if (msg_id == -1)
{
printf("%s(%d):%s ps_msg server error=%d:%s!\r\n", __FILE__, __LINE__, __FUNCTION__, errno,strerror(errno));
return;
}
MSG msg;
memset(&msg, 0, sizeof(msg));
while (true)
{
ssize_t ret = msgrcv(msg_id, &msg, sizeof(msg.data), 1, 0);
if (ret == -1) {
sleep(1);
printf("sleeping ~");
}
else
{
break;
}
}
printf("accept msg: %d, %s, %d, %s\n", msg.data.id, msg.data.name, msg.data.age, msg.data.msg);
getchar();
msgctl(msg_id, IPC_RMID, 0);
}
else
{
int msg_id = msgget(ftok(".", 3), IPC_CREAT | 0666);
MSG msg;
memset(&msg, 0, sizeof(msg));
msg.data.id = 5555;
msg.data.age = 19;
strcpy(msg.data.name, "moon");
strcpy(msg.data.msg, "hello friend!");
msgsnd(msg_id, &msg, sizeof(msg), 0);
printf("send msg: %d, %s, %d, %s\n", msg.data.id, msg.data.name, msg.data.age, msg.data.msg);
//休眠两秒后,待数据发送出去再做删除
sleep(2);
msgctl(msg_id, IPC_RMID, 0);
}
}
Function not implemented),因为我使用的是Ubuntu,消息队列在Ubuntu系统上无法处理:因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- gamedaodao.net 版权所有 湘ICP备2024080961号-6
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务