一、简介
服务器按处理方式可以分为迭代服务器和并发服务器两类。服务器每次只能处理一个客户的请求,它实现简单但效率很低,这种服务器通常称为迭代服务器。然而在实际应用中,不可能让一个服务器长时间为一个客户服务,而需要其具有同时处理多个客户请求的能力,这种同时可以处理多个客户请求的服务器称为并发服务器,其效率很高却实现复杂。在实际应用中,并发服务器应用的最广泛。Linux有3种实现并发服务器的方式:多进程并发服务器、多线程并发服务器、IO复用。
本篇文章,小编将带大家一起看看多进程并发服务器是如何实现的!
如果大家对于网络编程socket不是很熟悉,可以先看《APUE学习之网络编程socket》这篇文章!
在使用Socket进行多进程网络编程时,一般的步骤如下:
-
创建Socket: 在服务器端创建一个Socket,绑定并监听端口,等待客户端的连接请求。
-
接受连接: 当有客户端请求连接时,使用
accept()
函数接受连接,并创建一个新的进程来处理该连接。 -
处理请求: 新的进程负责处理特定客户端的请求。这包括接收和发送数据,执行相应的业务逻辑等。
-
并发处理: 服务器在接受连接后,可以并发地创建多个进程,每个进程独立处理一个连接。这样,服务器就能够同时服务多个客户端。
-
关闭连接: 处理完客户端请求后,关闭连接并退出进程。
使用多进程的优势在于每个连接都在独立的进程中运行,彼此之间不受影响。如果一个连接发生阻塞或其他问题,不会影响其他连接的正常处理。此外,多进程模型相对简单,容易理解和实现。
二、多进程并发服务器基本流程
三、多进程编程实例代码
1、多进程并发服务器端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <getopt.h>
#define port 8889
int main(int argc,char *argv[])
{
int sockfd = -1;
int rv = -1;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
int clifd = -1;
socklen_t len = 0;
pid_t pid;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
printf("Create sockfd failure:%s\n",strerror(errno));
return -1;
}
printf("Create sockfd[%d] successfully!\n",sockfd);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
rv = bind(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
if(rv < 0)
{
printf("Socket[%d] bind on port[%d] failure:%s\n",sockfd,port,strerror(errno));
return -2;
}
listen(sockfd,13);
printf("Strart to listen port[%d]\n",port);
while(1)
{
printf("Start to accept new client incoming....\n");
clifd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(clifd < 0)
{
printf("Accept new client failure:%s\n",strerror(errno));
continue;
}
printf("Accept new client [%s:%d] successfully\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
pid = fork();
if(pid < 0)
{
printf("fork create new child process failure:%s\n",strerror(errno));
continue;
}
else if(pid > 0)
{
close(clifd);
continue;
}
else if (0 == pid)
{
char buf[1024];
int i;
printf("child process start to commuicate with socket client....\n");
close(sockfd);
while(1)
{
memset(buf,0,sizeof(buf));
if((rv = read(clifd,buf,sizeof(buf))) < 0)
{
printf("Read data from client child[%d] failure:%s\n",clifd,strerror(errno));
close(clifd);
exit(0);
}
else if(0 == rv)
{
printf("clifd[%d] get disconnected\n",clifd);
close(clifd);
exit(0);
}
else if(rv > 0)
{
printf("Write to client by clifd[%d] :%s\n",clifd,buf);
}
rv = write(clifd,buf,rv);
if(rv < 0)
{
printf("Write to clifd[%d] failure:%s\n",strerror(errno));
close(clifd);
exit(0);
}
}
}
}
close(sockfd);
return 0;
}
2、客户端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8889
#define MSG_STR "Hello,Unix Network Program World!"
int main(int argc,char *argv[])
{
int con_fd = -1;
int rv = -1;
struct sockaddr_in ser_addr;
char buf[1024];
con_fd = socket(AF_INET,SOCK_STREAM,0);
if(con_fd < 0)
{
printf("create socket failure : %s\n",strerror(errno));
return -1;
}
memset(&ser_addr,0,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SERVER_PORT);
inet_aton(SERVER_IP,&ser_addr.sin_addr);
if(connect(con_fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
{
printf("connect to server [%s:%d] failure :%s\n",SERVER_IP,SERVER_PORT,strerror(errno));
return -2;
}
if(write(con_fd,MSG_STR,strlen(MSG_STR)) < 0)
{
printf("Write data to server failure : %s\n",strerror(errno));
goto cleanup;
}
memset(buf,0,sizeof(buf));
if((rv = read(con_fd,buf,sizeof(buf))) < 0)
{
printf("Read data from server failure :%s\n",strerror(errno));
goto cleanup;
}
else if(rv == 0)
{
printf("client connect to server failure get disconnected\n");
goto cleanup;
}
printf("Read %d bytes data from server:'%s'\n",rv,buf);
cleanup:
close(con_fd);
}
3、运行结果
四、多进程函数编程函数详解
1、fork()系统调用
pid_t fork(void);
fork()系统调用会创建一个新的进程,这时它会有两次返回。一次返回是给父进程,其返回值是子进程的PID(Process ID),第二次返回是给子进程,其返回值为0。所以我们在调用fork()之后,需要通过其返回值来判断当前的代码是在父进程还是在子进程运行,如果返回值是0说明现在是子进程在运行,如果返回值>0说明现在是父进程在运行,而如果返回值< 0的话,说明fork()系统调用出错。
fork函数调用失败的原因主要有两个: 1.系统中已经有太多的进程 2.该实际用户ID的进程总数超过了系统限制
下面我们以一个简单的程序例子来讲解一下进程的创建过程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(int argc,char *argv[])
{
pid_t pid;
printf("Parent process PID[%d] start running...\n",getpid());
pid = fork();
if(pid < 0)
{
printf("fork() create child process failure:%s\n",strerror(errno));
return -1;
}
else if(pid == 0)
{
printf("child process PID[%d] start running,my parent PID is [%d]\n",getpid(),getppid());
return 0;
}
else
{
printf("Parent process PID[%d] continue running,and child process PID is [%d]\n",getpid(),pid);
return 0;
}
}
fork()
用于创建一个新的进程。在调用 fork()
之后,父进程将被复制一份,包括其内存空间、寄存器状态等,生成一个新的子进程。这两个进程在执行后续代码时是完全独立的,它们有各自的地址空间,并行运行。子进程有自己的独立的空间,子进程对内存的修改并不会影响父进程空间的相应内存。这时系统中出现两个基本完全相同的进程(父、子进程),这两个程序执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。如果需要确保让父进程或子进程先执行,则需要程序员在代码中通过进程间通信的机制来自己实现。
2、子进程的继承
在多进程编程中,子进程会继承父进程的很多资源,但也有一些是独立的。以下是子进程会继承的主要资源:
-
内存空间: 子进程将会复制父进程的地址空间。不过,这并不是立即进行的,而是通过写时复制(Copy-on-write)实现的。只有当父进程或子进程尝试修改内存中的数据时,才会进行实际的复制。
-
文件描述符: 子进程会继承父进程的打开的文件描述符。这包括文件、网络连接等。
-
进程组和会话 ID: 子进程会成为新的进程组的组长,并且会继承父进程的会话 ID。
-
信号处理: 子进程会继承父进程的信号处理方式,不过子进程可以通过系统调用修改它们。
-
当前工作目录: 子进程继承父进程的当前工作目录。
-
用户 ID 和组 ID: 子进程会继承父进程的用户 ID 和组 ID。
-
资源限制: 子进程会继承父进程的资源限制(如内存限制、打开文件数限制等)。
而以下是子进程独立的一些方面:
-
进程 ID: 子进程有自己的唯一的进程 ID。
-
父进程 ID: 子进程的父进程 ID 是父进程的进程 ID。
-
计时器: 子进程的计时器会被重置为零。
-
未处理的信号: 子进程的未处理信号集会被清空。
-
子进程可能会关闭一些不需要的文件描述符。
总体而言,子进程在创建时是父进程的副本,但在执行过程中可以根据需要进行修改,例如修改内存中的数据、更改信号处理方式等。因为写时复制的机制,子进程通常只在需要修改某个资源时才会真正复制。这有助于节省系统资源。
3、exec*()执行另外一个程序
在上面的例子中,我们创建了一个子进程都是让子进程继续执行父进程的文本段,但更多的情况下是让该进程去执行另外一个程序。这时我们会在fork()之后紧接着调用exec*()系列的函数来让子进程去执行另外一个程序。其中exec*()是一系列的函数,其原型为:
在Linux下我们可以使用ifconfig eth0来获取网卡的IP地址,但如果我们想在c程序代码里获取IP地址又该如何实现呢?我们在子进程里将标准输出重定向到文件里,这样命令的打印信息会输出到该文件中,之后父进程就可以从该文件中读出相应的内容并做相应的字符串解析,就可以获取到IP地址了。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
//标准输出重定向的文件,/tmp路径是在Linux系统在内存里做的一个文件系统,放在这里不用写硬盘程序运行会快些
#define TMP_FILE "/tmp/.ifconfig.log"
int main(int argc,char *argv[])
{
pid_t pid;
int fd = -1;
char buf[1024];
int rv;
FILE *fp;
char *ptr;
char *ip_start;
char *ip_end;
char ipaddr[16];
//父进程打开这个文件,子进程将会继承父进程打开的这个文件描述符,这样父子进程都可以通过各自的文件描述符访问同一个文件了
if((fd = open(TMP_FILE,O_RDWR|O_CREAT|O_TRUNC,0644)) < 0)
{
printf("Redirect standard output to file failure:%s\n",strerror(errno));
return -1;
}
//父进程开始创建进程
pid = fork();
if(pid < 0)
{
printf("fork() create child process failure:%s\n",strerror(errno));
return -2;
}
else if(pid == 0) //子程序开始运行
{
printf("Child process start excute ifconfig programe\n");
//子进程会继承父进程打开的文件描述符,此时子进程重定向标准输出到父进程所打开的文件里
dup2(fd,STDOUT_FILENO);
/*
下面这句execl(...)函数是让子进程开始执行带参数的ifconfig命令:ifconfig eth0
execl()会导致子进程彻底丢掉父进程的文本段、数据段,并加载在/sbin/ifconfig这个程序的文本段、数据段,重新建立进程内存空间。
execl()函数的第一个参数是所要执行程序的路径,ifconfig命令(程序)的路径是 /sbin/ifconfig;
接下来的参数是命令及其相关选项、参数,每个命令、选项、参数都用双引号(“ ”)扩起来,并以NULL结束。
*/
/*
ifconfig eth0 命令在执行时会将命令的执行结果输出到标注输出上,而此时子进程已经重定向标注输出到文件中去了,
所以ifconfig命令的打印结果会输出到文件中去,这样父进程就会从该文件里读到子进程执行改命令的结果;
*/
execl("/sbin/ifconfig","ifconfig","eth0",NULL);
/*excel()函数并不会返回,因为他去执行另外一个程序了。如果excel()返回了,说明该系统调用出错了。*/
printf("Child process excute another programe,will not return here.Return here means excel() error\n");
return -1;
}
else
{
//父进程等待3s,让子进程先执行
sleep(3);
}
//子进程因为调用了excel(),它会丢掉父进程的文本段,所以子进程不会执行执行到这里了。只有父进程会继续执行这后面的代码
memset(buf,0,sizeof(buf));
//父进程这时候是读不到内容的,因为子进程往文件里写内容时已经将文件偏移量修改到文件尾了
//父进程如果需要将文件偏移量设置到文件头才能读到内容
lseek(fd,0,SEEK_SET);
rv = read(fd,buf,sizeof(buf));
printf("Read %d bytes data after lseek:\n %s",rv,buf);
//如果使用read()读的话,一下子就读N多个字节进buf,但有时我们希望一行一行地读取文件的内容,这时可以使用fdopen()函数将文件描述符fd转成文件流fp
fp = fdopen(fd,"r");
fseek(fp,0,SEEK_SET); //重新设置文件偏移量到文件头
while(fgets(buf,sizeof(buf),fp)) //fgets()从文件里一下子读一行,如果读到文件尾则返回NULL
{
/*
包含IP地址的那一行含有netmask关键字,如果在该行中找到该关键字就可以从这里面解析出IP地址了
*/
if(strstr(buf,"netmask"))
{
//查找“inet关键字,inet关键字后面跟的就是IP地址
ptr = strstr(buf,"inet");
if(!ptr)
{
break;
}
ptr += strlen("inet");
//inet关键字后面是空白符,我们不确定是空格还是TAB,所以这里使用isblank()函数判断,如果字符还是空白符就往后跳过;
while(isblank(*ptr))
{
ptr++;
}
//跳过空白符后跟着的就是IP地址的起始字符;
ip_start = ptr;
//IP地址后面又是跟着空白字符,跳过所有的非空白字符,即IP地址部分:***.***.***.***
while(!isblank(*ptr))
{
ptr++;
}
//第一个空白字符的地址也就是IP地址终止的字符位置
ip_end = ptr;
//使用memcpy()函数将IP地址拷贝到存放的IP地址的buffer中,其中ip_end-ip_start就是IP地址的长度,ip_start就是IP地址的起始位置
memset(ipaddr,0,sizeof(ipaddr));
memcpy(ipaddr,ip_start,ip_end-ip_start);
break;
}
}
printf("Parser and get IP address :%s\n",ipaddr);
fclose(fp);
unlink(TMP_FILE);
return 0;
}
4、vfork()系统调用
pid_t vfork(void);
vfork()是另外一个可以用来创建进程的函数,他与fork()用法相同,也用于创建一个新进程。但vfork()并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit(),于是也就不会引用改地址空间了。不过子进程在调用exec()或exit()之前,他将在父进程的空间中运行,但如果子进程想尝试修改数据域都会带来未知的结果,因为他会影响了父进程空间的数据可能会导致父进程的执行异常。此外,vfork()会保证子进程先运行,在他调用了exec或exit之后父进程才可能被调度运行。如果子进程依赖于父进程的进一步动作,则导致死锁。
5、system()函数
如果我们在程序中,想执行另外一个Linux命令时,可以调用fork()然后再exec执行相应的命令即可,但这样相对比较麻烦。Linux系统提供了一个system()库函数,该函数可以快速创建一个进程来执行相应的命令。
int system(const char *command);
譬如我们想执行ping命令,则可以使用下面的程序片段:
system("ping -c 4 -I eth0 4.2.2.2");
//如果这里的eth0、4.2.2.2等是一个变量参数,我们则可以使用snprintf()格式化生成该命令:
char cmd_buf[256];
int count = 4;
char *interface = "eth0";
char *dst_ip = "4.2.2.2";
snprintf(cmd_buf,sizeof(buf),"ping -c %d -I %s %s",count,inerface,dst_ip);
system(cmd_buf);
6、popen函数
FILE *popen(const char *command, const char *type);
对于之前我们使用fork()+execl()函数来执行一个命令,并将该命令执行的结果写入到文件后再来读取的实现,这个过程相对比较麻烦,另外涉及到了创建文件和读取文件的过程。其实也有另外一个函数popen()可以执行一条命令,并返回一个基于管道(pipe)的文件流,这样我们可以从该文件中一行一行地解析了。相应的代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
int get_ipaddr(char *interface,char *ipaddr,int ipaddr_size);
int main(int argc,char *argv[])
{
char ipaddr[16];
char *interface = "eth0";
memset(ipaddr,0,sizeof(ipaddr));
if( get_ipaddr(interface,ipaddr,sizeof(ipaddr)) )
{
printf("ERROR:get IP address failure\n");
return -1;
}
printf("get network interface %s IP address [%s]\n",interface,ipaddr);
return 0;
}
int get_ipaddr(char *interface,char *ipaddr,int ipaddr_size)
{
char buf[1024];
char *ptr;
char *ip_start;
char *ip_end;
FILE *fp;
int len;
int rv;
if(!interface||!ipaddr||ipaddr_size<16)
{
printf("Invalid input arguments\n");
return -1;
}
memset(buf,0,sizeof(buf));
snprintf(buf,sizeof(buf),"ifconfig %s",interface);
if(NULL == ( fp = popen(buf,"r") ))
{
printf("popen() to excute command \"%s\" failure:%s\n",buf,strerror(errno));
return -2;
}
rv = -3;
while( fgets(buf,sizeof(buf),fp) )
{
if( strstr(buf,"netmask") )
{
ptr = strstr(buf,"inet");
if(!ptr)
{
break;
}
ptr += strlen("inet");
while(isblank(*ptr))
{
ptr++;
}
ip_start = ptr;
while(!isblank(*ptr))
{
ptr++;
}
ip_end = ptr;
memset(ipaddr,0,sizeof(ipaddr));
len = ip_end - ip_start;
len = len>ipaddr_size ? ipaddr_size : len;
memcpy(ipaddr,ip_start,len);
rv = 0;
break;
}
}
return rv;
}