目前完成UDP广播上线、用户间对话功能。

目录

目录

前言

一、基础知识

二、“飞鸽”运行流程

1.用户运行程序后先设置用户名,即上线后别人看到的名字。

2.上线后进行UDP广播,同时可以接收到在线用户的反馈,将反馈写入用户链表中。

3.上线后可以进行一系列操作,可以通过输入“help”得到可实现功能列表。

4.退出程序。

三、代码解读

3.1 系统初始化

3.1.1飞鸽采用IPMSG协议

3.1.2 sprintf函数

3.1.3 sendto函数

3.1.4 setsockopt函数

3.1.5 设置登录用户名

3.2 用户链表

3.2.1 定义用户结构体

3.2.2 添加用户

3.2.3 删除用户

3.2.4 根据IP查找用户信息

3.2.5 根据用户名获取user结构体

3.2.6 遍历用户

3.3 收发信息

3.3.1 发送信息

3.3.2 接收信息

3.4 用户界面设计

3.4.1 指令分解

3.4.2 展示用户列表

3.4.3 退出

3.4.4 发送信息

3.4.5 打开帮助面板

总结


前言

        ipmsg全称:IP Messenger,中文名为“飞鸽传书”,是一款用C语言写的局域网聊天和文件传输工具。它是一个小巧方便的即时通信软件,它适合用于局域网内甚至广域网间进行实时通信和文档共享。特别是在局域网内传送文件/文件夹的效率很高。

      它具有很多优点,如数据通讯不需要建立服务器、直接在两台电脑间通信和数据传输,支持文件及文件目录的传输,安全快捷以及小巧方便等优异特点,因此很多公司都采用它作为部门、公司内部的IM即时通信工具。

        实训项目为在linux下用C语言写一个可以实现“飞鸽”功能的程序


一、基础知识

应先掌握TCP、UDP、UDP广播基础内容,进行基础代码编写,详情请见:

Linux下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);
功能: 用于发送数据
注意:不能用TCP协议发送0长度的数据包
参数:
sockfd: socket套接字
buf: 待发送数据缓存区的地址
nbytes: 发送缓存区大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:成功发送的字节数
头文件:#include <sys/socket.h>

    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);
成功执行返回0,否则返回-1

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);
功能: 用于接收网络数据
参数:
sockfd: 套接字
buf: 指向接收网络数据的缓冲区
nbytes: 接收缓冲区的大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:成功接收到字节数
头文件:#include <sys/socket.h>
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广播上线,与指定用户通信发消息功能。

Logo

更多推荐