Linux 下飞鸽传书设计实?span lang="EN-US">
1.pȝ功能
Ҏ(gu)飞鸽传书协议?span lang="EN-US"> linux 下实现飞鸽传输程?span lang="EN-US">,q且?span lang="EN-US"> windows 下飞鸽兼宏V具体功能模块包括用户上U?span lang="EN-US">,下线,h查看在线用户,收发消息,传送文?span lang="EN-US">/文g夹功能模块?span lang="EN-US">
2.具体实现
2.1 关键数据l构
/*命o(h)的结?span lang="EN-US">*/
typedef struct _command
{
int version;/*命o(h)的版?span lang="EN-US">*/
int seq;/*包编?span lang="EN-US">*/
char srcName[100];/*发送者姓?span lang="EN-US">*/
char srcHost[100];/*发送者主机名*/
int flag;/*命o(h)*/
char addtion[100];/*附加字段*/
}command;
/*在线用户信息*/
typedef struct _userInfo
{
char name[MAXLINE]; /*姓名*/
char host[MAXLINE]; /*L?span lang="EN-US">*/
char group[MAXLINE]; /*所在的l名*/
struct sockaddr_in addr; /*地址信息*/
struct _userInfo next; /*链表中下一?span lang="EN-US">*/
}userInfo;
/*在线用户列表*/
typedef struct _uList
{
userInfo *userListHead; /*链表?span lang="EN-US">*/
userInfo userListTail; /*链表?span lang="EN-US">*/
}uList;
/*消息队列*/
typedef struct _mesList
{
command *mesHead;
command *mesTail;
}mesList;
2.2 E序主要l构
本程序主要采用多U程l构,分ؓ(f) receive(接收消息), process(处理收到的消?span lang="EN-US">), sendData(发送文?span lang="EN-US">) 三个子线E。线E间通信互斥锁与 Posix 信号量进行通信?span lang="EN-US">
2.3 函数接口
(1) /*从文件描q符fd中读?span lang="EN-US">count个字W存?span lang="EN-US">buf?span lang="EN-US">*/
ssize_t readn(int fd,void *buf,size_t count)Q?span lang="EN-US">
(2) /*?span lang="EN-US">buf所指向的存储区中的len个字W吸入文件描q符fd?span lang="EN-US">*/
ssize_t writen(int fd,char *buf,int len);
(3) /*用于字符串{?span lang="EN-US">,|络传输中用gb2312~码Q?span lang="EN-US">linux?span lang="EN-US">gtk?span lang="EN-US">utf-8~码Q需要进行{?span lang="EN-US">*/
int code_convert(char *from_charset,char *to_charset,char *inbuf,int inlen,char *outbuf,int outlen);
(4) /*在用户链表中加入新用户信息,加入成功q回1Q否则返?span lang="EN-US">0,使用userInfoMutexq行U程间通信控制*/
int pushBack(uList *list,userInfo user);
(5) /*在用户链表中删除指定地址信息的用P删除成功后返?span lang="EN-US">1Q否则返?span lang="EN-US">0Q?span lang="EN-US">userInfoMutexq行U程间控?span lang="EN-US">*/
int delUser(uList *list, struct sockaddr_in addr);
(6) /*判断该用h否已l存在,已经存在则返?span lang="EN-US">1Q否则返?span lang="EN-US">0,使用userInfoMutexq行U程间控?span lang="EN-US">*/
int isExist(uList *list,struct sockaddr_in addr);
(7)清空用户链表Q释攄_(d)用于用户退出和用户h旉攄?span lang="EN-US">,使用userInfoMutexq行U程间控?span lang="EN-US">*/
int destroyList(uList *list);
(8)/*创徏命o(h)?span lang="EN-US">,comq回的命令字,flag 为消息标?span lang="EN-US">,addtion 为附加标?span lang="EN-US">*/
void createCmd(command & com,int flag,char addtion[])
(9)/*发送消息,com发送的消息Q?span lang="EN-US">servaddr发送的地址Q?span lang="EN-US">attach为文仉件信?span lang="EN-US">*/
void sendCmd(command com, struct sockaddr_in servaddr,char attach[]);
(10) /*把收到的消息加入到消息队列中*/
void addMes(mesList *mHead,command cmd);
(11) /*把消息队列中头部的节Ҏ(gu)息提取出来用于处?span lang="EN-US">*/
int delMes(mesList *mHead,command *cmd);
(12)/*初始化操作,飞鸽d时初始化消息链表Q用户链表,信号量,套接字信?span lang="EN-US">*/
void init();
(13)/*d操作,发送用户上U消?span lang="EN-US">*/
void login();
(14)/*解析收到的消息命令,提取各个字段*/
int analysisCmd(command *cmd,char *buf);
(15) /*接收消息U程处理函数,收到的消息加入消息队列中,通过信号?span lang="EN-US">waitNoFull?span lang="EN-US">waitNoEmpty和消息处理线E进行通信。消息队列用mesMutex与其他线E进行通信Q保证消息队列的正确?span lang="EN-US">*/
void *receive(void *arg);
(16)/*gtk界面中显C在U用户信?span lang="EN-US">*/
void showUser(uList *list);
(17)/*?span lang="EN-US">gtk界面中显C消?span lang="EN-US">*/
void showMessage(char *message);
(18)/*昄收到的信?span lang="EN-US">*/
void showRecvMessage(char *host,char *message);
(19)/*分析文g的信息,提取有用的字D?span lang="EN-US">*/
void fileAnalysis(char *recv,int *fNum,char *fName,int *fSize,int *fTime,int *fType);
(20) /*保存收到的单个文?span lang="EN-US">,saveNameZ存的文g?span lang="EN-US">*/
void saveSignalFile(char *saveName);
(21)/*分析目录附gQ获得目录文件的文g名,文g大小Q文件类?span lang="EN-US">*/
void getDirInfo(char *recv,char *fName,int *fSize,int *fType);
(22) /*保存目录,saveName保存的目?span lang="EN-US">*/
void saveDir(char *saveName);
(23)/*保存文g,recvType=1Z存文ӞrecvType=2Z存的目录,使用fileMutex来设|互斥?span lang="EN-US">*/
void saveFile();
(24)/*收到单个文g*/
void receiveSignalFile(char *recvFileName);
(25)/*收到单个目录*/
void receiveDir(char *recvDirName);
(26)/*接收文g*/
void receiveFile(command cmd);
(27)/*信号处理U程,从消息队列中取出消息q行处理*/
void *process(void *arg);
(28)/*发送消?span lang="EN-US">*/
int sendMes();
(29) /*文件名q行转换*/
char *transName(char *fileName);
(30)/*发送文?span lang="EN-US">*/
void sendFile();
(31)/*发送文件夹*/
void sendDir();
(32)/*用户点击h,h在线用户*/
void refresh();
(33) /*用户退?span lang="EN-US">*/
void quit();
(34)/*传输文gҎ(gu)据,递归函数*/
void transferDir(int fd,char *dir);
(35)/*监听TCP套接口,发送文件与文g夹线E?span lang="EN-US">*/
void *sendData(void *arg);
(36)/*创徏菜单*/
static void create_popup_menu(GtkWidget *menu,GtkWidget *view);
(37)/*叛_选中treeview,昄传送文件与文g夹菜?span lang="EN-US">*/
static gboolean showTreeView(GtkWidget *eventBox,GdkEventButton *event,GtkWidget *menu);
(38)/*选择要发送的文g */
static void selectFile();
(39)/*选择要发送的文g?span lang="EN-US">*/
static void selectDir();
(40)/*选择要保存的文g名或文g夹名*/
static void selectSaveFile();
3.ȝ
实现?span lang="EN-US">linux下飞鸽传书的基本功能Qƈ且能?span lang="EN-US">window下飞鸽进行通信Q传文g。熟(zhn)了(jin)linux下网l编E,多线E编E及(qing)U程间通信Q主要用C号量与互斥锁Q。但加密解密那块没有完成Q程序结构不是很好,界面做得太差。有I应该看看设计模?span lang="EN-US">.
界面截图Q界面比较垃?Q?/p>
附:(x)
飞鸽协议Q?http://bbs.chinaunix.net/viewthread.php?tid=1015775
在linux的网l编E中Q很长的旉都在使用select来做事g触发。在linux新的内核中,有了(jin)一U替换它的机Ӟ是epoll?br>
相比于selectQepoll最大的好处在于它不?x)随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的Q轮询的fd数目多Q自然耗时多。ƈ且,在linux/posix_types.h头文件有q样的声明:(x)
#define __FD_SETSIZE 1024
表示select最多同时监?024个fdQ当?dng)可以通过修改头文件再重编译内核来扩大q个数目Q但q似乎ƈ不治本?br>
epoll的接口非常简单,一共就三个函数Q?br>
1. int epoll_create(int size);
?
Z个epoll的句柄,size用来告诉内核q个监听的数目一共有多大。这个参C同于select()中的W一个参敎ͼl出最大监听的fd+1的倹{?
需要注意的是,当创建好epoll句柄后,它就是会(x)占用一个fd|在linux下如果查?proc/q程id/fd/Q是能够看到q个fd的,所以在
使用完epoll后,必须调用close()关闭Q否则可能导致fd被耗尽?br>
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函敎ͼ它不同与select()是在监听事g时告诉内核要监听什么类型的事gQ而是在这里先注册要监听的事gcd。第一个参数是epoll_create()的返回|W二个参数表C动作,用三个宏来表C:(x)
EPOLL_CTL_ADDQ注册新的fd到epfd中;
EPOLL_CTL_MODQ修改已l注册的fd的监听事Ӟ
EPOLL_CTL_DELQ从epfd中删除一个fdQ?br>
W三个参数是需要监听的fdQ第四个参数是告诉内栔R要监听什么事Qstruct epoll_eventl构如下Q?br>
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:(x)
EPOLLIN Q表C对应的文g描述W可以读Q包括对端SOCKET正常关闭Q;
EPOLLOUTQ表C对应的文g描述W可以写Q?br>
EPOLLPRIQ表C对应的文g描述W有紧急的数据可读Q这里应该表C有带外数据到来Q;
EPOLLERRQ表C对应的文g描述W发生错误;
EPOLLHUPQ表C对应的文g描述W被挂断Q?br>
EPOLLETQ?EPOLL设ؓ(f)边缘触发(Edge Triggered)模式Q这是相对于水^触发(Level Triggered)来说的?br>
EPOLLONESHOTQ只监听一ơ事Ӟ当监听完q次事g之后Q如果还需要l监听这个socket的话Q需要再ơ把q个socket加入到EPOLL队列?br>
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
{?
待事件的产生Q类gselect()调用。参数events用来从内核得C件的集合Qmaxevents告之内核q个events有多大,q个
maxevents的g能大于创建epoll_create()时的sizeQ参数timeout是超时时_(d)毫秒Q??x)立卌回?1不定Q也?
说法说是怹dQ。该函数q回需要处理的事g数目Q如q回0表示已超时?br>
--------------------------------------------------------------------------------------------
从man手册中,得到ET和LT的具体描q如?br>
EPOLL事g有两U模型:(x)
Edge Triggered (ET)
Level Triggered (LT)
假如有这样一个例子:(x)
1. 我们已经把一个用来从道中读取数据的文g句柄(RFD)d到epoll描述W?br>
2. q个时候从道的另一端被写入?KB的数?br>
3. 调用epoll_wait(2)Qƈ且它?x)返回RFDQ说明它已经准备好读取操?br>
4. 然后我们d?KB的数?br>
5. 调用epoll_wait(2)......
Edge Triggered 工作模式Q?br>
?
果我们在W?步将RFDd到epoll描述W的时候用了(jin)EPOLLET标志Q那么在W?步调用epoll_wait(2)之后有可能?x)挂P因ؓ(f)?
余的数据q存在于文g的输入缓冲区内,而且数据发出端还在等待一个针对已l发出数据的反馈信息。只有在监视的文件句柄上发生?jin)某个事件的时?ET
工作模式才会(x)汇报事g。因此在W?步的时候,调用者可能会(x)攑ּ{待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中Q会(x)有一个事件生在RFD句柄
上,因ؓ(f)在第2步执行了(jin)一个写操作Q然后,事g会(x)在第3步被销毁。因为第4步的d操作没有ȝ文g输入~冲区内的数据,因此我们在第5步调?
epoll_wait(2)完成后,是否挂v是不定的。epoll工作在ET模式的时候,必须使用非阻塞套接口Q以避免׃一个文件句柄的d?d
写操作把处理多个文g描述W的d饿死。最好以下面的方式调用ET模式的epoll接口Q在后面?x)介l避免可能的~陷?br>
i Z非阻塞文件句?br>
ii 只有当read(2)或者write(2)q回EAGAIN时才需要挂P{待?span style="font-weight: bold; color: #0001ff;">但这q不是说每次read()旉需要@环读Q直到读C生一个EAGAIN才认为此ơ事件处理完成,当read()q回的读到的数据长度于h的数据长度时Q就可以定此时~冲中已没有数据?jin),也就可以认?f)此事M件已处理完成?/span>
Level Triggered 工作模式
?
反的Q以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2)Qƈ且无论后面的数据是否被用,因此他们h同样的职能。因为即
使用ET模式的epollQ在收到多个chunk的数据的时候仍然会(x)产生多个事g。调用者可以设定EPOLLONESHOT标志Q在
epoll_wait(2)收到事g后epoll?x)与事g兌的文件句柄从epoll描述W中止掉。因此当EPOLLONESHOT讑֮后,使用带有
EPOLL_CTL_MOD标志的epoll_ctl(2)处理文g句柄成用者必M的事情?br>
然后详细解释ET, LT:
LT(level
triggered)是缺省的工作方式Qƈ且同时支持block和no-block
socket.在这U做法中Q内核告诉你一个文件描q符是否qA?jin),然后你可以对q个qA的fdq行IO操作。如果你不作M操作Q内核还是会(x)l箋通知?
的,所以,q种模式~程出错误可能性要一炏V传l的select/poll都是q种模型的代表.
ET(edge-triggered)
是高速工作方式,只支持no-block
socket。在q种模式下,当描q符从未qA变ؓ(f)qAӞ内核通过epoll告诉你。然后它?x)假设你知道文g描述W已l就l,q且不会(x)再ؓ(f)那个文g描述
W发送更多的qA通知Q直C做了(jin)某些操作D那个文g描述W不再ؓ(f)qA状态了(jin)(比如Q你在发送,接收或者接收请求,或者发送接收的数据于一定量时导?
?jin)一个EWOULDBLOCK 错误Q。但是请注意Q如果一直不对这个fd作IO操作(从而导致它再次变成未就l?Q内怸?x)发送更多的通知(only
once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark认Q这句话不理解)(j)?/span>
?
许多试中我们会(x)看到如果没有大量的idle
-connection或者dead-connectionQepoll的效率ƈ不会(x)比select/poll高很多,但是当我们遇到大量的idle-
connection(例如WAN环境中存在大量的慢速连?Q就?x)发现epoll的效率大大高于select/poll。(未测试)(j)
另外Q当使用epoll的ET模型来工作时Q当产生?jin)一个EPOLLIN事g后,
L据的时候需要考虑的是当recv()q回的大如果等于请求的大小Q那么很有可能是~冲有数据未dQ也意味着该次事gq没有处理完Q所以还需要再ơ读?/span>
Q?br>
while(rs)
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
// ׃是非d的模?所以当errno为EAGAIN?表示当前~冲区已无数据可?br>
// 在这里就当作是该ơ事件已处理?
if(errno == EAGAIN)
break;
else
return;
}
else if(buflen == 0)
{
// q里表示对端的socket已正常关?
}
if(buflen == sizeof(buf)
rs = 1; // 需要再ơ读?/span>
else
rs = 0;
}
q?
有,假如发送端量大于接收端的量(意思是epoll所在的E序L转发的socket要快),׃是非d的socket,那么send()函数虽然
q回,但实际缓冲区的数据ƈ未真正发l接收端,q样不断的读和发Q当~冲区满后会(x)产生EAGAIN错误(参考man
send),同时,不理?x)这ơ请求发送的数据.所?需要封装socket_send()的函数用来处理这U情?该函C(x)量数据写完再q回Q返?
-1表示出错。在socket_send()内部,当写~冲已满(send()q回-1,且errno为EAGAIN),那么?x)等待后再重?q种方式q?
不很完美,在理Z可能?x)长旉的阻塞在socket_send()内部,但暂没有更好的办?
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char *p = buffer;
while(1)
{
tmp = send(sockfd, p, total, 0);
if(tmp < 0)
{
// 当send收到信号?可以l箋?但这里返?1.
if(errno == EINTR)
return -1;
// 当socket是非d?如返回此错误,表示写缓冲队列已?
// 在这里做延时后再重试.
if(errno == EAGAIN)
{
usleep(1000);
continue;
}
return -1;
}
if((size_t)tmp == total)
return buflen;
total -= tmp;
p += tmp;
}
return tmp;
}
epoll有两U模?Edge Triggered(UET) ?Level
Triggered(ULT).在采用这两种模式时要注意的是,如果采用ET模式,那么仅当状态发生变化时才会(x)通知,而采用LT模式cM于原来的
select/poll操作,只要q有没有处理的事件就?x)一直通知.
以代码来说明问题:
首先l出server的代?需要说明的是每ơaccept的连?加入可读集的时候采用的都是ET模式,而且接收~冲区是5字节?也就是每ơ只接收5字节的数?
下面l出试所用的Perl写的client?在client中发?0字节的数?同时让client在发送完数据之后q入d@? 也就是在发送完之后q接的状态不发生改变--既不再发送数? 也不关闭q接,q样才能观察出server的状?
q行server和client发现,server仅仅d?字节的数?而client其实发送了(jin)10字节的数?也就是说,server仅当W一?
监听C(jin)EPOLLIN事g,׃没有d完数?而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事g?
(友情提示:上面的这个测试客L(fng),当你关闭它的时候会(x)再次出发IO可读事glserver,此时server׃(x)去读取剩下的5字节数据?但是q一事g与前面描q的ET性质q不矛盾.)
如果我们把client改ؓ(f)q样:
可以发现,在server接收?字节的数据之后一直监听不到client的事?而当client休眠5U之后重新发送数?server再次监听C(jin)变化,只不q因为只是读取了(jin)5个字?仍然?0个字节的数据(clientW二ơ发送的数据)没有接收?
如果上面的实验中,对accept的socket都采用的是LT模式,那么只要q有数据留在buffer?server׃(x)l箋得到通知,读者可以自行改动代码进行实?
?
于这两个实验,可以得出q样的结?ET模式仅当状态发生变化的时候才获得通知,q里所谓的状态的变化q不包括~冲Zq有未处理的数据,也就是说,如果
要采用ET模式,需要一直read/write直到出错为止,很多人反映ؓ(f)什么采用ET模式只接收了(jin)一部分数据再也得不到通知?大多因ؓ(f)q样;而LT
模式是只要有数据没有处理׃(x)一直通知下去?
另外,从这个例子中,也可以阐qC些基本的|络~程概念.首先,q接的两端中,一端发送成功ƈ不代表着Ҏ(gu)上层应用E序接收成功,
拿上面的client试E序来说,10字节的数据已l发送成?但是上层的serverq没有调用readd数据,因此发送成功仅仅说明了(jin)数据被对
方的协议栈接收存攑֜?jin)相应的buffer?而上层的应用E序是否接收?jin)这部分数据不得而知;同样?d数据时也只代表着本方协议栈的对应
buffer中有数据可读,而此时时候在对端是否在发送数据也不得而知.