linux实训项目——飞鸽(一)
目前完成UDP广播上线、用户间对话功能。
目前完成UDP广播上线、用户间对话功能。
目录
2.上线后进行UDP广播,同时可以接收到在线用户的反馈,将反馈写入用户链表中。
3.上线后可以进行一系列操作,可以通过输入“help”得到可实现功能列表。
前言
ipmsg全称:IP Messenger,中文名为“飞鸽传书”,是一款用C语言写的局域网聊天和文件传输工具。它是一个小巧方便的即时通信软件,它适合用于局域网内甚至广域网间进行实时通信和文档共享。特别是在局域网内传送文件/文件夹的效率很高。
它具有很多优点,如数据通讯不需要建立服务器、直接在两台电脑间通信和数据传输,支持文件及文件目录的传输,安全快捷以及小巧方便等优异特点,因此很多公司都采用它作为部门、公司内部的IM即时通信工具。
实训项目为在linux下用C语言写一个可以实现“飞鸽”功能的程序
一、基础知识
应先掌握TCP、UDP、UDP广播基础内容,进行基础代码编写,详情请见:
二、“飞鸽”运行流程
1.用户运行程序后先设置用户名,即上线后别人看到的名字。
2.上线后进行UDP广播,同时可以接收到在线用户的反馈,将反馈写入用户链表中。
3.上线后可以进行一系列操作,可以通过输入“help”得到可实现功能列表。
4.退出程序。
三、代码解读
3.1 系统初始化
char buf[200]="";
struct sockaddr_in addr={AF_INET};
addr.sin_addr.s_addr=inet_addr("192.168.1.255"); //设置广播地址
addr.sin_port = htons(2425); //“飞鸽”固定端口 2425
sprintf(buf, "1:%d:%s:%s:%d:%s",time(NULL),user,host,IPMSG_BR_ENTRY,user);
sendto(sockfd, buf, strlen(buf),0,(struct sockaddr*)&addr, sizeof(addr));
3.1.1飞鸽采用IPMSG协议
IPMSG基本格式:
版本号:包编号:发送者姓名:发送者机器名:命令字:附加信息
1.版本号固定为1;
2.包编号一般为不重复的十进制数,通常可以由time函数产生;
3.发送者姓名和发送者机器名可以任意,但在整个通信中必须保持一致。
4.命令字
1)报文中的命令字是一个32位无符号整数
包含命令(最低字节)和选项(高三字节)两部分。
2)常用基本命令(带有BR标识的为广播命令)
IPMSG_BR_ENTRY 用户上线
IPMSG_BR_EXIT 用户退出
IPMSG_ANSENTRY 通报在线
IPMSG_SENDMSG 发送消息
IPMSG_RECVMSG 通报收到消息
3)常用选项
IPMSG_SENDCHECKOPT 传送检查(需要对方返回回执)
IPMSG_FILEATTACHOPT 传送文件选项
4)附加信息
附加信息的内容根据命令字的不同而不同。
IPMSG_GETFILEDATA 请求通过TCP传输文件
IPMSG_RELEASEFILES 停止接收文件
IPMSG_GETDIRFILES 请求传输文件夹
3.1.2 sprintf函数
sprintf指的是字符串格式化命令,函数声明为 int sprintf(char *string, char *format [,argument,...]);,主要功能是把格式化的数据写入某个字符串中,即发送格式化输出到 string 所指向的字符串。sprintf 是个变参函数。使用sprintf 对于写入buffer的字符数是没有限制的,这就存在了buffer溢出的可能性。解决这个问题,可以考虑使用 snprintf函数,该函数可对写入字符数做出限制。
3.1.3 sendto函数
ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
addr.sin_port = htons(2425);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
strcpy(sys_user, user);
strcpy(sys_host, host);
if((udp_fd = socket(AF_INET, SOCK_DGRAM, 0))<0) //创建套接字(SOCK_DGRAM为数据报式)
{
perror("create udp");
exit(1);
}
if(bind(udp_fd, (struct sockaddr*)&addr, sizeof(addr))!=0) //绑定套接字
{
perror("bind udp");
exit(1);
}
setsockopt(udp_fd, SOL_SOCKET, SO_BROADCAST, &br_en, sizeof(br_en)); //套接口选项
if((tcp_fd = socket(AF_INET, SOCK_STREAM, 0))<0) //创建套接字(SOCK_STREAM为流式)
{
perror("create udp");
exit(1);
}
if(bind(tcp_fd, (struct sockaddr*)&addr, sizeof(addr))!=0) //绑定套接字
{
perror("bind tcp");
exit(1);
}
3.1.4 setsockopt函数
int setsockopt(int sockfd, int level,int optname, const void *optval, socklen_t optlen);
level
|
optname
|
说明
|
optval类型
|
SOL_SOCKET
|
SO_BROADCAST
|
允许发送广播数据包
|
int
|
SO_RCVBUF
|
接收缓冲区大小
|
int
| |
SO_SNDBUF
|
发送缓冲区大小
|
int
|
3.1.5 设置登录用户名
printf("Please input your name and hostname : ");
fgets(name,sizeof(name), stdin);
3.2 用户链表
3.2.1 定义用户结构体
typedef struct usr_date
{
char usr_name[20];
char host_name[20];
char usr_ip[20];
struct usr_date *next;
}USER;
3.2.2 添加用户
USER *pb,*pf;
if(find_usr(ip))
return NULL;
pb = (USER*)malloc(sizeof(USER)); //动态分配内存
//添加用户信息
strcpy(pb->usr_name,usr);
strcpy(pb->host_name,host);
strcpy(pb->usr_ip,ip);
if(head==NULL)
{
head=pb;
pb->next=NULL;
}
else
{
USER *pf=head;
while(pf->next!=NULL)
pf=pf->next;
pf->next = pb;
}
3.2.3 删除用户
while((strcmp(pb->usr_ip,ip)!=0)&&(pb->next!=NULL)) //判断ip位置
{
pf=pb;
pb=pb->next;
}
//查找结点并删除
if(strcmp(pb->usr_ip,ip)==0)
{
if(pb==head)
{
head=pb->next;
}
else
{
pf->next=pb->next;
}
}
if(head!=NULL)
{
pf=head;
printf("list\n");
while(pf!=NULL)
{
pf=pf->next;
}
}
删除链表节点可按位置分为以下几种情况:
1,要删除的节点在链表头,那么直接返回head->next,即去掉表头,返回后一个节点
2,要删除的节点在链表中间,那么就需要一个指针保存前一个节点,将前一个节点的next指向要删除节点的后一个节点,即pre->next=cur->next
3,要删除的节点在链表末尾,情况基本等同于在链表中间
4,链表中找不到要删除的节点,那么由于循环条件,链表指针会指向链表末尾后一位,cur指针会指向空,也不需要在链表中删除节点,但要考虑其他条件更改节点的操作不同的地方
3.2.4 根据IP查找用户信息
while(pf!=NULL)
{
if((strcmp(pf->usr_ip,ip))==0)
return 1;
pf=pf->next;
}
3.2.5 根据用户名获取user结构体
while(id!=0)
{
if(pf->next!=NULL)
pf=pf->next;
else
return NULL;
id--;
}
3.2.6 遍历用户
while(pf!=NULL)
{
printf("%2d %8s %8s\n", id++, pf->usr_name, pf->usr_ip);
pf=pf->next;
}
3.3 收发信息
3.3.1 发送信息
从键盘键入的数组,进行分割后传入发送信息的函数,通过判断传入的信息来决定输出
//输入对话指令时
if(argv[1]==NULL)
{
user_list();
printf("please select a user:");
scanf("%d",&uid);
getchar();
}
else
uid = atoi(argv[1]); //把传进来的字符串改成整形
通过序号寻找对应节点,即对应用户
usr = find_user_byid(uid);
addr.sin_port=htons(2425); //初始化,端口号
addr.sin_addr.s_addr = inet_addr(usr->usr_ip); //初始化地址
sprintf(buf, "1:%d:%s:%s:%d:",time(NULL),user(),host(),IPMSG_SENDMSG|IPMSG_SENDCHECKOPT); //把格式的字符串写到buf里
键入发送内容
printf("say to %s[%s]:",usr->usr_name, usr->usr_ip);
fflush(stdout);
fgets(buf+strlen(buf), sizeof(buf), stdin);
buf[strlen(buf)-1]='\0';
发送到指定用户
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&addr, sizeof(addr));
3.3.2 接收信息
recvfrom(udp_fd, buf, sizeof(buf), 0, (struct sockaddr*)&addr, &addr_len);//接收消息
函数含义
ssize_t recv(int sockfd, void *buf,size_t nbytes, int flags);
temp[i++]=strtok(buf, ":");//把buf的内容以 : 分开
通过收到的命令字来判断方向
switch(GET_MODE(atoi(temp[4]))) //判断命令字类型
{
case IPMSG_BR_ENTRY: //用户上线
add_usr(temp[2],temp[3],inet_ntoa(addr.sin_addr)); //创建用户
sprintf(buf, "1:%d:%s:%s:%d:%s",time(NULL),user(),host(),IPMSG_ANSENTRY,user()); //把本机信息传入buf
sendto(udp_fd, buf, strlen(buf),0,(struct sockaddr*)&addr,sizeof(addr)); //把buf发给上线用户
break;
case IPMSG_BR_EXIT: //用户退出
del_usr(inet_ntoa(addr.sin_addr)); //删除用户
break;
case IPMSG_ANSENTRY: //通报在线
add_usr(temp[2],temp[3],inet_ntoa(addr.sin_addr)); //创建用户
break;
case IPMSG_SENDMSG: //发送消息
if(temp[5]!=NULL)
{
printf("\r[%13s]:%s\n",inet_ntoa(addr.sin_addr),temp[5]);
printf("MY_IPMSG>>");
fflush(stdout);
}
if(atoi(temp[4])&IPMSG_SENDCHECKOPT) //传送检查(需要对方返回回执)
{
char buf[200]="";
sprintf(buf,"1:%d:%s:%s:%d:%s",time(NULL),user(),host(),IPMSG_RECVMSG,user());
sendto(udp_fd, buf, strlen(buf),0,(struct sockaddr*)&addr,sizeof(addr));
}
break;
default:
}
命令字含义:
1.用户上下线识别
IPMSG启动时,向局域网广播IPMSG_BR_ENTRY
其他已在线用户向该新用户回复IPMSG_ANSENTRY
IPMSG退出时,向局域网广播IPMSG_BR_EXIT
2.用户列表的维护
ENTRY报文和ANSENTRY报文 添加用户到用户列表
EXIT报文 将用户从用户列表中删除
ENTRY报文中的附加信息为用户名
3.消息收发
包含IPMSG_SENDMSG命令的报文表示发送消息
消息内容放在附加信息的位置
附加IPMSG_SENDCHECKOPT选项表示需要对方发送回执
如需回执,则消息接收方发送IPMSG_RECVMSG报文
附加信息为对方的包编号
回执报文为UDP数据包
4.文件发送
带有IPMSG_FILEATTACHOPT选项的IPMSG_SENDMSG报文表示要发送文件
文件属性等信息附加在附加信息中,以'\0'与消息正文分隔
文件信息的格式如下
文件序号:文件名:大小:修改时间:文件属性:
文件属性可选值
IPMSG_FILE_REGULAR 普通文件
IPMSG_FILE_DIR 目录文件
IPMSG_FILE_RETPARENT 返回上一级目录
文件相关信息可以通过stat()函数获得
文件大小、修改时间、文件属性以十六进制表示
可以附加多个文件信息,各文件之间以'\a'分隔
5.文件接收
接收端使用TCP协议发送IPMSG_GETFILEDATA报文表示希望启动文件传输
在附加信息中写入如下格式的信息以便请求某个文件
包编号:文件序号:偏移量
以上均为十六进制
发送端接收到上面的文件请求后即开始传送文件内容,没有任何格式
接收端可重复发送IPMSG_GETFILEDATA报文,以便请求多个文件
6.放弃接收文件
接收端发送IPMSG_RELEASEFILES报文到发送端,意味着放弃接收文件
将对方的包编号放在附加信息区
7.文件夹传输
接收端使用TCP协议发送IPMSG_GETDIRFILES报文表示希望启动文件夹传输
在附加信息中写入如下格式的信息以便请求某个文件夹
包编号:文件序号
以上均为十六进制
发送端接收到上面的请求后,将通过TCP连接发送如下格式的报文
报文头大小:文件名:文件大小:文件属性[:附加属性1=val1[,val2...][:附加属性2=...]]:文件内容下一个文件报文头大小:下一个文件名...
除文件名和内容外,全为十六进制
3.4 用户界面设计
3.4.1 指令分解
temp[i++]=strtok(buf," ");
将buf按照空格隔开分别存入temp数组
3.4.2 展示用户列表
if(strcmp(temp[0],"ls")==0)
{
printf("user list\n");
user_list();
}
3.4.3 退出
if(strcmp(temp[0], "exit")==0)
{
close(get_tcp_fd);
close(get_udp_fd);
printf("BYEBYE!\n");
exit(1);
}
3.4.4 发送信息
if(strcmp(temp[0],"say")==0)
{
send_msg(temp);
}
3.4.5 打开帮助面板
if (strcmp(temp[0],"help")==0)
{
help_cmd();
}
总结
整理代码后可以进行UDP广播上线,与指定用户通信发消息功能。
更多推荐
所有评论(0)