井发服务器端的实现方法
- 多进程服务器: 通过创建多个进程提供服务。
- 多路复用服务器: 通过捆绑并统一管理I/O对象提供服务。
- 多线程服务器:通过生成与客户端等量的线程提供服务。
创建进程:
#include <unistd.h>
pid_t fork(void);
//成功时返回进程ID, 失败时返回-1。
销毁僵尸进程1 : 利用wait 函数
#include <sys/wait.h>
pid_t wait(int * statloc);
//成功时返回终止的子进程ID, 失败时返回-1 。
实例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status ;
pid_t pid=fork();
if(pid==0)
{
return 3;//第一个子进程退出
}
else
{
printf("Child PID: %d \n", pid);
pid=fork();
if(pid==0)
{
exit(7);//第二个子进程退出
}
else
{
printf("Child PID: %d \n", pid);
wait(&status);
if(WIFEXITED(status))
printf("Child send one: %d \n", WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status))
printf("Child send two : %d \n" , WEXITSTATUS(status));
sleep(5); // Sleep 30 sec.
}
}
return 0;
}
运行结果:
$ ./a.out
Child PID: 27242
Child PID: 27243
Child send one: 3
Child send two : 7
销毁僵尸进程2: 使用waitpid 函数
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* statloc, int options);
//成功时返回终止的子进程ID (或0)' 失败时返回-1 。
#pid 等待终止的目标子进程的ID, 若传递-1 , 则与wait函数相同,可以等待任意子进程终止。
#statloc 与wait函数的statloc参数具有相同含义。
#options 传递头文件sys/wait.h 中声明的常量WNOHANG, 即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。
实例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid=fork();
if(pid==0)
{
sleep(9) ;
return 24;
}
else
{
while(!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status))
{
printf("Child send %d \n", WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
$ ./a.out
sleep 3sec.
sleep 3sec.
sleep 3sec.
Child send 24
————————————————————————————————————————————
信号处理
函数原型:signal
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
函数传入值
signum: 指定信号代码
handler:
- SIG_IGN: 忽略该信号
- SIG_DFL: 采用系统默认方式处理信号
- 自定义的信号处理函数指针
函数返回值
成功: 以前的信号处理配置
出错: 1
函数原型:sinaction
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数传入值
signum: 信号代码, 可以为除 SIGKILL 及 SIGSTOP 外的任何一个特定有效的信号
act: 指向结构 sigaction 的一个实例的指针, 指定对特定信号的处理
oldact: 保存原来对相应信号的处理函数返回值
成功: 0
出错: 1
首先给出了 sigaction 的定义, 代码如下:
struct sigaction {
void (*sa_handler)(int);//函数指针
void (*sa_sigaction)(int, siginfo_t *, void *);//带参数的函数指针
sigset_t sa_mask;//信号屏蔽集
int sa_flags;//标志位
void (*sa_restorer)(void);//现在已经不使用了
};
利用信号处理技术消灭僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void my_function(int arg)
{
int status;
pid_t id=waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status))
{
printf("Removed proc id: %d \n", id);
printf ("Child send: %d \n", WEXITSTATUS(status));
}
}
int main(void)
{
pid_t pid;
int stat;
int i;
struct sigaction new;
struct sigaction old;
new.sa_handler = my_function;
new.sa_flags = 0;
sigemptyset(&new.sa_mask);
sigaction(SIGCHLD, &new, NULL);
pid=fork();
if(pid == 0)
{
sleep(2);
return 1;
}
else
{
printf("Child(1) : proc id: %d \n", pid);
pid=fork();
if(pid == 0)
{
sleep(4);
exit(2);
}
else
{
printf("Child(2) : proc id: %d \n", pid);
for(i=0; i<5; i++)
{
puts("wait ... ");
sleep(5);
}
/* linux信号会终止休眠,所以要用上面的多次休眠
sleep(10);
printf("aaaaaaaa\n");*/
}
}
exit(0);
}
运行结果:
$ ./a.out
Child(1) : proc id: 28237
Child(2) : proc id: 28238
wait ...
Removed proc id: 28237
Child send: 1
wait ...
Removed proc id: 28238
Child send: 2
wait ...
wait ...
wait ...
基于多任务的井发服务器
- 第一阶段: 回声服务器端(父进程)通过调用accept函数受理连接请求。
- 第二阶段: 此时获取的套接字文件描述符创建并传递给子进程。
- 第三阶段:子进程利用传递来的文件描述符提供服务。
实例代码:
#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 *message);
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);
//设置sock
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("removed proc id : %d \n", pid);
}
void error_handling(char * message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
$ ./a.out 9190
new client connected .. .
client disconnected ...
removed proc id : 28460
new client connected .. .
client disconnected ...
removed proc id : 28462
• 第29~32行为防止产生僵尸进程而编写的代码。
• 第47 、52行第47行调用accept 函数后,在第52行调用fork 函数。因此,父子进程分别带有1 个第47行生成的套接字(受理客户端连接请求时创建的)文件描述符。
• 第58~66行子进程运行的区域。此部分向客户端提供回声服务。第60行关闭第33行创建的服务器套接字,这是因为服务器套接字文件描述符同样也传递到子进程。关于这—点稍后将单独讨论。
• 第69行第47 行中通过accept函数创建的套接字文件描述符己复制给子进程,因此服务器端需要销毁自己拥有的文件描述符。关于这一点稍后将单独说明。
——————————————————————————————————————
通过fork 函数复制文件描述符
示例中给出了通过fork函数复制文件描述符的过程。父进程将2个套接字(一个是服务器端套接字,另一个是与客户端连接的套接字)文件描述符复制给子进程。
“只复制文件描述符吗?是否也复制了套接宇呢?”
文件描述符的实际复制多少有些难以理解。调用fork函数时复制父进程的所有资源, 有些人可能认为也会同时复制套接字。但套接字并非进程所有————从严格意义上说,套接字属于操作系统————只是进程拥有代表相应套接字的文件描述符。也不一定非要这样理解,仅因为如下原因,复制套接字也并不合理。
“复制套接字后,同一端口将对应多个套接宇。”
示例中的fork函数调用过程如图所示。调用fork 函数后, 2个文件描述符指向同一套接字。
如图所示, 1 个套接字中存在2个文件描述符时,只有2个文件描述符都终止(销毁)后,才能销毁套接字。如果维持图中的连接状态,即使子进程销毁了与客户端连接的套接字文件描述符,也无法完全销毁套接字(服务器端套接字同样如此)。因此,调用fork 函数后, 要将无关的套接字文件描述符关掉,如图所示。
————————————————————————————————————————————————
分割客户端的I/O程序
分割I/O程序的优点
我们已经实现的同声客户端的数据回声方式如下:
“向服务器端传输数据,并等持服务器端回复。无条件等待,直到接收完服务器端的回声数据后,才能传输下一批数据。”
传输数据后需要等待服务器端返回的数据,因为程序代码中重复调用了read和write函数。只能这么写的原因之一是, 程序在1 个进程中运行。但现在可以创建多个进程,因此可以分割数据收发过程。默认的分割模型如图所示。
从下图可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样. 无论客户端是否从服务器端接收完数据都可以进行传输。
选择这种实现方式的原因有很多,但最重要的一点是,程序的实现更加简单。也许有人质疑:
既然多产生1个进程,怎么能简化程序实现呢?其实, 按照这种实现方式,父进程中只需编写接收数据的代码, 子进程中只需编写发送数据的代码,所以会简化。实际上,在1个进程内同时实现数据收发逻辑需要考虑更多细节。程序越复杂,这种区别越明显,它也是公认的优点。
分割I/O程序的另一个好处是,可以提高频繁交换数据的程序性能。
上图左侧演示的是之前的回声客户端数据交换方式,右侧演示的是分割VO后的客户端数据传输方式。服务器端相同,不同的是客户端区域。分割I/O后的客户端发送数据时不必考虑接收数据的情况,因此可以连续发送数据,由此提高同一时间内传输的数据量。这种差异在网速较慢时尤为明显。
实例代码:
#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 *message);
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 I");
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 * message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}