进程池实现代码分块解析

内部笔记——方便理解,自用(如有错误或不准确的地方请私信我,我会进行更正。)

分析代码——先看头文件中有哪些接口,再看调用关系。

#process_pool_bigfile.h

#ifndef __WD_FUNC_H
#define __WD_FUNC_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <error.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
#include <dirent.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/epoll.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <sys/uio.h>

#define SIZE(a) (sizeof(a)/sizeof(a[0]))

typedef void (*sighandler_t)(int);

#define ARGS_CHECK(argc, num)   {\
    if(argc != num){\
        fprintf(stderr, "ARGS ERROR!\n");\
        return -1;\
    }}

#define ERROR_CHECK(ret, num, msg) {\
    if(ret == num) {\
        perror(msg);\
        return -1;\
    }}

typedef enum {
    FREE,
    BUSY
}status_t;

typedef struct {
    int len;
    char buf[1000];
}train_t;


typedef struct {
    pid_t pid;//子进程的id
    int pipefd;//与子进程通信的管道
    status_t status;//0 空闲, 1 是忙碌
}process_data;

int makeChild(process_data *, int );
int doTask(int pipefd);

int sendFd(int pipefd, int fd);
int recvFd(int pipefd, int * pfd);

int tcpInit(const char * ip, unsigned short port);
int epollAddReadEvent(int epfd, int fd);
int epollDelReadEvent(int epfd, int fd);

int transferFile(int peerfd);

#endif

 分块解析:

1、各类头文件和宏函数

2、子进程状态结构体

typedef enum {
    FREE,
    BUSY
}status_t;

3、小火车结构体

解决TCP连接粘包问题的协议——小火车协议,在发送文件的基础上加一个int型占4个字节的长度信息

typedef struct {
    int len;
    char buf[1000];
}train_t;

4、子进程结构体

包含进程id、与父进程通信的管道的文件描述符、子进程的状态(空闲还是忙碌)

typedef struct {
    pid_t pid;//子进程的id
    int pipefd;//与子进程通信的管道
    status_t status;//0 空闲, 1 是忙碌
}process_data;
  •  5、各类函数接口

  1. 创建子进程——传入参数:子进程结构体,包含进程id、通信管道、当前状态。
    int makeChild(process_data *, int );
  2. 执行子进程任务——参数:传入通信管道中的文件描述符(代指任务内容)
    int doTask(int pipefd);
  3. 父子进程间传递文件描述符——父进程用sendFd将收到的客户端的任务fd传给管道,子进程再用recvFd从管道中将fd(pipefd)取出
    int sendFd(int pipefd, int fd);
    int recvFd(int pipefd, int * pfd);
  4. 与客户端建立tcp连接并让epoll的监督员epfd添加或删除读就绪事件
    int tcpInit(const char * ip, unsigned short port);
    int epollAddReadEvent(int epfd, int fd);
    int epollDelReadEvent(int epfd, int fd);
  5. 传输文件数据

    int transferFile(int peerfd);

# main.c

#include "process_pool.h"
#include <signal.h>

int main(int argc, char ** argv)
{
    //ip port processnum
    ARGS_CHECK(argc, 4);
    int processNum = atoi(argv[3]);
    process_data * pProcess = calloc(processNum, sizeof(process_data));
    //让父子进程都忽略掉SIGPIPE信号
    signal(SIGPIPE, SIG_IGN);
    //创建N个子进程
    makeChild(pProcess, processNum);
    //makechild函数之后,都是父进程的操作

    //创建监听的服务器
    int listenfd = tcpInit(argv[1], atoi(argv[2]));

    //创建epoll的实例
    int epfd = epoll_create1(0);
    ERROR_CHECK(epfd, -1, "epfd");
    //epoll监听Listenfd
    epollAddReadEvent(epfd, listenfd);
    ///epoll监听父子进程间通信的管道
    for(int i = 0; i < processNum; ++i) {
        epollAddReadEvent(epfd, pProcess[i].pipefd);
    }
    //定义保存就绪的文件描述符的数组
    struct epoll_event eventArr[10] = {0};
    int nready = 0;

    while(1)
    {
        nready = epoll_wait(epfd, eventArr, sizeof(eventArr), -1);
        for(int i = 0; i < nready; ++i) {
            int fd = eventArr[i].data.fd; 
            //新客户端到来
            if(fd == listenfd) {
                struct sockaddr_in clientaddr;
                socklen_t len = sizeof(clientaddr);
                int peerfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
                ERROR_CHECK(peerfd, -1, "accept");
                printf("client %s:%d connected.\n",
                       inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
                //将peerfd发送给一个空闲的子进程
                for(int j = 0; j < processNum; ++j) {
                    if(pProcess[j].status == FREE) {
                        sendFd(pProcess[j].pipefd, peerfd);
                        pProcess[j].status = BUSY;
                        break;
                    }
                }
                //如果要断开与客户端的连接,这里还得执行一次
                close(peerfd);
            } else {
                //管道发生了事件: 子进程已经执行完任务了
                int howmany = 0;
                read(fd, &howmany, sizeof(howmany));
                for(int j = 0; j < processNum; ++j) {
                    if(pProcess[j].pipefd == fd) {
                        pProcess[j].status = FREE;
                        printf("child %d is not busy.\n", pProcess[j].pid);
                        break;
                    }
                }

            }
        }
    }
    close(listenfd);
    close(epfd);

    return 0;
}

分块解析:

1、命令行参数校验

利用头文件中的宏函数实现命令行参数的校验,如果输入的参数数量不对,则会报错退出

一共有四个参数:1、服务端IP地址,2、通信进程的端口号,3、需要预先创建的子进程数量

//ip port processnum
    ARGS_CHECK(argc, 4);

2、忽略SIGPIPE信号

由于客户端断开连接时,导致服务器中某一个子进程挂掉变成僵尸进程,导致父子进程通信的管道被关闭,而父进程一直监听该管道,会不断的监听到发出的SIGPIPE信号(表示管道已经损坏),父进程会误认为管道中依然有数据处于读就绪状态,于是epoll_wait函数不断返回,会有服务器疯狂打印的情况出现。

//让父子进程都忽略掉SIGPIPE信号
    signal(SIGPIPE, SIG_IGN);

3、创建N个子进程

封装函数makechild()创建多个子进程用来和客户端交互。

 //创建N个子进程
    makeChild(pProcess, processNum);

 //makechild函数之后,都是父进程的操作。

 4、监听服务器

创建listenfd套接字用来控制连接。

 //创建N个子进程
    makeChild(pProcess, processNum);

 5、在epoll监督公司聘请监督员epfd来监听事件

//创建epoll的实例
    int epfd = epoll_create1(0);
    ERROR_CHECK(epfd, -1, "epfd");

 6、监督员epfd监听listenfd(代指新连接也是新客户端)

epoll公司的epfd会监听连接和管道文件等内容是处于读就绪还是写就绪,根据相应的缓冲区的内容,如果是非空(有数据)就表示读就绪,如果是未满(有空间)就表示写就绪。

监听的底层原理就是在内核的红黑树上添加新结点。

//epoll监听Listenfd
    epollAddReadEvent(epfd, listenfd);

 7、监督员epfdl监听父子进程间通信的管道

///epoll监听父子进程间通信的管道
    for(int i = 0; i < processNum; ++i) {
        epollAddReadEvent(epfd, pProcess[i].pipefd);
    }

8、定义数组——保存就绪事件的文件描述符

数组类型为监听事件结构体struct epoll_event

 //定义保存就绪的文件描述符的数组
    struct epoll_event eventArr[10] = {0};
    int nready = 0;

 //开始处理事件任务

9、循环用epoll_wait函数,遍历,取符

在循环中epoll_wait函数阻塞,等待监听的就绪事件的文件描述符被挂上就绪链表,并返回就绪事件的个数,并遍历就绪事件,取得所有就绪事件的文件描述符。(fd是epoll监听的就绪链表上的文件描述符。

 while(1)
    {
        nready = epoll_wait(epfd, eventArr, sizeof(eventArr), -1);
        for(int i = 0; i < nready; ++i) {
            int fd = eventArr[i].data.fd; 

分两种情况:

1、处理网络连接:新客户端到来,即有新连接,此时该就绪事件所对应的fd正好为服务器监听到的listenfd

2、 处理子进程状态变化(eg:已建立好连接的客户端的任务完成),此时fd不是服务器新监听到的listenfd而是本来就挂在就绪链表的就绪事件的文件描述符。

10、与客户端建立连接

定义网络地址的结构体并存储客户端的网络地址,并用accept函数接收返回与客户端进行交互的文件描述符peerfd

//新客户端到来
            if(fd == listenfd) {
                struct sockaddr_in clientaddr;
                socklen_t len = sizeof(clientaddr);
                int peerfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
                ERROR_CHECK(peerfd, -1, "accept");
                printf("client %s:%d connected.\n",
                       inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

 11、文件描述符传递

父进程在收到该连接成功后客户端的文件描述符peerfd后,通过sendFd函数将客户端文件描述符peerfd通过管道通信传递给一个子进程,实际上只发给管道的一端 ,

 //将peerfd发送给一个空闲的子进程
                for(int j = 0; j < processNum; ++j) {
                    if(pProcess[j].status == FREE) {
                        sendFd(pProcess[j].pipefd, peerfd);
                        pProcess[j].status = BUSY;
                        break;
                    }
                }

12、断开父进程和客户端之间的连接

此时父进程已经把客户端的文件描述符通过管道通信发给了子进程,已经下派完任务了,此时就不需要再和客户端保持着连接,所以要及时断开连接释放资源

 //如果要断开与客户端的连接,这里还得执行一次
                close(peerfd);

13、当子进程完成父进程下派的任务后,要更改子进程的状态,以便后续任务的下发

 } else {
                //管道发生了事件: 子进程已经执行完任务了
                int howmany = 0;
                read(fd, &howmany, sizeof(howmany));
                for(int j = 0; j < processNum; ++j) {
                    if(pProcess[j].pipefd == fd) {
                        pProcess[j].status = FREE;
                        printf("child %d is not busy.\n", pProcess[j].pid);
                        break;
                    }
                }

            }
        }
    }

14、关闭服务器时要释放资源

退出while循环时说明epoll公司的监督员epfd监听到的所有事件任务均处理完毕,内核中的红黑树和就绪链表为空了,若也无新连接到来的话,此时可以退出任务执行循环了,此时应令控制连接的listenfd和监督事件发生的epfd下班回家。

    close(listenfd);
    close(epfd);

    return 0;
}

编者的理解(仅供参考):

一轮for循环处理一个客户端的任务,

当退出for循环时说明在进入循环时的就绪链表上的任务都处理完成了,

但每一轮while循环都会通过epoll_wait函数更新内核中红黑树的节点和就绪链表节点,每一处理完一个客户端任务后都会重新遍历更新后的就绪链表,

这样可以确保每次新到来的客户端连接任务能够被处理,

while被设计成死循环,保证服务器一直处于开启状态。


AI的理解(仅供参考):

在这段代码中,`while`循环部分是事件循环,它不断监听并处理来自客户端的连接请求以及子进程之间的通信。具体来说,这段代码的作用是:

1. **等待事件发生 (`epoll_wait`):** `epoll_wait` 函数会阻塞直到有文件描述符事件发生,然后将发生的事件存储在 `eventArr` 数组中。`nready` 保存了就绪的文件描述符的数量。

2. **遍历所有就绪的文件描述符:** 使用一个 `for` 循环遍历所有发生事件的文件描述符。

3. **处理新客户端的连接:** 
   - 如果发生事件的文件描述符是 `listenfd`(服务器监听的文件描述符),表示有新的客户端连接到来。
   - 使用 `accept` 接受新连接,并获取客户端地址信息。
   - 打印客户端的连接信息。
   - 将新连接的文件描述符 `peerfd` 发送给一个空闲的子进程进行处理(通过管道通信),并将该子进程的状态设置为忙碌。
   - 关闭 `peerfd`。

4. **处理子进程的状态变化:** 
   - 如果发生事件的是某个子进程的管道文件描述符,表示该子进程已经完成了任务。
   - 从管道中读取数据,并将相应子进程的状态设置为空闲。
   - 打印该子进程不再忙碌的消息。

这个循环使得服务器能够同时处理多个客户端连接,并且合理地分配任务给多个子进程。

函数接口解析:

#child.c

1、makeChild

#include "process_pool.h"

int makeChild(process_data * pProcess, int num)
{
    for(int i = 0; i < num; ++i) {
        int fds[2];
        socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
        pid_t pid = fork();
        if(pid == 0) {//子进程
            close(fds[1]);
            //子进程执行任务
            doTask(fds[0]);
            exit(0);
        } 
        //父进程
        close(fds[0]);
        //记录子进程的信息
        pProcess[i].pid = pid;
        pProcess[i].pipefd = fds[1];//与子进程通信的管道
        pProcess[i].status = FREE;
    }
    return 0;
}

int doTask(int pipefd)
{
    printf("proces %d is doTask...\n", getpid());
    while(1)
    {
        int peerfd = -1;
        //子进程不断地读取管道中传递过来的peerfd
        recvFd(pipefd, &peerfd);
        //发送文件的操作
        transferFile(peerfd);
        printf("child %d send finish.\n", getpid());

        //关闭peerfd
        close(peerfd);

        //通知父进程,任务执行完毕
        int one = 1;
        write(pipefd, &one, sizeof(one));
    }

    return 0;
}

makechild分块解析:(参数:(process_data * pProcess, int num))

整个函数在一个for循环中,循环创建num个子进程和其与父进程之间的通信管道

int makeChild(process_data * pProcess, int num)
{
    for(int i = 0; i < num; ++i) {
……
}

1、创建全双工的套接字对fds[2]对作为通信管道

 int fds[2];
        socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);

2、创建子进程并执行任务(使用全双工的套接字对要注意关闭不用的管道端)

pid_t pid = fork();
        if(pid == 0) {//子进程
            close(fds[1]);
            //子进程执行任务
            doTask(fds[0]);
            exit(0);
        } 

 3、父进程记录子进程的信息

//父进程
        close(fds[0]);
        //记录子进程的信息
        pProcess[i].pid = pid;
        pProcess[i].pipefd = fds[1];//与子进程通信的管道
        pProcess[i].status = FREE;
    }
    return 0;
}

doTask分块解析:执行读操作完成父进程交代的任务,参数:(int pipefd)

整个函数在一个while(1)循环中

1、子进程不断地接收父进程从通信管道中传递来的文件描述符(任务),用peerfd接收

printf("proces %d is doTask...\n", getpid());
    while(1)
    {
        int peerfd = -1;
        //子进程不断地读取管道中传递过来的peerfd
        recvFd(pipefd, &peerfd);

2、发送文件操作(执行任务内容),并在执行完后关闭文件释放资源

 //发送文件的操作
        transferFile(peerfd);
        printf("child %d send finish.\n", getpid());

        //关闭peerfd
        close(peerfd);

3、任务执行完成后要通知父进程任务执行完毕

 //通知父进程,任务执行完毕
        int one = 1;
        write(pipefd, &one, sizeof(one));
    }

    return 0;
}

#transfer.c

函数中有两个函数:1、sendn函数用来确定发送的字节数是send函数的升级版

2、transferFile函数是用来传输文件数据的

#include "process_pool.h"

#define FILENAME "bigfile.avi"

//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
    int left = len;
    const char* pbuf = buff;
    int ret = -1;
    while(left > 0) {
        ret = send(sockfd, pbuf, left, 0);
        if(ret < 0) {
            perror("send");
            return -1;
        }
        left -= ret;
        pbuf += ret;
    }
    return len - left;
}

int transferFile(int peerfd)
{
    //读取本地文件
    int fd = open(FILENAME, O_RDONLY);
    ERROR_CHECK(fd, -1, "open");
    //获取文件的长度
    struct stat st;
    memset(&st, 0, sizeof(st));
    fstat(fd, &st);
    char buff[100] = {0};
    int filelength = st.st_size;
    printf("filelength: %d\n", filelength);

    //进行发送操作
    //1. 发送文件名
    train_t t;
    memset(&t, 0, sizeof(t));
    t.len = strlen(FILENAME);
    strcpy(t.buf, FILENAME);
    sendn(peerfd, &t, 4 + t.len);
    
    //2. 再发送文件内容
    //2.1 发送文件的长度
    sendn(peerfd, &filelength, sizeof(filelength));

    int ret = 0;
    int total = 0;
    //2.2 再发送文件内容
    while(total < filelength) {
        memset(&t, 0, sizeof(t));
        ret = read(fd, t.buf, 1000);
        if(ret > 0) {
            t.len = ret;
            //sendn函数确保 4 + t.len 个字节的数据能正常发送
            ret = sendn(peerfd, &t, 4 + t.len);
            if(ret < 0) {
                printf(">> exit while not send.\n");
                break;//发生了错误,就退出while循环
            }
            total += (ret - 4);
        }
    }

    return 0;
}

sendn函数分块解析:

1、准备数据

left表示待发送的字节数

pbuf表示缓冲区用来存放数据的首地址

ret表示已发送的字节数

int left = len;
    const char* pbuf = buff;
    int ret = -1;

2、用send函数循环发送数据,利用left和ret的变化控制字节数的发送

while(left > 0) {
        ret = send(sockfd, pbuf, left, 0);
        if(ret < 0) {
            perror("send");
            return -1;
        }
        left -= ret;
        pbuf += ret;
    }
    return len - left;
}

transferFile函数分块解析:

1、打开本地文件

//读取本地文件
    int fd = open(FILENAME, O_RDONLY);
    ERROR_CHECK(fd, -1, "open");

2、获取文件长度——利用fstat函数来获取文件的状态信息(长度等)

//获取文件的长度
    struct stat st;
    memset(&st, 0, sizeof(st));
    fstat(fd, &st);
    char buff[100] = {0};
    int filelength = st.st_size;
    printf("filelength: %d\n", filelength);

3、进行发送操作

发送文件名

先定义小火车,初始化小火车的长度信息(文件名长度)和内容信息(文件名内容),再一并发送共4+t.len字节的信息

//进行发送操作
    //1. 发送文件名
    train_t t;
    memset(&t, 0, sizeof(t));
    t.len = strlen(FILENAME);
    strcpy(t.buf, FILENAME);
    sendn(peerfd, &t, 4 + t.len);

发送文件内容

其中要先发送文件长度

//2. 再发送文件内容
    //2.1 发送文件的长度
    sendn(peerfd, &filelength, sizeof(filelength));

再发送文件内容

使用read系统调用获取文件内容的字节数量,再用sendn发送文件内容完成任务。

    int ret = 0;
    int total = 0;
 //2.2 再发送文件内容
    while(total < filelength) {
        memset(&t, 0, sizeof(t));
        ret = read(fd, t.buf, 1000);
        if(ret > 0) {
            t.len = ret;
            //sendn函数确保 4 + t.len 个字节的数据能正常发送
            ret = sendn(peerfd, &t, 4 + t.len);
            if(ret < 0) {
                printf(">> exit while not send.\n");
                break;//发生了错误,就退出while循环
            }
            total += (ret - 4);
        }
    }

    return 0;
}

#server.c

包含三个函数:

1、建立tcp连接的tcpInit函数

2、添加监听读事件节点函数 epollAddReadEvent

3、删除监听读事件节点函数 epollDelReadEvent

#include "process_pool.h"

int tcpInit(const char * ip, unsigned short port)
{
    //创建服务器的监听套接字
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(listenfd, -1, "socket");

    //设置套接字的网络地址可以重用
    int on = 1;
    int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    ERROR_CHECK(ret, -1, "setsockopt");

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    //指定使用的是IPv4的地址类型 AF_INET
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(port);
    serveraddr.sin_addr.s_addr = inet_addr(ip);

    //以人类可阅读的方式打印网络地址
    printf("%s:%d\n", 
           inet_ntoa(serveraddr.sin_addr),
           ntohs(serveraddr.sin_port));

    //绑定服务器的网络地址
    ret = bind(listenfd, (const struct sockaddr*)&serveraddr, 
                   sizeof(serveraddr));
    ERROR_CHECK(ret, -1, "bind");

    //监听客户端的到来
    ret = listen(listenfd, 1);
    ERROR_CHECK(ret, -1, "listen");

    return listenfd;
}

int epollAddReadEvent(int epfd, int fd)
{
    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    ERROR_CHECK(ret, -1, "epoll_ctl");
    return 0;
}

int epollDelReadEvent(int epfd, int fd)
{
    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
    ERROR_CHECK(ret, -1, "epoll_ctl");
    return 0;
}

1、tcpInit函数分块解析

1、创建服务器监听套接字

int tcpInit(const char * ip, unsigned short port)
{
    //创建服务器的监听套接字
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(listenfd, -1, "socket");

2、设置套接字的网络地址可以重用

这是为了当服务器断开连接进入TIME——WAIT状态时,可以快速地再次启动服务器进程

//设置套接字的网络地址可以重用
    int on = 1;
    int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    ERROR_CHECK(ret, -1, "setsockopt");

3、确定并存储服务端的网络地址

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    //指定使用的是IPv4的地址类型 AF_INET
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(port);
    serveraddr.sin_addr.s_addr = inet_addr(ip);

4、打印一下服务器网络地址用来交互显示

    //以人类可阅读的方式打印网络地址
    printf("%s:%d\n", 
           inet_ntoa(serveraddr.sin_addr),
           ntohs(serveraddr.sin_port));

5、绑定服务器的网络地址

    //绑定服务器的网络地址
    ret = bind(listenfd, (const struct sockaddr*)&serveraddr, 
                   sizeof(serveraddr));
    ERROR_CHECK(ret, -1, "bind");

6、监听客户端的到来

    //监听客户端的到来
    ret = listen(listenfd, 1);
    ERROR_CHECK(ret, -1, "listen");

    return listenfd;
}

2、epollAddReadEvent函数解析

1、创建监听事件结构体并初始化为读事件

int epollAddReadEvent(int epfd, int fd)
{
    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;
    ev.data.fd = fd;

2、使用epoll_ctl函数在内核监听红黑树上添加该读事件节点

int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    ERROR_CHECK(ret, -1, "epoll_ctl");
    return 0;
}

2、epollDelReadEvent函数解析

1、创建监听事件结构体并初始化为读事件

int epollDelReadEvent(int epfd, int fd)
{
    struct epoll_event ev;
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;

2、使用epoll_ctl函数在内核监听红黑树上删除该读事件节点

 int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
    ERROR_CHECK(ret, -1, "epoll_ctl");
    return 0;
}

#sendFd.c——用来实现管道通信传递文件描述符

共两个函数sendFd函数和recvFd函数

1、sendFd函数

1、构建msghdr结构体的第二组成员

int sendFd(int pipefd, int fd)
{
    //构建第二组成员
    char buff[6] = {0};
    struct iovec iov;
    memset(&iov, 0, sizeof(iov));
    iov.iov_base = buff;
    iov.iov_len = sizeof(buff);

2、构建msghdr结构体的第三组成员

    //构建第三组成员
    int len = CMSG_LEN(sizeof(fd));
    struct cmsghdr * pcmsg = 
        (struct cmsghdr*)calloc(1, len);
    pcmsg->cmsg_len = len;
    pcmsg->cmsg_level = SOL_SOCKET;
    pcmsg->cmsg_type = SCM_RIGHTS;
    int * p = (int*)CMSG_DATA(pcmsg);
    *p = fd;

3、构建msghdr结构体并用上面的成员初始化

    //构建msghdr
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = pcmsg;//传递文件描述符
    msg.msg_controllen = len;

4、使用sendmsg函数将文件描述符发送至父子进程间的通信管道

   //sendmsg的返回值大于0时,就是iov传递的数据长度
    int ret = sendmsg(pipefd, &msg, 0);
    printf("sendmsg ret: %d\n", ret);
    ERROR_CHECK(ret, -1, "sendmsg");
    free(pcmsg);
    return 0;
}

2、recvFd函数

 1、构建msghdr结构体的第二组成员

int recvFd(int pipefd, int * pfd)
{
    //构建第二组成员
    char buff[6] = {0};
    struct iovec iov;
    memset(&iov, 0, sizeof(iov));
    iov.iov_base = buff;
    iov.iov_len = sizeof(buff);

2、构建msghdr结构体的第三组成员

    //构建第三组成员
    int len = CMSG_LEN(sizeof(int));
    struct cmsghdr * pcmsg = 
        (struct cmsghdr*)calloc(1, len);
    pcmsg->cmsg_len = len;
    pcmsg->cmsg_level = SOL_SOCKET;
    pcmsg->cmsg_type = SCM_RIGHTS;

3、构建msghdr结构体并用上面的成员初始化

    //构建一个struct msghdr
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = pcmsg;//传递文件描述符
    msg.msg_controllen = len;

4、使用sendmsg函数将文件描述符发送至父子进程间的通信管道

    int ret = recvmsg(pipefd, &msg, 0);
    ERROR_CHECK(ret, -1, "recvmsg");
    int * p = (int*)CMSG_DATA(pcmsg);
    *pfd = *p;//读取文件描述符的值,并传给外界的变量
    return 0;
}

客户端代码:

#client.c

#include <func.h>

int main()
{
    //创建客户端的套接字
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(clientfd, -1, "socket");

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    //指定使用的是IPv4的地址类型 AF_INET
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(8080);
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    //连接服务器
    int ret = connect(clientfd, (struct sockaddr*)&serveraddr, 
                      sizeof(serveraddr));
    ERROR_CHECK(ret, -1, "connect");
    printf("connect success.\n");

    //进行文件的接收
    //1. 先接收文件的名字
    //1.1 先接收文件名的长度
    int length = 0;
    ret = recv(clientfd, &length, sizeof(length), 0);
    printf("filename length: %d\n", length);
    //1.2 再接收文件名本身
    char buff[1000] = {0};
    ret = recv(clientfd, buff, length, 0);
    printf("1 recv ret: %d\n", ret);
    int fd = open(buff, O_CREAT|O_RDWR, 0644);
    ERROR_CHECK(fd, -1, "open");

    //2. 再接收文件的内容
    //2.1 先接收文件内容的长度
    ret = recv(clientfd, &length, sizeof(length), 0);
    printf("fileconent length: %d\n", length);

    int total = 0;
    int len = 0;//每一个分片的长度
    //2.2 再接收文件内容本身
    while(total < length) {
        recv(clientfd, &len, sizeof(len), MSG_WAITALL);
        if(len != 1000) {
            printf("slice len: %d\n", len);
            //printf("total: %d bytes.\n", total);
        }
        memset(buff, 0, sizeof(buff));
        //将recv函数的第四个参数设置为MSG_WAITALL之后,
        //表示必须要接收len个字节的数据之后,才会返回
        ret = recv(clientfd, buff, len, MSG_WAITALL);// ret <= len
        //printf("slice %d bytes.\n", ret);
        if(ret > 0) {
            total += ret;
            write(fd, buff, ret);//写入本地文件
        }
    }

    close(fd);
    close(clientfd);

    return 0;
}

客户端main函数:

要点:

和服务端代码类似,步骤为:

  1. 创建客户端套接字
  2. 存储服务端网络地址
  3. 用connect函数连接服务器
  4. 进行文件的接收

先进行文件名的接收,再进行文件内容的接收,注意若文件内容较大时要将recv第四个参数设置为MSG_WAITALL来等待接收完全结束。

      5. 回收文件描述符

声明:未引入进程池退出机制,因为实在是太长了,改日再更!

其实是我还没学会,嘿嘿。

但是,岂有才情似沉阳?

勇敢阳阳,不怕困难!冲呀!

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值