【Linux】高并发服务器模型(多进程模型和多线程模型)

时间:2021-07-04 00:06:12

多进程并发服务器

使用多进程并发服务器时要考虑以下几点:

            1.      父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)

            2.      系统内创建进程个数(与内存大小相关)

            3.      进程创建过多是否降低整体服务性能(进程调度)

                     服务端 server

/*
多进程实现高并发服务器
*/

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#include<strings.h>
#include<string.h>
#include<ctype.h>


#include"wrap.h"

#define SERV_PORT 8000

// “192.168.170.128”

void wait_child(int signo)
{
while(waitpid(0,NULL,WNOHANG)); // 进行非阻塞轮询回收子进程

return;
}


int main(void)
{
int lfd,cfd;
int i,n;
pid_t pid;
struct sockaddr_in serv_addr,clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ];
char str[BUFSIZ];

lfd = Socket(AF_INET,SOCK_STREAM,0);

bzero(&serv_addr,sizeof(serv_addr));

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET,"192.168.170.132",&serv_addr.sin_addr.s_addr);

Bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));

Listen(lfd,128);

printf("Accept conection ...\n");
while(1)
{

clie_addr_len = sizeof(clie_addr);

cfd = Accept(lfd,(struct sockaddr*)&clie_addr,&clie_addr_len);

pid = fork();
if( pid < 0 )
{
perror("fork error");
exit(1);
}
else if(pid == 0 )
{
close(lfd);
break;
}
else
{
close(cfd);
signal(SIGCHLD,wait_child);
}
}

if(pid == 0)
{
while(1)
{
n = Read(cfd,buf,sizeof(buf));
if( n == 0 )
{
printf("the other side has been closed\n");
break;
}

printf("receive from %s at %d\n",
inet_ntop(AF_INET,&clie_addr.sin_addr,str,sizeof(str)),
ntohs(clie_addr.sin_port));

/*
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
*/
for(i=0;i<n;i++)
buf[i] = toupper(buf[i]);
write(cfd,buf,n);
}
}
else if(pid>0)
{
Close(lfd);
}
else
{
perr_exit("fork");
}

return 0;
}

       

          客户端 client

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

#include"wrap.h"

#define SERV_PORT 8000
#define MAXLINE 8192

int main(void)
{
int sfd;
struct sockaddr_in serv_addr;

char buf[MAXLINE];

int n;

// 创建和服务端通信的套接字

sfd = Socket(AF_INET,SOCK_STREAM,0);

// 给 socket 结构体清零
bzero(&serv_addr,sizeof(serv_addr));

// 给 serv_addr 结构体初始化
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr.s_addr);

// 连接服务器端
Connect(sfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));

while(fgets(buf,MAXLINE,stdin) != NULL)
{
// 把从键盘读到的数据发送给服务器

Write(sfd,buf,strlen(buf));

// 从服务器端接收数据

n = Read(sfd,buf,MAXLINE);

if( n == 0 )
{
printf("the other side has been closed\n");
break;
}
else
Write(STDOUT_FILENO,buf,strlen(buf));
}

return 0;
}

多线程并发服务器

在使用线程模型开发服务器时需考虑以下问题:

           1.      调整进程内最大文件描述符上限

           2.      线程如有共享数据,考虑线程同步

           3.      服务于客户端线程退出时,退出处理。(退出值,分离态)

           4.      系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU

                     服务端 server

 

/*
多线程 实现 高并发服务器
*/

#include<unistd.h>
#include<pthread.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<stdio.h>
#include<string.h>
#include<strings.h>
#include<stdlib.h>
#include<ctype.h>

#include"wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

// 定义一个结构体,将地址结构和 cfd 绑定在一起

struct s_info
{
struct sockaddr_in cliaddr;
int cfd;
};

void *do_work(void *arg)
{
int n,i;
struct s_info *ts = (struct s_info*)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; // #define INET_ADDRSTRLEN 16 可用 [+d 来查看

while(1)
{
// 读客户端
n = Read(ts->cfd,buf,MAXLINE);
if( n == 0 )
{
printf("the client %d closed ...\n",ts->cfd);
break; // 退出循环,关闭 cfd
}

// 打印客户端信息(IP/PORT)

printf("recieve from %s at prot %d\n",inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr,str,sizeof(str)),
ntohs((*ts).cliaddr.sin_port));

for(i=0;i<n;i++)
buf[i] = toupper(buf[i]);


// 把数据写到屏幕上
Write(STDOUT_FILENO,buf,n);

// 把转换后的数据发送给客户端
Write(ts->cfd,buf,n);
}

// 关闭和客户端连接的套接字
Close(ts->cfd);

return (void*)0;
}

int main(void)
{
struct sockaddr_in serveraddr,cliaddr;
socklen_t cliaddr_len;
int listenfd,connfd;
pthread_t tid;
struct s_info ts[256]; // 根据最大线程数,创建结构体数组
int i = 0;

// 创建一个 socket 得到 lfd;

listenfd = Socket(AF_INET,SOCK_STREAM,0);

// socket 通信的数据结构体 serveraddr 清空

bzero(&serveraddr,sizeof(serveraddr));

// 初始化地址结构体
serveraddr.sin_family = AF_INET; // 规定通信协议为 IPV4
serveraddr.sin_port = htons(SERV_PORT); // 指定端口号为 8000
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定本地任意 IP 地址

Bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)); // 绑定

Listen(listenfd,128); // 设置同一时刻连接服务器的上限数

printf("Accept client connect ...\n");

while(1)
{
cliaddr_len = sizeof(cliaddr);

// 阻塞监听客户端请求
connfd = Accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddr_len);
ts[i].cliaddr = cliaddr;
ts[i].cfd = connfd;

// 达到线程最大数时,pthread_create 出错处理,增加服务器的稳定性
pthread_create(&tid,NULL,do_work,(void*)&ts[i]);

pthread_detach(tid); // 子线程分离,防止僵尸线程产生

i++;
}

return 0;
}

         客户端 client


/*
客户端
*/

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>


#include"wrap.h"

#define SERV_PORT 8000
#define MAXLINE 80


int main(void)
{
int sfd;
struct sockaddr_in serv_addr;
char buf[MAXLINE];
int n;

sfd = Socket(AF_INET,SOCK_STREAM,0);

bzero(&serv_addr,sizeof(serv_addr));

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr.s_addr);

Connect(sfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));

while( fgets(buf,MAXLINE,stdin) != NULL)
{
// 把键盘读到的 数据写入到服务器中
Write(sfd,buf,strlen(buf));

// 从服务器读取数据

n = Read(sfd,buf,MAXLINE);

if( n == 0 )
printf("the other side has been closed ...\n");
else
Write(STDOUT_FILENO,buf,n);
}
Close(sfd);

return 0;
}

注: 

             为了减少进程服务器和多线服务器端代码的冗余性。我把出错处理全部封装成了独立的函数。把 socket、bind、listen、accept、connect、close、read、write 等函数进行了封装,把所有出错,封装在了大写的 Socket、Bind、Listen、Accept、Connect、Close、Read、Write。因此,在服务端和客户端,可以不用进行错误处理。而且,还有另外一个好处,把以上的函数封装成大写的函数,在程序中,当光标在 大写函数上时,按下 shift + k ,即跳转到小写的 man page 中对应的函数详解。

       封装内容如下:

               wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_

void perr_exit(const char *s);

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);

int Bind(int fd, const struct sockaddr *sa,socklen_t salen);

int Connect(int fd, const struct sockaddr *sa,socklen_t salen);

int Listen(int fd, int backlog);

int Socket(int family, int type, int protocol);

ssize_t Read(int fd, void *ptr, size_t nbytes);

ssize_t Write(int fd, const void *ptr, size_t nbytes);

int Close(int fd);

ssize_t Readn(int fd,void *vptr,size_t n);

ssize_t Writen(int fd,const void*vptr,size_t n);

ssize_t my_read(int fd,char *ptr);

ssize_t Readline(int fd,void *vptr,size_t maxlen);

#endif
       wrap.c

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>

void perr_exit(const char *s) // 出错处理函数
{
perror(s);
exit(-1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;

again:

if(( n = accept(fd,sa,salenptr))<0 )
{ // 如果连接被中断或者被信号打断,重新进行连接
if( (errno == ECONNABORTED) || (errno == EINTR) )
goto again;
else
perr_exit("accept error");
}
return n;
}

int Bind(int fd, const struct sockaddr *sa,socklen_t salen)
{
int n;
if(( n = bind(fd,sa,salen)) < 0 )
perr_exit("bind error:");

return n;
}
int Connect(int fd, const struct sockaddr *sa,socklen_t salen)
{
int n;
n = connect(fd,sa,salen);

if( n<0 )
{
perr_exit("connect error:");
}

return n;
}

int Listen(int fd, int backlog)
{
int n;

if( (n = listen(fd,backlog))<0 )
perr_exit("listen error:");
return n;

}

int Socket(int family, int type, int protocol)
{
int n;
if((n = socket(family,type,protocol))<0 )
perr_exit("socket error:");
return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;

again:

if( (n = read(fd,ptr,nbytes)) == -1 )
{
if( errno == EINTR )
goto again;
else
return -1;
}
return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;

again:

if(( n = write(fd,ptr,nbytes)) == -1 )
{
if( errno == EINTR )
goto again;
else
return -1;
}
return n;
}

int Close(int fd)
{
int n;
if( (n = close(fd)) == -1 )
perr_exit("close error:");

return n;
}

// 参数三:应该读取的字节数

ssize_t Readn(int fd,void *vptr,size_t n)
{
size_t nleft; // unsigned int 剩余未读取的字节数
ssize_t nread; // int 实际读到的字节数
char *ptr;

ptr = vptr;
nleft = n;

while(nleft>0)
{
if(( nread = read(fd,ptr,nleft)) <0 ) // 如果读数据时出错
{
if( errno == EINTR ) //被信号打断
nread = 0;
else // 其他出错
return -1;
}
else if( nread == 0 ) // 如果数据读完的话,跳出循环
break;
else
{
nleft -= nread;
ptr += nread;
}
}
return n - nleft;
}

ssize_t Writen(int fd,const void*vptr,size_t n)
{
size_t nleft;
ssize_t nwritten;
const char* ptr;

nleft = n;
ptr = vptr;

while(nleft>0)
{
if( (nwritten = write(fd,ptr,nleft))<0 )
{
if( nwritten <0 && errno == EINTR )
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr +=nwritten;
}
return n;
}

static ssize_t my_read(int fd,char *ptr)
{
static int read_cnt;
static char* read_ptr;
static char read_buf[100];

if( read_cnt <= 0 )
{

again:

if( (read_cnt = read(fd,read_buf,sizeof(read_buf))) < 0 )
{
if( errno == EINTR )
goto again;
return -1;
}
else if( read_cnt == 0 )
return 0;

read_ptr = read_buf;
}

read_cnt--;
*ptr = *read_ptr++;

return 1;
}

// Readline --- fgtes
// 传出参数 vptr

ssize_t Readline(int fd,void *vptr,ssize_t maxlen)
{
ssize_t n,rc;
char c,*ptr;

ptr = vptr;

for( n=1;n<maxlen;n++)
{
if( (rc = my_read(fd,&c)) == 1 )
{
*ptr++ = c;
if( c == '\n')
break;
}
else if( rc == 0 )
{
*ptr = 0;
return n-1;
}
else
return -1;
}

*ptr = 0;
return n;
}

     注:  以上多进程并发服务器和多线并发服务器模型的服务端都是从客户端收到消息后,对客户端的发送的字符串进行小写转

大写转换,然后把转换后的数据发送到客户端。然后,两个版本的客户端的主要功能是,从键盘获取字符串,然后发送给服务

端。等待服务端进行数据转换,然后接受到服务端转换后的消息,打印到屏幕上。