多进程并发服务器
基于该视频完成
通过的是非阻塞忙轮询的方式实现的
和阻塞等待的区别就是,阻塞是真的阻塞了,而这个方式是一直在问有没有请求有没有请求
文章目录
1.核心思路&功能
实现一个服务器可以连接多个客户端,每当accept函数等待到客户端进行连接时 就创建一个子进程;
核心思路:让accept循环阻塞等待客户端,每当有客户端连接时就fork子进程,让子进程去和客户端进行通信,父进程用于监听并使用信号捕捉回收子进程;(子进程关闭用于监听的套接字lfd,父进程关闭用于通信的cfd)
**功能:**客户端输入小写字符串,服务器转成大写返回给客户端
2.代码实现
warp.h
c
#ifndef __WRAP_H_
#define __WRAP_H_
#include<sys/epoll.h>
//#include<event2/event.h>
#include<sys/select.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
#include<string.h>
#include<dirent.h>
#include<sys/stat.h>
#include<wait.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<strings.h>
#include<netinet/ip.h>
#define SRV_PORT 1234
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 addrlen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
size_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
warp.c
c
#include"warp.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 addrlen)
{
int n;
n=connect(fd,sa,addrlen);
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;
}
size_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 0;
}
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;
ssize_t nread;
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;
}
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
char *ptr;
ptr=(char *)vptr;
nleft=n;
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;
}
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;
}
ssize_t Readline(int fd, void *vptr, size_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;
}
multi_process_concurrency_sever.c
c
#include"warp.h"
//回调函数,父进程用来完成子进程回收的
void catch_child(int signum)
{
while((waitpid(0,NULL,WNOHANG))>0);
return ;
}
int main(int argc,char *argv[])
{
int lfd,cfd;
pid_t pid;
int ret;
char buf[1024];
struct sockaddr_in srv_addr,clt_addr;
socklen_t clt_addr_len;
//地址结构清0
bzero(&srv_addr,sizeof(srv_addr));
srv_addr.sin_family=AF_INET;
srv_addr.sin_port=htons(SRV_PORT);
srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
lfd=Socket(AF_INET,SOCK_STREAM,0);
Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
Listen(lfd,128);
clt_addr_len=sizeof(clt_addr);
int i;
while(1)
{
cfd=Accept(lfd,(struct sockaddr *)&clt_addr,&clt_addr_len);
pid=fork();
if(pid<0)
{
perr_exit("fork error");
}
else if(pid==0)//子进程
{
//关闭用不到的socket
close(lfd);
for(;;)
{
ret=Read(cfd,buf,sizeof(buf));
if(ret==0)
{
Close(cfd);
exit(1);
}
//逻辑处理部分
for(i=0;i<ret;i++)
buf[i]=toupper(buf[i]);
write(cfd,buf,ret);
write(STDOUT_FILENO,buf,ret);
}
}
else//父进程
{
//注册信号
struct sigaction act;
act.sa_handler=catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
ret=sigaction(SIGCHLD,&act,NULL);
if(ret!=0)
{
perr_exit("sigaction error");
}
Close(cfd);
continue;
}
}
return 0;
}
gcc
gcc warp.c multi_process_concurrency_sever.c -o multi_process_concurrency_sever
运行图
两个客户端访问服务器端
3.代码解释
1.指定的固定端口号为1234
也可以用argv接受ip和port参数
2.srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
sin_addr
是struct sockaddr_in
结构体中的一个嵌套结构体,其成员s_addr
用于存放 IP 地址信息(以 32 位整数形式表示)。htonl
函数和htons
类似,不过它是将主机字节序的 32 位整数(通常用于 IP 地址)转换为网络字节序。INADDR_ANY
是一个特殊的常量,它表示服务器端套接字可以绑定到本机的任意可用 IP 地址上,通过这行代码,将转换为网络字节序后的INADDR_ANY
值赋给了srv_addr
结构体的sin_addr.s_addr
成员,使得服务器能够监听来自本机所有网络接口上对应端口的连接请求。
3.需要用到的调用都需要错误处理,封装到warp.c后进行使用
4.父子进程是共享文件描述符表的
- 父子进程共享文件描述符表的含义
- 在 Unix 和 Linux 系统中,当父进程创建子进程时,子进程会继承父进程的文件描述符表。这意味着父子进程可以通过相同的文件描述符来访问同一个文件或 I/O 资源。文件描述符表是一个进程用于管理打开文件或其他 I/O 设备的表格,其中每个文件描述符是一个整数索引,对应着一个打开的文件或设备的相关信息(如文件状态、读写位置等)。
- 例如,父进程打开了一个文件,得到文件描述符为 3,当创建子进程后,子进程也会有一个文件描述符为 3,并且这个文件描述符在父子进程中都指向同一个打开的文件。这种共享机制使得父子进程可以方便地共享文件资源,比如共同对一个文件进行读写操作。
- 父进程
close(5)
后子进程的情况- 当父进程执行
close(5)
操作后,只是父进程自己释放了文件描述符 5 所对应的资源。子进程的文件描述符表仍然保留文件描述符 5,并且子进程仍然可以使用文件描述符 5 来访问对应的文件。 - 原因是父子进程的文件描述符表虽然在创建子进程时是共享的,但它们在操作文件描述符时是相互独立的。父进程的
close
操作不会影响子进程中已经继承的相同文件描述符。
- 当父进程执行
- 子进程是否可以
close(5)
以及继续访问文件描述符 5 对应的文件- 子进程可以执行
close(5)
操作。当子进程执行close(5)
后,它自己释放了文件描述符 5 对应的资源,之后就不能再通过文件描述符 5 来访问原来对应的文件了。 - 在没有执行
close(5)
之前,子进程可以继续访问文件描述符 5 对应的文件。它可以进行正常的读写操作(前提是该文件是以合适的模式打开的,如可读可写模式),并且文件的读写位置等状态是在父子进程间共享的。例如,如果父进程已经读取了文件的一部分内容,子进程通过相同的文件描述符 5 继续读取文件时,会从父进程读取后的位置开始读取。
- 子进程可以执行
5.子进程做的事情:
close(lfd)关闭监听套接字
read()
逻辑处理:小写转大写
write()
6.父进程做的事情:子进程的回收
close(cfd)
注册信号捕捉函数
在回调函数中,完成子进程的回收