先来分析下:
客户端可能同时会有多个登录,通信实际上是两个客户端之间进行信息交流,服务器端只是起到了一个中间传递人的作用。(就像我们用QQ进行通信的时候一样)
接下来我们就需要考虑数据如何在客户端与服务器端进行传输了,当然这里不涉及网络编程。单纯的是练习系统编程。IPC通信的方式有很多种,我们选择一个相对简单的方式安吧。用有名管道吧
在通信之前呢,我们先来规划下数据如何组织或者说消息的组织方式【发送方、接收方、数据本身、协议】
这里的协议不是网络中的协议,只是单纯的为了区别:登录、交流、不在线、退出等状态【客户端可能会出现的状态】
我们进行封装,方便传输
qq_ipc.h
#ifndef QQ_IPC_H #define QQ_IPC_H /* protocal: 1 登录 2 数据传输 3 不在线 4 退出登录 */ struct DATA_INFO{ int protocal; //协议 char srcname[20]; //发送方 char destname[20]; //接收方 char data[100]; //发送的数据 }; #endif
我们还需要考虑一个问题,同时会有多个客户端在线。这些客户端都需要与服务器端进行联系的,那么服务器端怎么管理这些客户端呢?
客户端会有在线、退出的状态,而且相对来说是很频繁的操作,那么根据链表方便插入和删除操作,所以我们用链表来管理在线的用户
mylink.h
#ifndef _MYLINK_H_ #define _MYLINK_H_ typedef struct node *mylink; struct node{ char item[20]; //客户端的名字 int fifo_fd; //该客户端使用的私有管道 mylink next; }; //初始化链表 void mylink_init(mylink *head); //创建节点 mylink make_node(char *name,int fd); //插入 void mylink_insert(mylink *head,mylink p); //查找 mylink mylink_search(mylink *head,const char *keyname); //删除 void mylink_delete(mylink *head,mylink p); void free_node(mylink p); //遍历(服务器端有时候会向所有的客户端发送更新等消息) void mylink_travel(mylink *head,void (*vist)(mylink)); //销毁链表 void mylink_destory(mylink *head); #endif
mylink.c
#include <stdio.h> #include <string.h> #include "mylink.h" //初始化链表 void mylink_init(mylink *head) { *head = NULL; } //创建节点 mylink make_node(char *name,int fd) { mylink p = (mylink)malloc(sizeof(struct node)); strcpy(p->item,name); p->fifo_fd = fd; p->next = NULL; } //插入 void mylink_insert(mylink *head,mylink p) { //头插法 p->next = *head; *head = p; } //查找 mylink mylink_search(mylink *head,const char *keyname) { mylink p; for(p = *head;p != NULL;p = p->next) { if(strcmp(p->item,keyname) == 0) { return p; } } return NULL; } //删除 void mylink_delete(mylink *head,mylink p) { mylink tmp; //如果在头节点 if(*head == p) { *head = (*head)->next; return; } //如果不在头节点 for(tmp = *head;tmp != NULL;tmp = tmp->next) { if(tmp != NULL && tmp->next == p) { tmp->next = p->next; return; } } } void free_node(mylink p) { free(p); } //遍历(服务器端有时候会向所有的客户端发送更新等消息) void mylink_travel(mylink *head,void (*vist)(mylink)) { mylink p; for(p = *head;p != NULL;p = p->next) { vist(p); } } //销毁链表 void mylink_destory(mylink *head) { mylink p= *head, q; while (p != NULL) { q = p->next; free(p); p = q; } *head = NULL; }
管道设计:管道是半双工通信的,所以我们向两端相互通信的情况下,最好是设计两个管道,由于客户端向服务器端发送数据的时候,服务器端是统一个的,而且管道是队列结构的,所以所以客户端可以向同一个管道中写数据,服务器从这个管道中依次读数据就行。但是服务器向客户端端发送数据的时候,可能把数据发给A或者发给B,所以每一个客户端都有一个中间的管道。
client:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <error.h> #include <errno.h> #include "qq_ipc.h" #include "mylink.h" //服务器端统一的管道 #define SERVER_FIFO "SEV_FIFO" void sys_error(const char *str) { perror(str); } int main(int argc,char *argv[]) { if(argc < 2) { printf("./client name\n"); } int server_fd,client_fd,flag,len; char cmdbuf[256]; //打开文件向服务器中写数据的管道 server_fd = open(SERVER_FIFO,O_NONBLOCK); if(server_fd == -1) { sys_error("open"); } //创建一个专属自己管道 mkfifo(argv[1],0777); //通知服务器我现在要登录了 struct DATA_INFO cbuf,tmpbuf,talkbuf; cbuf.protocal = 1; //把管道的标识传给服务器端,服务器才能向里面写数据 strcpy(cbuf.srcname,argv[1]); client_fd = open(argv[1],O_RDONLY|O_NOBLACK); //将管道设置为非阻塞方式 flag = fcntl(STDIN_FILENO,F_GETFL); flag |= O_NONBLOCK; fcntl(STDIN_FILENO,F_SETFL,flag); //将登录信息传给服务器 write(server_fd,&cbuf,sizeof(cbuf)); //信息交互 while(1) { //判断要交互的客户端 len = read(client_fd,&tmpbuf,sizeof(tmpbuf)); if(len > 0) { if(tmpbuf.protocal == 3) { printf("%s id not online\n",tmpbuf.destname); } else if(tmpbuf.protocal == 2) { printf("%s : %s\n",tmpbuf.srcname,tmpbuf.destname); } } else if(len < 0) { if(errno != EAGAIN)//不是管道中没有数据 { sys_err("client read\n"); } } len = read(STDIN_FILENO,&cmdbuf,sizeof(cmdbuf)); //从键盘输入 if(len >0) { char *dname,*databuf; memset(&talkbuf,0,sizeof(talkbuf)); cmdbuf[len] = '\0'; dname = strtok(cmdbuf, "#\n"); /*按既定格式拆分字符串*/ if (strcmp("exit", dname) == 0) { /*退出登录:指定包号,退出者名字*/ talkbuf.protocal = 4; strcpy(talkbuf.srcname, argv[1]); write(server_fd, &talkbuf, sizeof(talkbuf));/*将退出登录包通过公共管道写给服务器*/ break; } else { talkbuf.protocal = 2; /*聊天*/ strcpy(talkbuf.destname, dname); /*填充聊天目标客户名*/ strcpy(talkbuf.srcname, argv[1]); /*填充发送聊天内容的用户名*/ databuf = strtok(NULL, "\0"); strcpy(talkbuf.data, databuf); } write(server_fd, &talkbuf, sizeof(talkbuf)); /*将聊天包写入公共管道*/ } } unlink(argv[1]); /*删除私有管道*/ close(client_fd); /*关闭私有管道的读端(客户端只掌握读端)*/ close(server_fd); /*关闭公共管道的写端(客户端值掌握写端)*/ return 0; }
server.c
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> #include "qq_ipc.h" #include "mylink.h" #define SERVER_PROT "SEV_FIFO" /*定义众所周知的共有管道*/ mylink head = NULL; /*定义用户描述客户端信息的结构体*/ void sys_err(char *str) { perror(str); exit(-1); } /*有新用户登录,将该用户插入链表*/ int login_qq(struct DATA_INFO *buf, mylink *head) { int fd; fd = open(buf->srcname, O_WRONLY); /*获取登录者名字,以只写方式打开以其名字命名的私有管道*/ mylink node = make_node(buf->srcname, fd); /*利用用户名和文件描述符创建一个节点*/ mylink_insert(head, node); /*将新创建的节点插入链表*/ return 0; } /*客户端发送聊天,服务器负责转发聊天内容*/ void transfer_qq(struct DATA_INFO *buf, mylink *head) { mylink p = mylink_search(head, buf->destname); /*遍历链表查询目标用户是否在线*/ if (p == NULL) { struct DATA_INFO lineout = {3}; /*目标用户不在, 封装3号数据包*/ strcpy(lineout.destname, buf->destname); /*将目标用户名写入3号包*/ mylink q = mylink_search(head, buf->srcname); /*获取源用户节点,得到对应私有管道文件描述符*/ write(q->fifo_fd, &lineout, sizeof(lineout)); /*通过私有管道写给数据来源客户端*/ } else write(p->fifo_fd, buf, sizeof(*buf)); /*目标用户在线,将数据包写给目标用户*/ } /*客户端退出*/ int logout_qq(struct DATA_INFO *buf, mylink *head) { mylink p = mylink_search(head, buf->srcname); /*从链表找到该客户节点*/ close(p->fifo_fd); /*关闭其对应的私有管道文件描述符*/ mylink_delete(head, p); /*将对应节点从链表摘下*/ free_node(p); /*释放节点*/ } void err_qq(struct DATA_INFO *buf) { fprintf(stderr, "bad client %s connect \n", buf->srcname); } int main(void) { int server_fd; /*公共管道文件描述符(读端)*/ struct DATA_INFO dbuf; /*定义数据包结构体对象*/ if (access(SERVER_PROT, F_OK) != 0) { /*判断公有管道是否存在, 不存在则创建*/ mkfifo(SERVER_PROT, 0664); } if ((server_fd = open(SERVER_PROT, O_RDONLY)) < 0) /*服务器以只读方式打开公有管道一端*/ sys_err("open"); mylink_init(&head); /*初始化链表*/ while (1) { read(server_fd, &dbuf, sizeof(dbuf)); /*读取公共管道,分析数据包,处理数据*/ switch (dbuf.protocal) { case 1: login_qq(&dbuf, &head); break; case 2: transfer_qq(&dbuf, &head); break; case 4: logout_qq(&dbuf, &head); break; default: err_qq(&dbuf); } } close(server_fd); }