今天,来小结一下纠结我几个小时的linux C。需求是这样的,用c实现tcp的文件上传与下载
一开始,很容易想到的上传思路是,直接在内存中开一块buffer,得到一个file description后,进行一读一发。char buffer[1024];
int fd = open(path, O_RDONLY);
int n = read(fd, buffer, 1024);
if(n<0) {
perror("read");
}
buffer[n] = 0;
write(socket, buffer, n);
//...
然而,其实在linux内核中已经实现了一种更为高效的方法,sendfile不需要频繁的调用read/write,也不需要开辟buffer,减少了内核函数的调用,提高性能。
函数说明定义int sendfile(int fd, int s, off_t offset, off_t *len, struct sf_hdtr *hdtr, int flags);
解释argument nameexplantionfd需要发送的文件的fd(file description)
ssocket的fd
offset文件从那开始发,NULL表示为0
len输出参数,输出一共发送了多少byte,包括后面的hdtr
hdtr额外发送的头和尾
flags设置为0即可
关于flags, man page原文如下:The flags parameter is reserved for future expansion and must be set to 0. Any other value will cause sendfile() to return EINVAL.
意思是,flags是为了后面备用的,现在还没实现,现在传入0即可。
下面着重解释len与hdtr参数 结构体sf_hdtr, 成员如下struct sf_hdtr {
struct iovec *headers; /* pointer to header iovecs */
int hdr_cnt; /* number of header iovecs */
struct iovec *trailers; /* pointer to trailer iovecs */
int trl_cnt; /* number of trailer iovecs */
};
而,结构体iovecstruct iovec {
void * iov_base;/* [XSI] Base address of I/O memory region */
size_t iov_len;/* [XSI] Size of region iov_base points to */
};
可以看到,iovec数据就是表示一段iov_len长度的数据区,而sf_hdtr则是2个iov_len数组(指针)。
headers就是发送文件数据前发送的数据段,trailers则是跟在文件数据EOF之后的。
解释完该方法后,其实上传文件,只需要调用该方法即可,而headers和trailers可以用来界定文件数据,ngnix osx版本中,便有使用该方法。
为了简化文件数据划分的逻辑,我未采用,http协议中类似Content-Length字段来表示文件的大小,从而拼接出完整的文件内容,而是简单的在文件数据头尾加上了自定义的字符串。
代码发送文件bool _sendFile(int out_fd, const char* file) {
int fd = open(file, O_RDONLY);
char* tmp = strrchr(file, '/');
const char* filename = tmp!=NULL? tmp+1: file;
if(fd==-1) {
char b[1024];
sprintf(b, "open failed %s", file);
perror(b);
return false;
}
struct stat state;
fstat(fd, &state);
printf("sending File %s ...n", file);
off_t offset = 0;
off_t len = 0; // 必须初始化0, 不然下次重入时,会被旧值覆盖
char head[1024], sizehd[1024];
sprintf(head, "---file: %srn", filename); // 拼装头部字符串
// sprintf(sizehd, "---size: %lldrnrn", state.st_size);
struct sf_hdtr hdtr = NULL;
iovec headers = NULL, trailers = NULL;
headers.iov_base = head;
headers.iov_len = strlen(head);
// trailers.iov_base = (void *)"file---rn"; //todo: don't recv sometimes ??
// trailers.iov_len = 9;
hdtr.headers = &headers;
hdtr.hdr_cnt = 1;
hdtr.trailers = NULL;
hdtr.trl_cnt = 0;
if(0 == sendfile(fd, out_fd, offset, &len, &hdtr, 0)) {
close(fd);
write(out_fd, "file---rn", 9); // 未使用trailers,因为有时候上传大文件,trailers会丢失。
printf("sendFile %s success, return len: %lld.n", file, len);
return true;
} else {
close(fd);
write(out_fd, "file---rn", 9);
perror("sendfile");
return false;
}
}接收文件bool _receFile(FILE* &pfsile, char* buffer, ssize_t n, bool& receiveing, char* rfilename, int size) {
bool run = false;
char* last = NULL;
if(!receiveing && isfileHead(buffer)) {
memset(rfilename, 0, 50);
strcpy(rfilename, "data/");
if (stat(rfilename, NULL) == -1) {
mkdir(rfilename, 0700);
}
char name[40];
sscanf(buffer, "---file: %srn", name);
int othlen = 11+strlen(name);
int addonlen = n-othlen;
strcat(rfilename, name);
pfile = fopen(rfilename, "wb+"); //!! 以二进制打开文件
receiveing = true;
printf("Downloading %s ...nhead addon: %snn",
rfilename, buffer+othlen);
if(addonlen > 0) {
fwrite(buffer+othlen, addonlen, 1, pfile);
}
run= true;
}
if(receiveing && (last = fileTail(buffer, n))!=NULL) {
receiveing = false;
fwrite(buffer, last-buffer, 1, pfile);
fclose(pfile);
printf("Downloaded %s. and savedn", rfilename);
run= true;
} else if(receiveing && !run) {
printf("download chunk, size: %ldn", n);
if(n
receiveing = false;
fclose(pfile);
}
fwrite(buffer, n, 1, pfile);
run= true;
}
return run;
}
最后
其实还是会有bug的,比如---file: a.pngrn ... ---filern的数据,接收方buffer设置较小,不能容纳完整的---file标志,可能就不会被认为是file;或者结尾截断了。而对于上诉情况,应用层只能通过更复杂的代码逻辑来控制了。