服务器按处理方式可以分为迭代服务器和并发服务器两类。
简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,实现简单但效率很低,通常这种服务器被称为迭代服务器。
一个服务器具有同时处理多个客户请求的能力,其效率很高却实现复杂,这种称为并发服务器,在实际应用中,并发服务器应用的最广泛。
Linux有3种实现并发服务器的方式:多进程并发服务器,多线程并发服务器,IO复用。
Linux下的创建进程
在创建新进程时,要进行资源拷贝。Linux 有三种资源拷贝的方式:
- 共享:新老进程共享通用的资源。当共享资源时,两个进程共同用一个数据结构,不需要为新进程另建。
- 直接拷贝:将父进程的文件、文件系统、虚拟内存等结构直接拷贝到子进程中。子进程创建后,父子进程拥有相同的结构。
- Copy on Write:拷贝虚拟内存页是相当困难和耗时的工作,所以能不拷贝就最好不 要拷贝,如果必须拷贝,也要尽可能地少拷贝。为此,Linux 采用了 Copy on Write 技术,把真正的虚拟内存拷贝推迟到两个进程中的任一个试图写虚拟页的时候。如 果某虚拟内存页上没有出现写的动作,父子进程就一直共享该页而不用拷贝。
进程创建函数fork与vfork
下面介绍创建新进程的两个函数:fork()和 vfork()。
其中,fork 用于普通进程的创建,采用的是 Copy on Write 方式;而 vfork 使用完全共享的创建,新老进程共享同样的资源,完全没有拷贝。
● fork函数原型如下:
#include <unistd.h>
pid_t fork (void);
函数调用失败会返回-1。fork 函数调用失败的原因主要有两个:
- 系统中已经有太多的进 程;
- 该实际用户 ID 的进程总数超过了系统限制。
而如果调用成功,该函数调用会在父子进程中分别返回一次。在调用进程也就是父进程中,它的返回值是新派生的子进程的 ID 号,而在子进程中它的返回值为 0。因此可以通过返回值来区别当前进程是子进程还是父进程。
为什么在 fork 的子进程中返回的是 0,而不是父进程 id 呢?
原因在于:没有子进程都只 有一个父进程,它可以通过调用 getppid 函数来得到父进程的 ID,而对于父进程,它有很多 个子进程,他没有办法通过一个函数得到各子进程的ID。如果父进程想跟踪所有子进程的ID, 它必须记住 fork 的返回值。
● vfork函数原型如下:
#include <unistd.h>
pid_t vfork (void);
vfork 是完全共享的创建,新老进程共享同样的资源,完全没有拷贝。当使用 vfork()创 建新进程时,父进程将被暂时阻塞,而子进程则可以借用父进程的地址空间运行。这个奇特 状态将持续直到子进程要么退出,要么调用 execve(),至此父进程才继续执行。
fork与vfork的区别
1.vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
2.fork要拷贝父进程的进程环境;而vfork则不需要完全拷贝父进程的进程环境,在子进程没有调用exec和exit之前,子进程与父进程共享进程环境,相当于线程的概念,此时父进程阻塞等待。
为什么会有vfork呢?
因为以前的fork当它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,然后将会有两种行为:
1.执行从父进程那里拷贝过来的代码段
2.调用一个exec执行一个新的代码段
当进程调用exec函数时,一个新程序替换了当前进程的正文,数据,堆和栈段。这样,前面的拷贝工作就是白费力气了,这种情况下,聪明的人就想出了vfork。vfork并不复制父进程的进程环境,子进程在父进程的地址空间中运行,所以子进程不能进行写操作,并且在儿子“霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。
因此,如果创建子进程是为了调用exec执行一个新的程序的时候,就应该使用vfork。
并发服务器端:echo_mpserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *messages);
void read_childproc(int sig);
int main(int argc,char *argv[]){
int serv_sock,clnt_sock;
struct sockaddr_in serv_adr,clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len,state;
char buf[BUF_SIZE];
if(argc!=2){
printf("Usage: %s <port>\n",argv[0]);
exit(1);
}
act.sa_handler=read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
state=sigaction(SIGCHLD,&act,0);
serv_sock=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("bind()error");
if(listen(serv_sock,5)==-1)
error_handling("listen()error");
while(1){
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
if(clnt_sock==-1)
continue;
else
puts("new client connected...");
pid=fork();
if(pid==-1){
close(clnt_sock);
continue;
}
if(pid==0){
close(serv_sock);
while((str_len=read(clnt_sock,buf,BUF_SIZE))!=0)
write(clnt_sock,buf,str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig){
pid_t pid;
int status;
pid=waitpid(-1,&status,WNOHANG);
printf("remove proc id: %d \n",pid);
}
void error_handling(char * messages){
fputs(messages,stderr);
fputc('\n',stderr);
exit(1);
}
运行:
# gcc echo_mpserv.c -o mpserv
./mpserv 9190
分割TCP的I/O的程序
一般回声客户端的数据回声方式:
“向服务器端传输数据,并等待服务器端回复。无条件等待,直到接收完服务器端的回声数据后,才能传输下一批的数据。”
传输数据后需要等待服务器端返回的数据,因为程序代码中重复调用了read和write函数。只能这么写的原因之一是,程序在一个进程中运行。但现在可以创建多个进程,因此可以分割数据收发过程。
即,客户端的父进程负责接收数据,额外创建的子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样,无论客户端是否从服务器端接收完数据都可以进行传输。
(实际上,回声客户端不用分割I/O程序,因为数据的收发逻辑需要考虑更多的细节。在此,只是明白分割I/O的方而选取回声客户端)
分割I/O程序的另一个优点是,可以提高频繁交换数据的程序性能。尤其是在网速较慢时尤为明显。
客户端:echo_mpclient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling( char *messages);
void read_routine(int sock,char *buf);
void write_routine(int sock,char *buf);
int main(int argc,char *argv[]){
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc!=3){
printf("Usage:%s <IP><port>\n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("connect() error");
pid=fork();
if(pid==0)
write_routine(sock,buf);
else
read_routine(sock,buf);
close(sock);
return 0;
}
void read_routine(int sock,char *buf){
while(1){
int str_len=read(sock,buf,BUF_SIZE);
if(str_len==0)
return;
buf[str_len]=0;
printf("message from server:%s",buf);
}
}
void write_routine(int sock,char*buf){
while(1){
fgets(buf,BUF_SIZE,stdin);
if(!strcmp(buf,"q\n")||!strcmp(buf,"Q\n")){
shutdown(sock,SHUT_WR);
return;
}
write(sock,buf,strlen(buf));
}
}
void error_handling(char *messages)
{
fputs(messages,stderr);
fputc('\n',stderr);
exit(1);
}
运行:
# gcc echo_mpclient.c -o mpclient
./mpclient 127.0.0.1 9190
部分内容参考koma丶的https://blog.csdn.net/qq_29227939/article/details/53771782
部分内容参考尹圣雨《TCP/TP 网络编程》