基本思路如下图所示:
服务器端代码
process_pool.h头文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CHILD_THREAD_ERROR_CHECK(ret,funcName){if(ret!=0){printf("%s:%s\n",funcName,strerror(ret));return (void*)-1;}}
#define THREAD_ERROR_CHECK(ret,funcName){if(ret!=0){printf("%s:%s\n",funcName,strerror(ret));return -1;}}
#define ARGS_CHECK(argc, val) {if(argc!=val) {printf("error args\n"); return -1;}}
#define ERROR_CHECK(ret, retval, funcName) {if(ret == retval) {perror(funcName); return -1;}}
#define FILENAME "file"
typedef struct{ //父进程管理每个子进程需要的数据结构
pid_t pid; //记录子进程的pid
int pipeFd; //子进程的管道对端
short busy; //用来标识子进程是否忙碌,0代表非忙碌,1代表忙碌
}process_data_t;
typedef struct{ //数据传输的协议设计
int dataLen; //存储buf上需要发送数据的长度
char buf[1000]; //存储需要发送的数据
}train_t; //小火车的协议设计
int makeChild(process_data_t*,int);
int childHandle(int);
int sendFd(int,int);
int recvFd(int,int*);
int tcpInit(int*,char*,char*);
int epollInAdd(int,int);
int tranFile(int);
main.c文件
#include "process_pool.h"
//#define DEBUG
int main(int argc,char* argv[])
{
if(argc != 4)
{
printf("./process_pool_server ip port process_num\n");
return -1;
}
int processNum = atoi(argv[3]); //将字符型数据转化成整型
process_data_t *pData = (process_data_t*)calloc(processNum,sizeof(process_data_t)); //为父进程记录每个子进程的信息结构体分配空间
makeChild(pData,processNum); //创建子进程
int i;
#ifdef DEBUG //测试代码
for(i=0;i<processNum;i++)
{
printf("pid=%d pipeFd=%d\n",pData[i].pid,pData[i].pipeFd);
}
#endif
//sleep(2);
int socketFd; //通过netstat -an|grep ^tcp命令查看tcpInit函数是否执行成功了
tcpInit(&socketFd,argv[1],argv[2]); //初始化socket并开启监听
int epfd=epoll_create(1); //创建epoll句柄
struct epoll_event *evs; //监听描述符的个数为processNum+1
evs=(struct epoll_event*)calloc(processNum+1,sizeof(struct epoll_event));
epollInAdd(epfd,socketFd); //往epfd指向的事件表中注册socketFd上的读事件
for(i=0;i<processNum;i++)
{
epollInAdd(epfd,pData[i].pipeFd); //注册监听每一个子进程的管道对端(读事件)
}
int readyFdCount,newFd,j;
char noBusyflag;
while(1)
{
//等待客户端连接
readyFdCount=epoll_wait(epfd,evs,processNum+1,-1); //监控多少个描述符,evs申请的空间就多大
for(i=0;i<readyFdCount;i++) //单次客户端请求并发数量
{
if(evs[i].data.fd==socketFd) //有客户端连接
{
newFd=accept(socketFd,NULL,NULL); //接收客户端申请,返回一个newFd
//此处不拿出客户端的信息,后面两个参数设置为NULL
for(j=0;j<processNum;j++) //寻找非忙碌的子进程
{
if(0==pData[j].busy)
{
//任务就是客户端的newFd,sendFd是进程间的dup机制
//newfd描述符发送一次,引用计数+1
sendFd(pData[j].pipeFd,newFd); //把任务发给对应的子进程
pData[j].busy=1; //子进程标识为忙碌
printf("%d pid is busy\n",pData[j].pid);
break;
}
}
close(newFd); //父进程要关闭newFd
}
//子进程完成任务,开始写管道时,epoll也会被唤醒
for(j=0;j<processNum;j++) //判断evs[i].data.fd对应的具体哪个子进程的管道对端
{
if(evs[i].data.fd==pData[j].pipeFd)
{
//当对应子进程的pipeFd可读时,将数据读出来,子进程不忙碌
read(pData[j].pipeFd,&noBusyflag,1); //收到子进程的通知
pData[j].busy=0; //子进程设置为非忙碌
printf("%d pid is not busy\n",pData[j].pid);
break;
}
}
}
}
return 0;
}
tcp_init.c文件
#include "process_pool.h"
int tcpInit(int* sfd,char* ip,char* port) //sfd为传出参数
{
int socketFd=socket(AF_INET,SOCK_STREAM,0);
ERROR_CHECK(socketFd,-1,"socket");
struct sockaddr_in serAddr;
bzero(&serAddr,sizeof(serAddr)); //清空存放socket通信信息的结构体
serAddr.sin_family=AF_INET;
serAddr.sin_port=htons(atoi(port));
serAddr.sin_addr.s_addr=inet_addr(ip);
int ret;
ret=bind(socketFd,(struct sockaddr*)&serAddr,sizeof(serAddr));
ERROR_CHECK(ret,-1,"bind");
listen(socketFd,10);
*sfd=socketFd;
return 0;
}
child.c文件
#include "process_pool.h"
//创建子进程,并初始化主数据结构
int makeChild(process_data_t *p, int processNum)
{
int i;
pid_t pid;
int fds[2]; //for循环和fds[2]没有关系
int ret;
for(i=0;i<processNum;i++)
{
//socketpair()函数用于创建一对无名的、相互连接的套接字
//如果函数成功,则返回0.创建好的套接字分别是fds[0]和fds[1];否则返回-1,错误码保存于errno中
ret=socketpair(AF_FILE,SOCK_STREAM,0,fds); //父进程和每个子进程之间都存在互通的管道
ERROR_CHECK(ret,-1,"socketpair");
pid=fork();
if(0==pid) //子进程不会从这个if语句中出来
{
close(fds[0]); //子进程关闭fds[0]socket管道的读写端
childHandle(fds[1]); //子进程处理函数
}
close(fds[1]); //父进程关闭fds[1]socket管道的读写端
p[i].pid=pid; //子进程的pid
p[i].pipeFd=fds[0]; //储存每个子进程的管道对端
p[i].busy=0;
}
return 0;
}
int childHandle(int pipeFd) //每个子进程都限制在while1中
{
int newFd; //父进程传给子进程的对应客户端的newFd
char finishFlag; //完成任务标志位
while(1)
{
//如果父进没有派任务,子进程会在recvFd位置sleep
recvFd(pipeFd,&newFd); //接收目标文件的描述符
tranFile(newFd); //向客户端发送文件
close(newFd); //关闭连接
write(pipeFd,&finishFlag,1); //子进程通过向socket管道fds[1]中写入来通知父进程处理完任务了
}
}
//父进程持有sockfds[0] 套接字进行读写,而子进程持有sockfds[1] 套接字进行读写。
//如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
/*基本用法:
* 1. 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
* 2. 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
* 3. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。
* 如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述符sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符
* socketpair()函数创建出两个进程,fork()之后这两个进程都会执行主程序中的代码,这个一定要注意!
* 尤其是bind的时候,如果bind两次的话,那就会出错了。一般会在子进程里调用一个带死循环的函数,这样就好了。*/
tran_file.c
#include "process_pool.h"
void sigFunc(int signum) //信号捕捉处理函数
{
printf("%d is coming\n",signum);
}
int tranFile(int newFd) //向客户端发送数据
{
signal(SIGPIPE,sigFunc); //捕捉SIGPIPE信号
train_t train; //定义发送数据的小火车
int ret;
//发送文件名和文件名大小给客户端
train.dataLen=strlen(FILENAME);
strcpy(train.buf,FILENAME);
send(newFd,&train,4+train.dataLen,0); //向buf中写入文件名发送给客户端,4+train.dataLen为buf中的内容加buf大小的值(字节数)
//ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
//发送文件的大小给客户端
struct stat buf;
int fd=open(FILENAME,O_RDWR);
fstat(fd,&buf); //buf.st_size 保存着文件的大小
train.dataLen=sizeof(buf.st_size);
memcpy(train.buf,&buf.st_size,train.dataLen); //buf中存储着文件的大小
send(newFd,&train,4+train.dataLen,0); //发送文件大小
//发送文件内容给客户端
//最后一节火车接受完,dataLen返回的是0
while((train.dataLen=read(fd,train.buf,sizeof(train.buf))))
{
//如果recv一端断开,send第一次会返回-1,再次send就会崩溃
ret=send(newFd,&train,4+train.dataLen,0); //读到多少字节就发送多少字节
if(-1==ret)
{
return -1;
}
}
send(newFd,&train,4,0); //发送结束标志,train.dataLen值为0
return 0;
}
/*ssize_t read(int fd, void *buf, size_t nbytes);
* 返回值:读到的字节数,若已到文件尾,返回0;若是出错返回-1
* 若在到达文件末尾之前有30字节,而要求读100字节,则read返回30,下次再调用read时,它将返回0
* 有关read函数详解 UNIX p57*/
/*有关SIGPIPE信号,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号,我们需要在代码
* 中捕获处理该信号,或者至少忽略它,因为程序收到SIGPIPE信号的默认行为是结束进程,而我们绝对
* 不希望因为错误的写操作而导致程序退出。我们应该使用send函数反馈的errno值来判断管道或者socket
* 连接的读端是否已经关闭*/
sendfd.c
#include "process_pool.h"
/*传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符,
* 并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项*/
int sendFd(int pipeFd,int fd)
{
struct msghdr msg;
bzero(&msg,sizeof(msg)); //linux高性能服务器编程,P267页
struct iovec iov[2];
char buf1[10]="hello";
iov[0].iov_base=buf1;
iov[0].iov_len=5;
char buf2[10]="world";
iov[1].iov_base=buf2;
iov[1].iov_len=5;
msg.msg_iov=iov;
msg.msg_iovlen=2;
struct cmsghdr *cmsg;
int cmsgLen=CMSG_LEN(sizeof(int));
cmsg=(struct cmsghdr *)calloc(1,cmsgLen);
cmsg->cmsg_len=cmsgLen;
cmsg->cmsg_level=SOL_SOCKET;
cmsg->cmsg_type=SCM_RIGHTS;
*(int *)CMSG_DATA(cmsg)=fd;//要把传递的描述符告诉内核
msg.msg_control=cmsg;
msg.msg_controllen=cmsgLen;
int ret;
ret=sendmsg(pipeFd,&msg,0);
ERROR_CHECK(ret,-1,"sendmsg");
return 0;
}
int recvFd(int pipeFd,int *fd) //子进程从管道接收目标文件的描述符,fd是传出参数,其值为目标文件的描述符
{
struct msghdr msg;
bzero(&msg,sizeof(msg));
struct iovec iov[2];
char buf1[10]="hello";
iov[0].iov_base=buf1;
iov[0].iov_len=5;
char buf2[10]="world";
iov[1].iov_base=buf2;
iov[1].iov_len=5;
msg.msg_iov=iov;
msg.msg_iovlen=2;
struct cmsghdr *cmsg;
int cmsgLen=CMSG_LEN(sizeof(int));
cmsg=(struct cmsghdr *)calloc(1,cmsgLen);
cmsg->cmsg_len=cmsgLen;
cmsg->cmsg_level=SOL_SOCKET;
cmsg->cmsg_type=SCM_RIGHTS;
msg.msg_control=cmsg;
msg.msg_controllen=cmsgLen;
int ret;
ret=recvmsg(pipeFd,&msg,0);
ERROR_CHECK(ret,-1,"recvmsg");
*fd=*(int*)CMSG_DATA(cmsg);
return 0;
}
epoll_op.c
#include "process_pool.h"
int epollInAdd(int epfd,int fd) //往epfd指向的事件表上注册fd上的读事件
{
struct epoll_event event; //定义注册事件
event.events=EPOLLIN; //读事件
event.data.fd=fd;
int ret;
ret=epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
ERROR_CHECK(ret,-1,"epoll_ctl");
return 0;
}
Makefile
SRCS:=$(wildcard *.c)
OBJS:=$(patsubst %.c,%.o,$(SRCS))
ELF:=process_pool_server
CC:=gcc
CFLAGS:=-g -Wall
$(ELF):$(OBJS)
gcc -o $@ $^
clean:
rm -rf $(OBJS) $(ELF)
客户端的程序
tcp_client.c文件
#include
int recvCycle(int,void*,int);
int main(int argc,char* argv[])
{
ARGS_CHECK(argc,3);
int socketFd=socket(AF_INET,SOCK_STREAM,0);
ERROR_CHECK(socketFd,-1,"socket");
struct sockaddr_in serAddr;
bzero(&serAddr,sizeof(serAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_port=htons(atoi(argv[2]));
serAddr.sin_addr.s_addr=inet_addr(argv[1]);
int ret;
ret=connect(socketFd,(struct sockaddr*)&serAddr,sizeof(serAddr));
ERROR_CHECK(ret,-1,"connect");
int fd;
int dataLen;
char buf[1000]={0};
recvCycle(socketFd,&dataLen,4); //接收文件名的大小,存入dataLen(占多少字节)
recvCycle(socketFd,buf,dataLen); //接收文件名
fd=open(buf,O_CREAT|O_RDWR,0666);//创建文件名,加权限
ERROR_CHECK(fd,-1,"open");
//接收文件大小
off_t fileSize,downLoadSize=0;
recvCycle(socketFd,&dataLen,4); //长度存入dataLen,比如 400000字节,占4个字节空间
recvCycle(socketFd,&fileSize,dataLen); //实际fileSize的值为40000字节
time_t lastTime, nowTime;
lastTime=nowTime=time(NULL);
struct timeval start,end;
gettimeofday(&start,NULL);
while(1)
{
recvCycle(socketFd,&dataLen,4); //车厢的长度
if(dataLen>0)
{
recvCycle(socketFd,buf,dataLen);
write(fd,buf,dataLen);
downLoadSize+=dataLen;
time(&nowTime);
if(nowTime-lastTime>=0.1)
{
printf("%5.2f%s\r",(float)downLoadSize/fileSize*100,"%");
fflush(stdout); //每次打印后,情况输出缓冲区
lastTime=nowTime;
}
}
else
{
printf("100.00%%\n");
break;
}
}
gettimeofday(&end,NULL);
printf("use time=%ld\n",(end.tv_sec-start.tv_sec)*1000000+end.tv_usec-start.tv_usec);
close(fd);
close(socketFd);
return 0;
}
recv_cycle.c
#include
/*循环接收*/
/*接1000个数据,一定让它接1000个字节,否则不让它出循环*/
int recvCycle(int sfd,void* buf,int recvLen) //socketfd
{
char *p=(char*)buf;
int total=0,ret;
while(total<recvLen) //recv返回接收的字节大小
{
ret=recv(sfd,p+total,recvLen-total,0); //下次再次接收数据的时候放在buf的p+total的位置
total+=ret; //累加到total上
}
return 0;
}
Makefile
SRCS:=$(wildcard *.c)
OBJS:=$(patsubst %.c,%.o,$(SRCS))
ELF:=client
CC:=gcc
CFLAGS:=-g -Wall
$(ELF):$(OBJS)
gcc -o $@ $^
clean:
rm -rf $(OBJS) $(ELF)