高并发服务器编程之多进程并发服务器

时间:2022-01-16 17:56:26

同类基础博客:
基于Linux的SOCKET编程之TCP半双工Client-Server聊天程序
基于Linux的Socket编程之TCP全双工Server-Client聊天程序

一、多进程并发分析:

特点:
占用资源大,但是一个进程挂了不会影响另一个。这与多线程刚好相反,多线程服务器不稳定。

分析:
父进程循环accept,当父进程接收到链接请求之后,立即fork出一个新的子进程去处理通信,而父进程继续循环等待接收accept()(没有连接请求父进程则阻塞,但是不会影响到子进程通信)。而对于自己进程回收,父进程可以用一个单独的子进程去回收用于通信的子进程。子进程也可以自己fork出新的子进程与原进程分别处理读与写(发与收),以致于读写之间不受阻塞限制。
注意:子进程会继承父进程文件描述符,对于用不到的文件描述符listenfd需要关闭,并且父进程中在创建fork之后也需要关闭confd。防止文件描述符无意义的耗费过度。

结构图:
高并发服务器编程之多进程并发服务器

可能需要考虑的问题:
①子进程资源回收:如果客户端关闭,服务器相应的子进程则要结束,不能一直阻塞等待接收信息。
②不用的文件描述符要回收。
③对于回射式客户-服务器模型,并不需要交互式客户-服务器模型那样需要子进程创建新的进程去分别处理读写操作,那么结束一个进程自然不需要发送信号去通知另一个。交互式客户-服务器模型则必须要处理读写进程中任意一个结束,信号告知另外一个进程。即对于交互式模型,多进程处理多链接,而子进程又需要多进程分别处理读与写的不同操作不至于客户端为阻塞式,对于多进程的资源回收需要注意。

二、源代码基本实现:

文件关系:

高并发服务器编程之多进程并发服务器

1、Server端:

(1)、server_main.c:

/*server_main.c*/
#include<multiproc.h>
#include<server.h>

int main(char argc, char **argv)
{
if(argc < 3 ){
printf("Too few parameter!\n");
exit(EXIT_FAILURE);
}

socket_server_create(argv[1], argv[2]);
return 0;
}

(2)、server.c:

/*server.c*/
#include<multiproc.h>
#include<server.h>

void sys_err(const char * ptr_err)
{
perror(ptr_err);
exit(EXIT_FAILURE);
}
void socket_server_create(const char * ipaddr, const char * port)
{
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen;
int confd, ret_bind, ret_listen;
/*创建套接字socket、绑定bind、监听listen、接收accept等基本操作*/
int listenfd = socket(AF_INET, SOCK_STREAM, 0);

serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(ipaddr);
serveraddr.sin_port = htons(atoi(port));

if( (ret_bind = bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) < 0)
sys_err("bind");

if( (ret_listen = listen(listenfd, BACKLOG_SIZE)) < 0)
sys_err("listen");

while(1){//循环监听,以保证多进程可以并发
addrlen = sizeof(clientaddr);
again:
if( (confd = accept(listenfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0){
if ((errno == ECONNABORTED) || (errno == EINTR))//接收到的-1是因为无请求链接
goto again;
else
sys_err("accept");
}
fork_create(listenfd, confd, &clientaddr);//对接收到的客户端链接进行处理
}
close(listenfd);
}

void fork_create(const int listenfd, const int confd, const struct sockaddr_in * clientaddr)
{
pid_t pid = fork();
if(pid < 0)
sys_err("fork");
else if(pid > 0)
close(confd);//父进程关闭上一个接收到的链接的socket秒数符,继续循环accept
else{
close(listenfd);//关闭继承过来的无用的文件秒数符
deal_connect(confd, clientaddr);//子进程处理通信
close(confd);//推出前关闭文件描述符断开链接
exit(EXIT_SUCCESS);//子进程结束退出
}
}
void deal_connect(const int confd, const struct sockaddr_in * clientaddr)
{
pid_t pid = fork();
if(pid < 0)
sys_err("fork");
else if(pid == 0){//处理读:保存信息到日志文件中
write_logfile(clientaddr);//写日志
sleep(10);//模拟其他操作
/*信息写完后向父进程发送一个SIGCHILD结束信号*/
kill(SIGCHLD, getppid());
exit(EXIT_SUCCESS);
}
else{//处理写:回射登录状态信息
int ret_write;
signal(SIGCHLD, SIG_DFL);//处理子进程结束信号,采用默认处理方式(忽略),也可以自定义并将wait()写入自定义信号处理中
if( (ret_write = write(confd, "connect success!\n", 17)) < 0)//连接成功将链接成功的信息回射给客户端
sys_err("write connect success");
wait(NULL);//等待回收子进程资源
if( (ret_write = write(confd, "quit success!\n", 14)) < 0)//wait不再阻塞,表明子进程结束,通信结束,将结束信息会射到客户端
sys_err("write quit success");
}
}
/*处理些日志文件的操作*/
void write_logfile(const struct sockaddr_in * clientaddr)
{
int logfd = open("sersock.log", O_RDWR);//以可读可写的方式打开日志文件
if(logfd < 0)
sys_err("open sersock.log");

char addrbuf[ADDR_PORT_SIZE] = {};
/*将登录信息写入日志文件*/
lseek(logfd, 0, SEEK_END);
sprintf(addrbuf, "%s:%d ",inet_ntoa(clientaddr->sin_addr), ntohs(clientaddr->sin_port));//将IP地址与端口号拼接到一起
write(logfd, addrbuf,strlen(addrbuf));//将拼接好的地址信息写入日志文件

close(logfd);
}

(3)、server.h:

/*server.h*/
#ifndef _SERVER_H_
#define _SERVER_H_

#include<multiproc.h>
#include<fcntl.h>
#include<signal.h>
#include<sys/wait.h>

void sys_err(const char *);
void socket_server_create(const char *, const char *);
void fork_create(const int, const int, const struct sockaddr_in *);
void deal_connect(const int, const struct sockaddr_in *);
void write_logfile(const struct sockaddr_in *);

#endif

2、Client端:

(1)、client_main.c:

/*client_main.c*/
#include<multiproc.h>
#include<client.h>

int main(char argc, char ** argv)
{
if(argc < 3){
printf("Too few parameter!\n");
exit(EXIT_FAILURE);
}
socket_client_create(argv[1], argv[2]);
return 0;
}

(2)、client.c:

/*client.c*/
#include<multiproc.h>
#include<client.h>

void sys_err(const char * ptr_err)
{
perror(ptr_err);
exit(EXIT_FAILURE);
}
void socket_client_create(const char * ipaddr, const char * port)
{
struct sockaddr_in serveraddr;

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(ipaddr);
serveraddr.sin_port = htons(atoi(port));

int ret = connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret < 0)
sys_err("connect");

deal_connect(sockfd);
close(sockfd);
}

void deal_connect(const int sockfd)
{
char buf[BUF_SZIE] = {};
while(1){
bzero(&buf, strlen(buf));//每次清空
int ret = read(sockfd , buf, sizeof(buf));//循环读取服务器回射信息
if(ret < 0)
sys_err("read");
write(STDOUT_FILENO, buf, strlen(buf));//将接收到的回射信息写到标准输出上
if(strcmp(buf, "quit success!\n") == 0)//接收到的信息为“quit success!\n”时,表明服务器将客户端请求(sleep(10)模拟)信息处理完毕
break;
}
}

(3)、client.h:

/*client.h*/
#ifndef _CLIENT_H_
#define _CLIENT_H_

#include<multiproc.h>

void socket_client_create(const char *, const char *);
void deal_connect(const int);

#endif

3、其他文件:

(1)、multiproc.h:

/*multiproc.h*/
#ifndef _MULTI_PROCESS_SOCKET_
#define _MUTLI_PROCESS_SOCKET_

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/stat.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<errno.h>

/*数据传输接收缓冲区长度*/
#define BUF_SZIE 1024
/*监听队列长度*/
#define BACKLOG_SIZE 128
/*客户端地址信息缓冲区长度*/
#define ADDR_PORT_SIZE 40

#endif

(2)、Makefile:

/*Makefile*/
CPPFLAGS= -I ../ -I ./
CFLAGS= -g -Wall
#LDFLAGS=
CC= gcc

src = $(wildcard *.c)
obj = $(patsubst %.c,%.o,$(src))
#服务器端目标文件
target = server

#客户端目标文件
target = client
$(target):$(obj)
$(CC) $^ $(LDFLAGS) -o $@

%.o:%.c
$(CC) -c $< $(CFLAGS) $(CPPFLAGS) -o $@

.PHONY:clean
clean:
-rm -f server
-rm -f *.o

三、多进程并发功能与测试:

服务器启动后,连接多个客户端,便可在单独的终端:
netstat -apn | grep “port”查看连接状态等信息

我们可以看到在处理客户端链接时,服务器创建了多个进程进行处理:

高并发服务器编程之多进程并发服务器
对于图中三个进程,其关系如下:

高并发服务器编程之多进程并发服务器

我们cat查看一下log日志文件中的信息变化(可以看到几次测试时,系统为客户端分配的端口号是不一样的,服务器接收到了链接的客户端地址信息,并将其写入日志文件):

高并发服务器编程之多进程并发服务器

server-client的一些TCP链接状态(包含有之前测试的TIME_WAIT状态、服务器的LISTEN状态以及新的链接的ESTABLISHED通信状态、新链接断开时的TIME_WAIT状态等):

高并发服务器编程之多进程并发服务器

四、多进程并发需要注意的问题:

对于大型Socket项目中,客户端异常终止,服务器用于处理客户端的自己成还处于read阻塞,如何处理?

1、调用setsockopt()函数,让内核自己处理:

int setsockopt(int sockfd, int level, int optname, void * optval, socklen_t * optlen);//该函数能够设置TCP链接的存活属性
getsockopt(int sockfd, int level, int optname, void * optval, socklen_t optlen);

当设置连接的TCP存活属性之后,如果长时间(2小时左右(总之很长))客户端都没有向服务器发送数据请求(通信数据),那么服务器就会向客户端发送一个确认客户端是否异常断开链接的请求包,如果线路通常,客户端TCP/IP内核栈会自动发送一个携带RST复位信息的包,当服务器接收到RST复位信息后就知道客户端是异常结束的(如:Ctrl+C等),那么服务器就会自动断开该条链接。如果客户端是因为网络等问题结束的,服务器发送的包,客户端可能不会收到,也不会恢复服务器,那么服务器就重新发送,发送多次(十多次)以后,服务器依然没有收到回复,服务器就认为是客户端网络等问题导致的线路不通畅,就会关闭该条链接。服务器断开该条链接后,服务器端的阻塞的read,也会结束阻塞返回-1,并判断-1是因为连接断开返回的,而结束通信的子进程。

优点:方便
缺点:不能实时响应

2、C/S心跳机制:

服务器单独的进程/线程每隔几分钟(固定时间)发送一个心跳包给连接到的每个客户端,检测客户端是否存活。如果客户端无回复,重复发送几次,若还无回复则关闭链接。客户端也可以设置心跳进程,若果是服务器异常退出,客户端可以关闭并重新建立链接,如果重新建立几次失败,客户端就会向用户报告出错信息。(自己设置一个服务器的心跳包)。对于心跳机制,首先需要保存所有连接到服务器的客户端信息。