毕业设计的代码基本完成,编程能力还有待提高,现在总结一下。
功能简介:由于发现arm板和PC机(虚拟机的linux系统)传输文件比较不便利,所以任务就是实现linux和arm版的文件传输,包括界面和网络两部分。
第一个问题:
客户端架构:对于架构,吃了不少苦头(毕竟自己还是菜鸟)。开始就是直接网络代码和界面代码没有分的很开,当界面发送文件按钮一按下,对应的触发函数调用网络发送函数。这导致了两个问题。一个就是进度条和进度数值无法在文件发送或者接收过程更新(原因就是界面和网络代码可以说在一个线程里,虽然界面更新使用定时器,但是在文件读写过程中,暂时无法触发了)。第二个问题,当忽略某种情况时候,修改代码起来,改动过大。例如忽略服务端如果存在相同文件的情况,这除了要修改代码使得显示小窗口提示是否覆盖,还要修改网络代码支持是否覆盖服务端的文件,这还是有点麻烦。
改进的做法就是把界面代码和网络代码分开,客户端主函数创建两个函数,一个处理界面问题,一个处理网络问题,通过一个结构体实现两者间的信息通信:
init_MainBridge(&mainbridge);
if(pthread_create(>k_tid,NULL,gui_main,&mainbridge) !=0)
{
My_Debug("%s %d:can not create gtk pthread\n");
}
if(pthread_create(&client_tid,NULL,client_main,&mainbridge) !=0)
{
My_Debug("%s %d:can not create client pthread\n");
}
client_main就是客户端的网络代码,而gui_main就是处理界面 代码,包括主界面和小界面。这个做法解决的问题,一个就是进度条和进度标签更新,因为此时进度条和进度标签更新和文件发送和接收过程是在两个不同线程,用mainbridge结构体来进行联系。
客户端效果图:
而服务端的架构就比较简单:
struct serverDoRequist{
reqType CmdType; //客户端命令
int (* func)(int fd); //命令处理函数
}serverDoRequistType[]=
{
{REQ_NONE,NULL},
{REQ_SEND,server_recieve_noexit_file},
{REQ_FORCE_SEND,server_recieve_canexit_file},
{REQ_RECV,server_send_file}
};
void process(int connfd)
{
char message[PKG_SIZE_ALL];
struct serverDoRequist *aRequist;
for(;;)
{
if(receive_message(connfd,message)<0)
{
My_Debug("%s %d:ignore message\n",__FUNCTION__,__LINE__);
break;
}
if((aRequist = parse_message(message))==NULL)
{
My_Debug("%s %d:parse message err\n",__FUNCTION__,__LINE__);
send_reply_message(connfd,message,NULL,REPLY_FAIL);
continue;
}
send_reply_message(connfd,message,REPLY_SUCCESS);
aRequist -> func(connfd);
}
第二个问题:
此次还有用到很多小型窗口:
正确窗口,例如:
还有失败窗口,例如:
选择窗口,例如:
首先不可能为每一个小窗口都写一个函数的,所以我使用了miniWind函数来实现所有的小窗口,代码如下:
#include "gui.h"
#include "gui_set.h"
static void selectNo_event(GtkWidget *widget,gpointer data )
{
gint *selectResult = (gint *)data;
*selectResult = SELECTBUTTON_NO;
gtk_main_quit();
}
static void selectYes_event(GtkWidget *widget,gpointer data )
{
gint *selectResult = (gint *)data;
*selectResult = SELECTBUTTON_YES;
gtk_main_quit();
}
static gint delete_event(GtkWidget *widget,GdkEvent *event,gpointer data)
{
gtk_main_quit();
return FALSE;
}
/*select and err window*/
/*return value about select window
*Yes:1;No:0
*/
int miniWind(const char *printStr,MiniWind mwIndex)
{
GtkWidget *miniwindow;
GtkWidget *errLabel;
GtkWidget *sureButton;
GtkWidget *yesButton;
GtkWidget *noButton;
GtkWidget *table;
gint selectResult;
/*set miniwindow*/
miniwindow = gtk_window_new(GTK_WINDOW_POPUP);
gtk_window_set_position (GTK_WINDOW(miniwindow),GTK_WIN_POS_CENTER);
gtk_window_set_default_size(GTK_WINDOW(miniwindow),200,100);
/*add table to miniwindow*/
table = gtk_table_new(3,3,TRUE);
gtk_container_add(GTK_CONTAINER(miniwindow),table);
/*add label to table*/
if(printStr!=NULL)
{
errLabel = gtk_label_new (NULL);
set_label_markup(GTK_LABEL (errLabel),printStr);
gtk_table_attach_defaults(GTK_TABLE(table),errLabel,0,3,0,2);
}
/*add sure button to table*/
if((mwIndex == ERRWIND) || (mwIndex == SUCCESSWIND))
{
if(mwIndex == ERRWIND)
fill_image_for_widget(miniwindow,"./pic/errWind.png");
else
fill_image_for_widget(miniwindow,"./pic/successWind.png");
sureButton = gtk_button_new_with_label("确定");
gtk_table_attach_defaults(GTK_TABLE(table),sureButton,1,2,2,3);
g_signal_connect(G_OBJECT(sureButton),"clicked",
G_CALLBACK(delete_event),NULL);
}
if(mwIndex == SELECTWIND)
{
fill_image_for_widget(miniwindow,"./pic/warnWind.png");
yesButton = gtk_button_new_with_label("Yes");
gtk_table_attach_defaults(GTK_TABLE(table),yesButton,0,1,2,3);
g_signal_connect(G_OBJECT(yesButton),"clicked",
G_CALLBACK(selectYes_event),&selectResult);
noButton = gtk_button_new_with_label("No");
gtk_table_attach_defaults(GTK_TABLE(table),noButton,2,3,2,3);
g_signal_connect(G_OBJECT(noButton),"clicked",
G_CALLBACK(selectNo_event),&selectResult);
}
gtk_widget_show_all(miniwindow);
gtk_main();
gtk_widget_destroy(miniwindow);
return selectResult;
}
第三个问题:
文件的传输过程也花了我不少时间。一个就是如何发送和接受一个文件。我的最终的做法:
发送:
1、客户端发送请求(发送文件),等待服务端反馈。
2、如果反馈成功,那么客户端发送文件名,等待服务端的反馈包。
3、若反馈成功,客户端就发送文件长度给服务端(需要文件的长度原因有三:需要更新进度条、发送方可以判断文件是否读完,接收方可以判断是否接收完毕),等待服务端的反馈。
4、若反馈成功就发送文件内容,发送完毕,等待服务端服务端的反馈包。
接收:
1、客户端发送请求(接收文件),等待服务端反馈。
2、如果反馈成功,那么客户端发送待接收的文件名,等待服务端的反馈包。
3、若反馈成功,客户端就接收服务端发来的待接收文件长度(原因有三和发送类同),发送反馈包给服务端。
4、客户端接收文件内容,接收完毕,发送反馈包给服务端。
所谓的反馈包实际就是确认包。例如,客户端发送给服务端文件名字,服务端反馈包就说明服务端已经存在该文件了,是否还有继续。又例如,待发送的文件,服务端根本没有这个文件存在,反馈错误。
所以对于这个操作过程,采取的就是传输的数据都是一个个数据包。每一个数据包,都包含包头和包数据,其中包头表明这个是反馈包还是请求包还是数据包,若是请求包,包头要区分是发送还是接收,数据包(包括文件名还是文件内容)和反馈包(成功还是失败还是出错类型)。包数据存放的就是有效数据部分,例如文件名或者文件内容(刚开始还包括服务端反馈的额出错信息,例如文件已经存在,后来发现会增加复杂度就舍弃了)。
#define PKG_SIZE_HEAD 4 //包头大小
#define PKG_TYPE 0 //包头第一个字节表示包数据的格式
#define PKG_LEN_L 1//包头第二、三个字节表示包的总长度
#define PKG_LEN_H 2
#define PKG_TYPE_CLASSIFY 3 //包头第四个字节,是数据格式的分类
#define PKG__SIZE_VADATA (1000) //包有效数据大小
#define PKG_SIZE_ALL (PKG_SIZE_HEAD+PKG__SIZE_VADATA)
/*包数据的格式*/
ttypedef enum pkg_type{TYPE_NONE,TYPE_REQ,TYPE_DATA,TYPE_REPLY,TYPE_END} pkgType;
/*类型分类*/
typedef enum classify_req{REQ_NONE,REQ_SEND,REQ_FORCE_SEND,REQ_RECV} reqType;
typedef enum classify_data{DATA_NONE,DATA_FILENAME,DATA_FILELEN,DATA_FILECONTENT}dataType;
typedef enum classify_reply{REPLY_NONE,REPLY_SUCCESS,REPLY_FAIL,REPLY_FILEEXIT,REPLY_FILENOEXIT} replyType;
/*以下宏定义实现对包头的一些设置工作*/
#define PVAL(buf,pos,type) (*((type *)(buf + pos)))
#define CVAL(buf,pos) PVAL(buf,pos,unsigned char) //读取包头的第pos位字节数值
#define SCVAL(buf,pos,x) PVAL(buf,pos,unsigned char) = (x)//对包头的第pos位字节设置为x
//设置传输包的头
#define messageHead_set(pkgType,classifyType,len,message) {\
SCVAL(message, PKG_TYPE,pkgType); \
SCVAL(message,PKG_TYPE_CLASSIFY,classifyType); \
SCVAL(message, PKG_LEN_L,(len%256)); \
SCVAL(message, PKG_LEN_H,(len/256)); \
}
//得到传输包
#define messageHead_get(pkgType,classifyType,len,message) { \
pkgType = CVAL(message, PKG_FLAG_DATA); \
classifyType = CVAL(MESSAGE,PKG_TYPE_CLASSIFY); \
len = CVAL(message, PKG_LEN_L)|CVAL(message, PKG_LEN_H)<<8; \
}
经常有人说“一个函数实现一个功能”,这次还是蛮有体会,一方面在于如果函数的书写不够好,会导致你修改代码越修改到后面也难修改,并且如果不重构,函数功能会越混乱。再者就是服务端和客户端有很多相似代码,例如发送和接收文件两方都需要,所以需要提取共用函数。这也方便调试BUG,调试BUG的过程要远大于自己书写代码的过程(关键部分的出错信息打印也是调试BUG总要部分)
最后一个问题
一个东西做出来,从使用上说要别人很容易就会使用;从代码上上来看,除了易读外也要容易扩展功能。界面的好处可以让别人容易使用,但是经常要依赖一些库,没有库就用不了,这样从方便使用者角度来说就得不偿失,所以如果为了便于使用,并且功能又那么简单,觉得还是不要用界面,这样使得代码不易移植。
我的毕设硬性要求需要界面支持,我选择的是GTK,GTK对于linux系统来说,系统可选择性自带,这样就不用装得那么麻烦,再者就是GTK是使用C语言编写。