内部笔记——方便理解,自用(如有错误或不准确的地方请私信我,我会进行更正。)
分析代码——先看头文件中有哪些接口,再看调用关系。
#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、各类函数接口
- 创建子进程——传入参数:子进程结构体,包含进程id、通信管道、当前状态。
int makeChild(process_data *, int );
- 执行子进程任务——参数:传入通信管道中的文件描述符(代指任务内容)
int doTask(int pipefd);
- 父子进程间传递文件描述符——父进程用sendFd将收到的客户端的任务fd传给管道,子进程再用recvFd从管道中将fd(pipefd)取出
int sendFd(int pipefd, int fd); int recvFd(int pipefd, int * pfd);
- 与客户端建立tcp连接并让epoll的监督员epfd添加或删除读就绪事件
int tcpInit(const char * ip, unsigned short port); int epollAddReadEvent(int epfd, int fd); int epollDelReadEvent(int epfd, int fd);
传输文件数据
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函数:
要点:
和服务端代码类似,步骤为:
- 创建客户端套接字
- 存储服务端网络地址
- 用connect函数连接服务器
- 进行文件的接收
先进行文件名的接收,再进行文件内容的接收,注意若文件内容较大时要将recv第四个参数设置为MSG_WAITALL来等待接收完全结束。
5. 回收文件描述符
声明:未引入进程池退出机制,因为实在是太长了,改日再更!
其实是我还没学会,嘿嘿。
但是,岂有才情似沉阳?
勇敢阳阳,不怕困难!冲呀!