引子:
笔者在尝试建站。做软件者,自然最担心服务器程序崩溃;即便能重启,也要损失一些资源。这就产生了一种需要:
能否象数据可以备份从而损失时可以用于恢复一样,让服务器程序在崩溃情况下可以有个备份来用于恢复?读到过
Richard Stevens在《UNIX环境高级编程》中论述过的“传递文件描述符”,这种强大的功能象进程之间的dup/dup2有象
生成备份的意思,让我产生了想法:使用它来实现“进程备份”;当然了,这个不是备份一个进程“映像”,而是“备份”
一个进程中的文件描述符,包括我希望处理的网络连接的套接字。写了个小程序试了一下,成功。还有待于在大规模
服务程序中试用验证。
下面试验的思路是:
(1)模拟两客户进程“聊天”;
S
/ \
C1 C2
(2)对Server进行“进程备份”;
S -> B
(3)Master发指令,原Server "crash";
(4)发现crash,启动新进程,监听原端口(比如websocket聊天端口);
(5)为新进程准备好fd、用户关系等资源;
(6)看两客户clients还能否继续聊天。
源码:
server_crash_backup-restore_test_2.tgz 下载
该试验代码未加整理,:-(。
该tgz文件解压缩后目录结构如下:
目录reuse_test/ 、 pass_fd_and_struct_test/可以删去。建议保留空目录tmp/,用途见下。
用到四个程序:master( server)、(data )server、(data )client、backup_process,各使用
一个单独的目录。
我们会用两个data client进程。我这儿意为两个聊天客户(data client)通过聊天服务器
(data server)通信,担心聊天服务器崩溃,将其向backup_process进程“备份”,master则是
试验时用来发指令的,每个发出的指令会发给所有slave,每个slave选择自己需要处理的指令
执行相应的动作。
master外的其他程序都是master的slave/client,(data )client是(data )server的client,
data server又是backup_process的client。
先进入各个目录运行make编译。
由于backup进程和data server这里通过(有名)UNIX域套接字通信,要指定路径,在
backup_process/client.cpp中,改
const char* path="/usr/working3/server_crash_backup-restore_test/tmp/tmp.sock"
为你指定的路径,我在百度BCC上测试,改为
const char* path="/usr/working/test/server_crash_backup-restore_test_2/tmp/tmp.sock";
在server_test/client.cpp中作同样改动。
再编译。
启动master,
(cd master-server_test ;)
./server_crash_test-master 5000 3
参数3表示master开始将有3个slave,两个data client,
一个data server。
启动(data )server,
(cd server_test/ ;)
./server_test 5001
启动clients:
(cd client_test;)
./client_test 127.0.0.1 5001 c1 (客户1)
./client_test 127.0.0.1 5001 c2 (客户2)
这时master界面上提示等待输入指令。(所有指令均由master发出,不区分大小写字母。)
>
输入若干1to2 / 2to1 ,(模拟client 1和client 2聊天,) 收到消息的client端会显示
接收到消息的序号(#1、#2……)。(输入下一条指令时可能需要回车一下显示提示符。)
现在启动backup进程,
>create
应显示backup process started OK及已连接上master。 master又多了一个slave。
进行备份,
>backup
然后来观察server crash,
>crash
观察server界面,应有segmentation fault (core dumped)。
>restart 重启server(以参数restart:这次data server不再accept连接)
应有new data server started OK. ps命令观察有server进程重新出现。
>restore
恢复
再在>提示符下输入1to2 / 2to1指令,看到clients之间能继续正常收发数据,好象没有
server crash一样。
退出试验:
>exit
下面顺序详述上面的过程及指令使用:
启动Master,slaves 3个;
启动Server,accept 2个连接;
以参数C1启动Client 1(简称C1),连接Server,Server建立结构体信息;
以参数C2启动Client 2(简称C2),连接Server,Server建立结构体信息;
server_test\server.cpp:
data_sock = server( service);
data_sock2 = server( service);
fd_info[0].fd = data_sock;
fd_info[0].owner = 1;
fd_info[0].target = 2;
fd_info[0].paired = 1;
fd_info[0].fd_orig = data_sock;
fd_info[1].fd = data_sock2;
fd_info[1].owner = 2;
fd_info[1].target = 1;
fd_info[1].paired = 1;
fd_info[1].fd_orig = data_sock2;
1TO2/2TO1用来指示Server和Client(指令目标):
1TO2 :Server准备从C1接收;C1发给Server;Server寻找C2;Server发给C2;C2准备接收;C2接收;
2TO1 :Server准备从C2接收;C2发给Server;Server寻找C1;Server发给C1;C1准备接收;C1接收;
这里是些普通的socket通信的代码。
CREATE :指令目标:Master自身。
(Master)启动Backup进程。
master-server_test\server.cpp:
if(!strcasecmp(buf, "CREATE" ))
{
int pid = fork();
......
if(pid == 0)
{
execl("../backup_process/backup_process", "backup_process", NULL);
......
}
}
BACKUP :指令目标:Server和Backup进程。
Server发送结构体信息给Backup进程;(后者整理信息;)
代码后述。
CRASH :指令目标:Server。
RESTART:指令目标:Master自身。
(Master)启动新Server;(以参数restart:以不再进入
老Server启动后accept连接的代码;)
master-server_test\server.cpp:
if(!strcasecmp(buf, "RESTART" ))
{
int pid = fork();
......
if(pid == 0)
{
......
execl("../server_test/server_test", "server_test", "2000","restart", NULL);
......
}
}
server_test\server.cpp:
if (!strcasecmp(argv[argc - 1], "restart"))
;
else
......
RESTORE:指令目标:Backup进程和新Server。
Backup进程发送结构体信息给新Server;(后者整理信息;)
代码后述。
1TO2/2TO1:如上。继续观察聊天正常与否。
(代码中还可见两个指令CTOS/STOC,这是老版本中用的,类似1TO2/2TO1,这里不用。)
仅仅传递fd是不够的,无法分清哪个是哪个;还好使用sendmsg/recvmsg可以传递结构信息。
data server向backup_process备份的信息是包括文件描述符fd在内的结构,在server_test/、backup_process/ 内均有
pass_fd_struct_rw.h
typedef struct{
SOCKET fd; //socket fd
int owner; //client id
int target; //chating peer
int paired; //is it a chat pair?
int fd_orig; //some original information
}fd_s;
extern fd_s fd_info[2];
现在来看BACKUP和RESTORE:
BACKUP:
(data )Server端:
server_test\client.cpp:
if(!strcasecmp(buf, "BACKUP"))
{
......
int ret = to_backup(data_sock );
if(ret < 0)
printf("backup 1 error!\n");
sleep(3);
ret = 0;
ret = to_backup(data_sock2);
if(ret < 0)
printf("backup 2 error!\n");
}
server_test\client.cpp:
int to_backup(SOCKET fd_to_backup)
{
......
ret = write_fd_struct(sockfd, (fd_to_backup == fd_info[0].fd)? &fd_info[0] : &fd_info[1] , \
sizeof(fd_s) );
......
}
Backup process端:
backup_process\client.cpp:
if(!strcasecmp(buf, "BACKUP"))
{
......
for(int i = 0; i < 2; i++)
{
int ret = do_backup();
if(ret < 0)
fprintf(stderr, "do_backup() %d return FAIL.\n", i+1);
else
printf("got passed fd seems OK.\n");
if(i == 0)
fd_backup = ret;
else
fd_backup2 = ret;
}
}
backup_process\client.cpp:
SOCKET do_backup()
{
......
static int count = 0;
ret = read_fd_struct(fdaccept, &fd_info[count], sizeof(fd_s));
++count;
......
return ret;
}
两个读写函数在pass_fd_struct_rw.cpp中实现,在Server和Backup程序中都有,是一样的。由网上代码(见下面参考网址)稍加改写而成。
此处可参见子目录pass_fd_and_struct_test/ 下的单独测试读写的代码及server_test/ 或backup_process/ 下的pass_fd_rw.c。
pass_fd_struct_rw.cpp:
#include "pass_fd_struct_rw.h"
int write_fd_struct(int sock, fd_s* data, int size)
{
msghdr msg;
// init msg_control
if(data->fd == -1){
msg.msg_control = NULL;
msg.msg_controllen = 0;
}
else{
union {
struct cmsghdr cm;
char space[CMSG_SPACE(sizeof(int))];
} cmsg;
memset(&cmsg, 0, sizeof(cmsg));
cmsg.cm.cmsg_level = SOL_SOCKET;
cmsg.cm.cmsg_type = SCM_RIGHTS; // we are sending fd.
cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
msg.msg_control = (cmsghdr*)&cmsg;
msg.msg_controllen = sizeof(cmsg);
*(int *)CMSG_DATA(&cmsg.cm) = data->fd;
}
// init msg_iov
iovec iov[1];
iov[0].iov_base = data;
iov[0].iov_len = size;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
// init msg_name
msg.msg_name = NULL;
msg.msg_namelen = 0;
if (sendmsg(sock, &msg, 0) == -1){
cout << "[write_fd_struct] sendmsg error" << endl;
return (-1);
}
return 0;
}
int read_fd_struct(int sock, fd_s* data, int size)
{
msghdr msg;
// msg_iov
iovec iov[1];
iov[0].iov_base = data;
iov[0].iov_len = size;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
// msg_name
msg.msg_name = NULL;
msg.msg_namelen = 0;
// msg_control
union { // union to create a 8B aligned memory.
struct cmsghdr cm; // 16B = 8+4+4
char space[CMSG_SPACE(sizeof(int))]; // 24B = 16+4+4
} cmsg;
memset(&cmsg, 0, sizeof(cmsg));
msg.msg_control = (cmsghdr*)&cmsg;
msg.msg_controllen = sizeof(cmsg);
if (recvmsg(sock, &msg, 0) == -1) {
cout << "[read_fd_struct] recvmsg error" << endl;
return (-1);
}
#if 1
printf( "recvmsg() ends, data is: fd:%d\n", data->fd);
#endif
data->fd = *(int *)CMSG_DATA(&cmsg.cm);
//int fd = *(int *)CMSG_DATA(&cmsg.cm);
#if 1
printf( "recvmsg() ends, after pass, fd turns to:%d, different?\n", data->fd);
#endif
return data->fd;
//return fd;
}
RESTORE:与BACKUP读写方向相反。
Backup process端:
backup_process\client.cpp:
if(!strcasecmp(buf, "RESTORE"))
{
......
int ret = do_restore(fd_backup2);
if(ret < 0)
fprintf(stderr, "do_restore() II return FAIL.\n");
ret = do_restore(fd_backup);
if(ret < 0)
fprintf(stderr, "do_restore() I return FAIL.\n");
}
backup_process\client.cpp:
int do_restore(SOCKET fd)
{
......
ret = write_fd_struct(fdaccept, (fd == fd_info[0].fd)? &fd_info[0] : &fd_info[1] , \
sizeof(fd_s) );
......
}
(data )Server端:
server_test\client.cpp:
if(!strcasecmp(buf, "RESTORE"))
{
......
for(int i = 0; i < 2; i++)
{
sleep(3);
int fd_restore = to_restore();
if(fd_restore < 0)
fprintf(stderr, "to_restore() return FAIL.\n");
}
data_sock = fd_info[0].owner == 1 ? fd_info[0].fd : fd_info[1].fd ;
data_sock2 = fd_info[0].owner == 2 ? fd_info[0].fd : fd_info[1].fd ;
*p_data_sock[0] = data_sock ;
*p_data_sock[1] = data_sock2;
}
server_test\client.cpp:
int to_restore()
{
......
static int count = 0;
ret = read_fd_struct(sockfd, &fd_info[count], sizeof(fd_s));
++count;
......
return ret;
}
传递描述符由sendmsg/recvmsg完成,书中有论述,网上也有资料提供,这里列举一些网址:
如何在进程之间传递文件描述符
高级进程间通信之传送文件描述符
进程间传递描述符一
进程间传递描述符二
进程间传递描述符三
进程间传递文件描述符或
这里
使用Unix域套接字在进程间传递文件描述符
在《UNIX环境高级编程》中讲了,《TCP/IP详解》第3卷讲了其实现。《UNIX网络编程》第1卷也讲到了。其他书如《Linux网络编程》宋敬彬、
Linux Socket Programming By Example by Warren Gay(中文版名:《实战Linux socket编程》)、《Linux高级程序设计》杨宗德。
据说Windows上有类似机制:DuplicateHandle、WSADuplicateSocket。
此外,为了使服务器程序能立即重启,socket使用了SO_REUSEADDR选项。
server_test\server.cpp
int opt_val = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val));
代码在Redhat 9和Ubuntu 14上均试验通过。(cygwin上虽然可以编译,但无Linux上传递描述符的功能。)
当然最好是写的服务器程序毫无bug,绝不崩溃。但如果实现本文的想法,会是一个强大的工具。