JDFS:一款分布式文件管理实用程序第二篇(更新升级、解决一些bug)
一 前言
本文是《JDFS:一款分布式文件管理实用程序》系列博客的第二篇,在上一篇博客中,笔者向读者展示了JDFS的核心功能部分,包括:服务端与客户端部分的上传、下载功能的实现,epoll的运用,线程池的运用等。当然目前JDFS还仅仅支持上传、下载功能,还不具备分布式文件管理的功能,这些都会在后续的开发过程中加进来。在写博客的过程中,笔者发现最好是每完成一个小的功能就及时用博客记录下来,如果等功能全部实现完成后再写博客的话,一方面由于功能点比较多,博客写起来会比较费力;另一方面由于时间间隔太长,有些有价值的细节恐怕会忘记。所以最好是增量式写博客,每实现一个关键点的功能,及时用博客记录下来。本文是在该系列博客第一篇的基础上做了部分更新升级以及解决一些小bug.当然主要针对的是上传部分的功能。如果读者对这篇博客的背景不是太了解的话,请先移步笔者的上一篇博客:点击我 。
根据上一篇博客我们知道,JDFS的服务端主程序在epoll里面先recv客户端的数据,然后解析头部,根据请求类型,把作业交给线程池来执行。对于查询、下载部分的功能这是没有问题的,因为查询、下载部分客服端只是发送一个头部过来,服务端接收后解析的过程不会太占用多少时间。而如果是上传功能的话,服务端recv到的数据不仅包含头部而且包含客服端期望上传的文件实体的数据,而笔者的本意是让线程池来接收数据的,所以这个代码的实现与笔者的期望是矛盾的。本文首先就会对这一点进行更新改进,使得查询、上传、下载都可以并行的被线程池来执行。
另外在上一篇博客中,上传部分的功能代码比较粗糙,这次也进行了一些更新改进。在笔者测试上传功能的时候,发现了一些偶尔出现而且不容易重现的bug,而下载功能目前为止在笔者的测试过程当中还没有遇到过bug。所以从代码实现以及测试的过程来看,上传部分的功能要比下载部分复杂、更难调试。具体代码实现请移步笔者的github链接:点击我。
PS: 本篇博客是博客园用户“cs小学生”的原创作品,转载请注明原作者和原文链接,谢谢。
二 上传功能演示
在上一篇博客中,笔者展示了上传功能的截图,但那个只有一个客户端在向服务端上传文件,在这里再补充一个同时有两个客服端向服务端上传文件的截图。
在此次的上传中(使用shell脚本来提交),两个客服端分别同时向服务端上传不同的文件:CRLS-en.pdf和CRLS-e.pdf. 图左半边是服务端打印的一些信息,我们可以清晰的看到服务端交叉的接收CRLS-e.pdf和CRLS-en.pdf。图右半边是客服端打印的一些消息,我们也可以清晰的看到客服端也是交叉上传两个文件。服务端最后三次接收是:CRLS-e.pdf、CRLS-en.pdf、CRLS-en.pdf,客服端最后三次上传的是CRLS-en.pdf、CRLS-e.pdf、CRLS-en.pdf,可见客服端上传和服务端接收的文件的次序并不是一致的。但是从图中我们也可以很容易的发现:对于同一个文件,服务端接收的次序和客服端发送的次序是一致的。
下图是服务端接收完成后的截图:
三 改进
1. 修改服务端epoll框架,使得上传也能并行化
for(int i=;i<num_of_events_to_happen;i++){
struct sockaddr_in client_socket;
int client_socket_len=sizeof(client_socket);
if(*server_listen_fd==event_for_epoll_wait[i].data.fd){
int client_socket_fd=accept(*server_listen_fd,(struct sockaddr *)&client_socket,&client_socket_len);
if(client_socket_fd==-){
perror("Http_server_body,accept");
continue;
} event_for_epoll_ctl.data.fd=client_socket_fd;
event_for_epoll_ctl.events=EPOLLIN; epoll_ctl(epoll_fd,EPOLL_CTL_ADD,client_socket_fd,&event_for_epoll_ctl); }else if(event_for_epoll_wait[i].events & EPOLLIN){ int client_socket_fd=event_for_epoll_wait[i].data.fd;
if(client_socket_fd<){
continue;
} callback_arg *cb_arg=(callback_arg *)malloc(sizeof(callback_arg));
cb_arg->socket_fd=client_socket_fd;
threadpool_add_jobs_to_taskqueue(pool, Http_server_callback, (void *)cb_arg); //epoll delete client_fd }
}
如上述代码是服务端主程序使用epoll不断接收客服端连接、发送数据的主要逻辑部分。在第16行,原来的代码是先接收,然后解析头部,根据具体的请求再将之打包成作业加入到作业队列,然后线程池的线程就会来执行之。现在不这样做了,只要服务端epoll监听到读请求,就把该读请求打包成作业交给线程池来处理。线程相关函数会负责从传入的客户端连接的socket fd上读数据,然后根据具体请求再做不同操作。整个逻辑很简单,就是把要干的事情从服务端推迟到线程池里面去做。前文提到的打包作业是这样的:以前服务端解析完头部后,分别把三个代表查询、下载、上传的回调函数指针设置好,加入到作业队列里面。按照本文所述的新方法,服务端不用关心具体的操作是什么,只需要把回调函数指针Http_server_callback()和参数client_socket_fd传递到线程池就行了。而Http_server_callback()是新添加的一个函数,其代码如下:
void *Http_server_callback(void *arg){ if(arg==NULL){
printf("Http_server_callback,argument error\n");
exit();
} callback_arg *cb_arg=(callback_arg *)arg;
int client_socket_fd=cb_arg->socket_fd;
memset(cb_arg->server_buffer, , sizeof(http_request_buffer)+);
int ret=recv(client_socket_fd,cb_arg->server_buffer,sizeof(http_request_buffer)+,);
if(ret!=(+sizeof(http_request_buffer))){
close(client_socket_fd);
return (void *);
} http_request_buffer *hrb=(http_request_buffer *)(cb_arg->server_buffer);
if(hrb->request_kind==){ callback_arg_query cb_arg_query;
cb_arg_query.socket_fd=client_socket_fd;
cb_arg_query.server_buffer=cb_arg->server_buffer;
cb_arg_query.server_buffer_size=cb_arg->server_buffer_size;
strcpy(cb_arg_query.file_name, hrb->file_name); Http_server_callback_query((void *)(&cb_arg_query)); }else if(hrb->request_kind==){ callback_arg_upload cb_arg_upload;
cb_arg_upload.socket_fd=client_socket_fd;
cb_arg_upload.server_buffer=cb_arg->server_buffer;
cb_arg_upload.server_buffer_size=cb_arg->server_buffer_size;
cb_arg_upload.range_begin=hrb->num1;
cb_arg_upload.range_end=hrb->num2; strcpy(cb_arg_upload.file_name, hrb->file_name); Http_server_callback_upload((void *)(&cb_arg_upload)); }else if(hrb->request_kind==){ callback_arg_download cb_arg_download;
cb_arg_download.socket_fd=client_socket_fd;
cb_arg_download.server_buffer=cb_arg->server_buffer;
cb_arg_download.server_buffer_size=cb_arg->server_buffer_size;
cb_arg_download.range_begin=hrb->num1;
cb_arg_download.range_end=hrb->num2; strcpy(cb_arg_download.file_name, hrb->file_name); Http_server_callback_download((void *)(&cb_arg_download)); }else{ } }
2. 上传部分代码的改进
int recv_size=0;
1 while(){
int ret=recv(client_socket_fd,server_buffer+recv_size,range_end-range_begin+-recv_size,);
if(ret<=){
perror("Http_server_callback_upload,recv in while");
break;
} recv_size+=ret;
if(recv_size==(range_end-range_begin+)){
break;
} }
上面是服务端上传功能的部分逻辑,在一个while无限循环中,服务端接收客服端发送过来的[n,m]区间的数据,因为recv一次不一定能够接收完整个[n,m]区间的数据,因此需要在循环里面不断地接收,直到接收到的数据达到m-n+1的长度,这个时候就用break跳出循环。另外如果recv的返回值ret<=0,则表明网络出错或者客服端断开网络,此时也要break出去。
跳出while循环后,要判断是正确接收到了完整数据还是出错了,并且给客服端发送一个ack消息,客服端根据ack消息来决定下一步的走向,该部分逻辑如下:
if(recv_size==(range_end-range_begin+)){ int ret1=fwrite(server_buffer,range_end-range_begin+, , fp);
memset(server_buffer, , sizeof(http_request_buffer)+);
http_request_buffer *hrb=(http_request_buffer *)server_buffer;
if(ret1==){
hrb->request_kind=;
hrb->num1=range_begin;
hrb->num2=range_end;
}else{
hrb->request_kind=;
} int ret=send(client_socket_fd,server_buffer,sizeof(http_request_buffer)+,); if(ret!=(sizeof(http_request_buffer)+)){ perror("Http_server_callback_upload, send ack to client"); }else{ } }else{
在第6行判断如果数据接收完毕并且成功写入到服务端,则给客服端发送一个正确接收并写入的消息,或者设置request_kind=4,代表服务端接收失败,客服端需要重新发送数据。更详细的代码请读者直接阅读github里面的源代码。
四 一些遇到的问题、bug
1. 在后来跑JDFS的时候,发现会提示no host to route的错误,发现原因是虚拟Ubuntu的ip地址发生了变化,执行下ifconfig命令,用最新的ip地址就可以了。
2.
int ret=fread(upload_buffer+sizeof(http_request_buffer)+, size_of_last_piece, , fp);
if(ret!= && ret!=){
printf("JDFS_http_upload,fread failed,ret=%d\n",ret);
exit();
}
上面这段代码是客户端读取文件的最后一段数据准备上传,下面是一个if判断语句,之前判断语句是if(ret!=1){...},而且之前一直上传也没出现过fread的错误,但是这次却发生客户端上传文件都能成功但是到了传送最后一部分数据的时候fread老是提示错误,经分析fread由于到达了文件尾部,所以返回0,在if语句里面加上这个判断就没问题了。但是奇怪的是,笔者之前好多次上传都成功并没有提示这个错误啊。
3. 在下载功能部分,客户端从服务端recv数据,一旦recv返回值小于等于0,则不管错误原因的类型,客户端无条件重新连接到服务端,并请求数据。而客户端上传数据到服务端,某种程度上比较像服务端从客户端下载数据,不同的是服务端此时是被动从客户端下载数据。那么此时如果服务端接收数据时recv返回错误的结果,服务端不应该重新连接客户端请求那部分失败的数据,而应该是告诉客户端数据接收失败,由客户端决定此时应该怎么办。为什么呢?一方面,服务端是被动的服务客户端的请求,如果服务端主动向客服端重新连接,并请求那部分失败的数据,此时服务端变成了客户端,客户端变成了服务端,这不符合C/S的模型;另一方面,服务端应该是服务大量并发的请求,也不应该因为某一个请求服务失败,就主动重新连接客户端,请求数据,万一这个过程老是出错,服务端岂不是一直陷入特定的请求泥潭,而大量其他的请求得不到服务?
所以,服务端只需要告诉客户端该请求服务失败就行了,剩下的客户端要么重新向服务端提交请求,要么终止执行或者其他。笔者一开始想的比较简单,在服务失败的时候,服务端关闭socket fd,这样客户端检测到链接被关闭,自然就知道服务失败了。在客户端逻辑里,如果send失败,只发送了部分数据,也关闭链接,这样服务端就检测到socket fd链接被关闭。这么做结果引发了很多问题,原因在于send()端一次发送的数据,recv()端可能要分好几次才能接收完毕,如果一方已经close套接字,而另一方还没有接收完数据,就因为对端close了而接收失败,因此close的时机不好协调。
于是取消了用close()传递消息的方法,而改为:线程池Job的一次操作结果,无论有没有达到目的,都给客户端发送一条ack确认信息,客户端根据ack信息如果成功则继续,否则重新上传失败的数据。这么做基本上解决了问题,但是很奇怪的是,非常偶然的情况下会出现一个bug:在重新启动server端,然后执行客户端的时候,服务端调用recv的时候会提示bad file descriptor的错误;在重新启动server一到两次后又恢复正常了,这个错误由于非常难重新,目前还没有找到问题的根源所在。
4. 在调试的过程中,还遇到过另外一个问题:客户端是执行的上传功能,而服务端有时候会解析为查询的操作。经分析可能原因如下:有可能客户端发送了[n1,m1] [n2,m2]两段数据, 而服务端接收的序列很可能是这样的,[n1,b],[b+1,m1],[n2,m2]. 服务端在接收完[n1,b]后(由于网络原因[n1,m1]很可能不是一次接收完成),下一次接收[b+1,m1]的时候误以为是一段新的数据,并把开头若干字节的数据当做头部处理,而头部恰好有一部分数据是0,而0就代表着查询请求。但是经过研究代码并没有发现明显会产生上述场景的条件。由于修复其他bug后,导致这个错误没能继续重现,现在也很难找到根源,也留到以后再研究吧。
五 结束语
至此本篇博客就结束了,此篇博客主要解决了上传功能的并行化问题,以及修复了一些bug,当然还有一些不容易重新的bug,其原因有待进一步的分析解决。截止目前JDFS的上传、下载功能已趋于完成了,下一篇博客开始将在此基础上增加分布式文件管理的功能,比如把本地文件冗余地存储于不同的虚拟节点上,查询虚拟文件系统,从虚拟文件系统上读取目标文件等。欢迎继续关注本系列博客,我们下期再见。
JDFS:一款分布式文件管理实用程序第二篇(更新升级、解决一些bug)的更多相关文章
- JDFS:一款分布式文件管理实用程序第一篇(线程池、epoll、上传、下载)
一 前言 截止目前,笔者在博客园上面已经发表了3篇关于网络下载的文章,这三篇博客实现了基于socket的http多线程远程断点下载实用程序.笔者打算在此基础上开发出一款分布式文件管理实用程序,截止目前 ...
- JDFS:一款分布式文件管理系统,第三篇(流式云存储)
一 前言 看了一下,距离上一篇博客的发表已经过去了4个月,时间过得好快啊.本篇博客是JDFS系列的第三篇博客,JDFS的目的是为了实现一个分布式的文件管理系统,前两篇实现了基本的上传.下载功能,但是那 ...
- JDFS:一款分布式文件管理系统,第四篇(流式云存储续篇)
一 前言 本篇博客是JDFS系列博客的第四篇,从最初简单的上传.下载,到后来加入分布式功能,背后经历了大量的调试,尤其当实验的虚拟计算结点数目增加后,一些潜在的隐藏很深的bug就陆续爆发.在此之前笔者 ...
- JDFS:一款分布式文件管理系统,第五篇(整体架构描述)
一 前言 截止到目前为止,虽然并不完美,但是JDFS已经初步具备了完整的分布式文件管理功能了,包括:文件的冗余存储.文件元信息的查询.文件的下载.文件的删除等.本文将对JDFS做一个总体的介绍,主要是 ...
- UItableView UIcollectionView下拉刷新会跳动?看了此篇就能解决这个Bug了
顺序如下: 1.数组添加: for (id model in modellist.list) { IDSCommentWeplayList *commentListModel = [I ...
- 如何制作一款HTML5 RPG游戏引擎——第二篇,烟雨+飞雪效果
今天我们来实现烟雨+飞雪效果.首先来说,一款经典的RPG游戏难免需要加入天气的变化.那么为了使我们的RPG游戏引擎更完美,我们就只好慢慢地实现它. 本文为该系列文章的第二篇,如果想了解以前的文章可以看 ...
- 跟我学SpringCloud | 第二篇:注册中心Eureka
Eureka是Netflix开源的一款提供服务注册和发现的产品,它提供了完整的Service Registry和Service Discovery实现.也是springcloud体系中最重要最核心的组 ...
- [转]Android开源项目第二篇——工具库篇
本文为那些不错的Android开源项目第二篇--开发工具库篇,主要介绍常用的开发库,包括依赖注入框架.图片缓存.网络相关.数据库ORM建模.Android公共库.Android 高版本向低版本兼容.多 ...
- Android开源项目第二篇——工具库篇
本文为那些不错的Android开源项目第二篇——开发工具库篇,**主要介绍常用的开发库,包括依赖注入框架.图片缓存.网络相关.数据库ORM建模.Android公共库.Android 高版本向低版本兼容 ...
随机推荐
- 【Azure】Azure学习方法和学习资料
学习方法: DEX为入门培训,fundamental book进阶材料,Azure 官方为补充权威材料,网站一些大拿的Blog是很多实践精华,推荐阅读. 推荐教材和学习内容: EDX培训:http:/ ...
- [Python] Spark平台下实现分布式AC自动机(一)
转载请注明出处:http://www.cnblogs.com/kirai/ 作者:Kirai 零.问题的提出 最近希望在分布式平台上实现一个AC自动机,但是如何在这样的分布式平台上表示这样的非线性数据 ...
- grid实例(Asp.net)
<link href="../../js/jqGrid/css/ui.jqgrid.css" rel="stylesheet" type="te ...
- node express安装
我们现在全局安装只需要安装这个命令行工具就可以,指令如下: npm install -g express-generator 这时我们就着手安装express框架,指令如下: express blog ...
- js日期转化(计算一周的日期)
之前做项目的时候遇到过一个日期转化的问题,一个日期控件和近一天,近七天和近一月的的联动效果.发现自己不会,后来就百度了一下解决了这个问题. 现在抽空又写了一个时间转化的案例(计算一周的日期),因为之前 ...
- EzHttp 使用Https协议时证书如何部署
今天为EzHttp增加了https支持, EzHttp介绍见这里:使用EzHttp框架 开发基于HTTP协议的CS轻应用 服务端启动时会创建自签名证书,并将其绑定到启动参数url对应的端口上. 服务端 ...
- 懵懂oracle之存储过程
作为一个oracle界和厨师界的生手,笔者想给大家分享讨论下存储过程的知识,因为在我接触的通信行业中,存储过程的使用还是占据了一小块的地位. 存储过程是什么?不得不拿下百度词条的解释来:"存 ...
- 线段树区间更新操作及Lazy思想(详解)
此题题意很好懂: 给你N个数,Q个操作,操作有两种,‘Q a b ’是询问a~b这段数的和,‘C a b c’是把a~b这段数都加上c. 需要用到线段树的,update:成段增减,query:区间求 ...
- 「七天自制PHP框架」第二天:模型与数据库
往期回顾:「七天自制PHP框架」第一天:路由与控制器,点击此处 什么是模型? 我们的WEB系统一定会和各种数据打交道,实际开发过程中,往往一个类对应了关系数据库的一张或多张数据表,这里就会出现两个问题 ...
- 微信公众号开发笔记1(nodejs开发的)
本篇记录了微信公众号开发的一些笔记 一.微信服务器与我们服务器的交流 微信开发者拥有自己的服务器,在我们服务器上可以与微信服务器进行交流.既然可以交流,那就必定需要前提条件(微信认证),也就是说,只有 ...