1. UDP协议介绍

UDP协议 相对TCP协议来讲属于不可靠协议,UDP协议是广播方式发送数据,没有服务器和客户端的概念。

在Linux下使用socket创建UDP的套接字时,属性要选择数据报类型SOCK_DGRAM

 sockfd=socket(AF_INET,SOCK_DGRAM,0);

2. UDP协议发送和接收数据的函数

2.1 recvfrom函数

UDP使用recvfrom()函数接收数据,他类似于标准的read(),但是在recvfrom()函数中要指明数据的目的地址。

#include <sys/types.h>  
#include <sys/socket.h>  
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * from, size_t *addrlen);

返回值

成功返回接收到数据的长度,负数失败 

前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。最后两个参数类似于accept的最后两个参数(接收客户端的IP地址)。

2.2 sendto函数

UDP使用sendto()函数发送数据,他类似于标准的write(),但是在sendto()函数中要指明目的地址。

#include <sys/types.h>  
#include <sys/socket.h>  
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr * to, int addrlen);

返回值

成功返回发送数据的长度,失败返回-1

前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。

参数to指明数据将发往的协议地址,他的大小由addrlen参数来指定。

2.3 设置套接字属性

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

setsockopt()函数用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的“套接口”层次上的选项。选项影响套接口的操作,诸如加急数据是否在普通数据流中接收,广播数据是否可以从套接口发送等等。

参数

sockfd:标识一个套接口的描述字。

level:选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次。

optname:需设置的选项。

optval:指针,指向存放选项值的缓冲区。

optlen:optval缓冲区的长度。

UDP协议发送数据时,设置具有广播特性: 默认情况下socket不支持广播特性

char bBroadcast=1;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(char));

3. 案例: UDP协议数据收发

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <poll.h>


#define SEND_MSG "1314520"   //发送的数据包
#define PORT 8888           //固定的端口号

int sockfd;
int main(int argc,char **argv)
{   
    if(argc!=2)
    {
        printf("./app <广播地址>  当前程序固定的端口号是8888\n");
        return 0;
    }

    /*1. 创建socket套接字*/
    sockfd=socket(AF_INET,SOCK_DGRAM,0);

    //设置端口号的复用功能
    int on = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    /*2. 绑定端口号与IP地址*/
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(PORT); // 端口号0~65535
    addr.sin_addr.s_addr=INADDR_ANY;    //inet_addr("0.0.0.0"); //IP地址
    if(bind(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr))!=0)
    {
        printf("UDP服务器:端口号绑定失败.\n");
        return 0;
    }

    /*3. 接收数据*/
    unsigned char buff[1024+1];
    int cnt;
    struct sockaddr_in client_addr;
    socklen_t addrlen=sizeof(struct sockaddr_in);

    struct pollfd fds;
    fds.fd=sockfd;
    fds.events=POLLIN;
    while(1)
    {
        cnt=poll(&fds,1,1000);
        if(cnt>0)
        {
            cnt=recvfrom(sockfd,buff,1024,0,(struct sockaddr *)&client_addr,&addrlen);
            buff[cnt]='\0';
            
            //判断是不是探测包数据
            if(strcmp(buff,SEND_MSG)==0)
            {
                printf("在线好友:%s,%d-->%s:%d\n",buff,cnt,inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
                cnt=sendto(sockfd,SEND_MSG,strlen(SEND_MSG),0,(const struct sockaddr *)&client_addr,sizeof(struct sockaddr));
                printf("回应探测包:%d字节.\n",cnt);

                //这里可以继续写代码,将存在的好友保存在链表,并记录在线好友数量
            }
        }
        else
        {
            ssize_t cnt;
            struct sockaddr_in addr;
            addr.sin_family=AF_INET;
            addr.sin_port=htons(PORT); // 端口号0~65535
            addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址

            cnt=sendto(sockfd,SEND_MSG,strlen(SEND_MSG),0,(const struct sockaddr *)&addr,sizeof(struct sockaddr));
            printf("探测包发送:%d字节.\n",cnt);
        }
    }
    return 0;
}

4. 案例: 使用UDP协议探测在线好友

前面几篇文章介绍了Linux下TCP协议设计的群聊天室的一个程序,如果想要知道同一个网络下有多少好友在线,就可以使用UDP协议进行广播探测。 大家的端口号是固定的,也就是只要在这个网络范围内,大家都跑这个同一个聊天室程序,就可以互相探测,得到对方IP地址之后,再完成TCP协议建立,完成点对点聊天通信。

#include <stdio.h>	
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <libgen.h>
#include <sys/stat.h>
#include <time.h>
#include <errno.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥锁
#define CLIENT_COUNT 100 //服务器可容纳客户端数量
#define SERVER_PORT 8080 //服务器端口号

/*----------------------------链表相关函数----------------------------*/
/*---------------------存储客户端信息和线程ID结构体-------------------*/
//存储客户端信息和线程ID结构体
typedef struct node
{
	int nfds;			//客户端套接字
	pthread_t phid;		//线程ID
	char name[50];	//用户名
	struct node *next;
}node, *ptr_node;

//链队列
typedef struct
{
	ptr_node head, tail;
}qlink;

/**********************
	链表初始化
**********************/
void qlink_Init(qlink *s)
{
	s->head = s->tail = (ptr_node)malloc(sizeof(node));
	if(s->tail == NULL)
	{
		printf("创建节点失败!\n");
		exit(0);
	}
	s->head->next = NULL;
}
/****************************************
 添加节点
 参数:
		s --- 链表
		c --- 客户端套接字
	  phid --- 线程ID
*****************************************/
void qlink_Add(qlink *s, int c, pthread_t phid)
{
	pthread_mutex_lock(&mutex);  //互斥锁上锁(带阻塞)
	ptr_node p;
	p = (ptr_node)malloc(sizeof(node));
	if (p == NULL)
	{
		printf("创建节点失败[2]!\n");
		exit(0);
	}
	p->nfds = c;
	p->phid = phid;
	p->next = s->head->next;
	s->head->next = p;
	pthread_mutex_unlock(&mutex);//互斥锁解锁
}
/*--------------------------------END---------------------------------*/
/****************************全局变量定义******************************/
qlink s;			//链表
pthread_t phid;	//线程ID
int sockfd_UDP;			//UDP套接字
int Find_user = 0;		//找到用户标志位
int f_dp;		//TCP客户端套接字
int server_flag = 0;		//当成服务器
int client_flag = 0;		//当成客户端
/*---------------------------发送数据结构体---------------------------*/
typedef struct UDP_Test
{
	char name[50];//用户名
	char buff[100];//发送数据,探测数据:UDP_TEST
	int flag;//1表示探测好友,2好友回复,3正常数据
}UDP_Test;
struct UDP_Test recv_msg;			//接收信息结构体
struct UDP_Test send_msg;			//发送信息结构体
/*--------------------------------END---------------------------------*/
/**********************************************************************/


/**********************************************************************/


/*****************************
	信号捕获函数 
*****************************/
void signal_capture(int sig)
{
	if(sig==SIGSEGV)//段错误
	{
		time_t sec=time(NULL); //获取系统秒单位时间	
		char buff[50];
		struct tm *time;
		time=localtime(&sec); //秒单位时间转换时间结构体
		strftime(buff,sizeof(buff),"%Y/%m/%d %k:%M:%S\n",time);
		printf("段错误时间:%s\n",buff);
	}
	else
	{
		#if 0
		ptr_node p = s.head->next;
		while(p != NULL)
		{
			printf("已清理资源!p->nfds = %d\n", p->nfds);
			close(p->nfds);
			pthread_cancel(p->phid);//杀死线程
			p = p->next;
		}
		free(s.head);
		#endif
	}
	printf("服务器资源清理完成\n");
	exit(0);
}
/*--------------------------------END---------------------------------*/
/****************************
	线程工作函数 
****************************/
void *start_routine(void *arg)
{
	#if 0
	int f_dp = *(int *)arg;
	memset(&send_msg, 0, sizeof(send_msg));
	char buff[100];
	while (1)
	{
		scanf("%s", buff);	
		strncpy(send_msg.name, "777" ,sizeof(send_msg.name));//用户名 
		strncpy(send_msg.buff, buff, sizeof(buff));//探测消息内容
		
		printf("send_msg.buff = %s\n", send_msg.buff);
		write(f_dp, &send_msg, sizeof(send_msg));
		usleep(100);
	}
	#endif
	char buff[50];
	int cho = *(int *)arg;
	if(cho == 1)			//写数据线程
	{
		memset(&send_msg, 0, sizeof(send_msg));
		while (1)
		{
			scanf("%s", buff);	
			//strncpy(send_msg.name, buff ,sizeof(send_msg.name));//用户名
			strncpy(send_msg.buff, buff, sizeof(buff));//探测消息内容
			
			printf("send_msg.buff = %s\n", send_msg.buff);
			write(f_dp, &send_msg, sizeof(send_msg));
			usleep(100);
		}
		pthread_exit(NULL);
	}
	else					//读数据线程
	{
		int res, cnt;
		fd_set readfds;//读事件
		struct timeval timeout;
		struct UDP_Test read_msg;
		while (2)
		{
			FD_ZERO(&readfds);
			FD_SET(f_dp, &readfds);
			timeout.tv_sec = 0;
			timeout.tv_usec = 0;
			res = select(f_dp + 1, &readfds, NULL, NULL, &timeout);
			if(res > 0)
			{
				cnt = read(f_dp, &read_msg, sizeof(read_msg));
				if(cnt > 0)
				{
					printf("接收到数据: name:%s \t data:%s\n", read_msg.name, read_msg.buff);			//接收到了对方的用户名
					//break;
				}
				else
				{
					printf("好友下线!\n");
					break;
				}
			}
			else if(res < 0)
			{
				printf("错误!\n");
			}
			usleep(100);
		}
		pthread_exit(NULL);
	}
	
}
/*--------------------------------END---------------------------------*/
/***********************
 用于接收探测返回信息
***********************/
void *Receive_probe_retur(void *arg)
{
	memset(&recv_msg, 0, sizeof(recv_msg));
	fd_set readfds;//读事件
	struct timeval timeout;
	int res, cnt;
	struct sockaddr_in src_addr;			//保存其它用户的ip地址和端口号
	socklen_t addrlen = sizeof(struct sockaddr_in);
	while(1)	//等待接收消息
	{
		FD_ZERO(&readfds);
		timeout.tv_sec=0;
		timeout.tv_usec=0;
		FD_SET(sockfd_UDP,&readfds);
		res = select(sockfd_UDP+1, &readfds, NULL, NULL, &timeout);
		if(res > 0)
		{
			cnt = recvfrom(sockfd_UDP, &recv_msg, sizeof(struct UDP_Test), 0, (struct sockaddr*)&src_addr, &addrlen);
			if(cnt > 0)
			{
				if(recv_msg.flag == 2)		//接收到返回信息
				{
					printf("接收到%s用户的确认信息\n", recv_msg.name);
					client_flag = 3;		//该用户当作客户端
					Find_user = 1;			//标志搜索到用户,主线程不用再发探测信息了
					usleep(200);
					break;
				}			
			}
		}
		else if(res < 0)
		{
			printf("错误!\n");
		}
	}
	pthread_exit(NULL);		//杀死线程
}

int main(int argc,char *argv[])
{
	
	if(argc!=2)
	{
		printf("./a.out <用户名>\n");
		return 0;
	}
	
	signal(SIGPIPE,SIG_IGN);//忽略SIGPIPE信号
	signal(SIGINT, signal_capture);			//捕获CTRL+c
	signal(SIGSEGV, signal_capture);			//捕获段错误

	/* 初始化链表 */
	qlink_Init(&s);
	
	/*--------------------------通过UDP获取在线用户信息--------------------------*/
	/*1.创建套接字*/
	sockfd_UDP = socket(AF_INET,SOCK_DGRAM,0);
	if(sockfd_UDP == -1)
	{
		printf("创建套接字失败\n");
		return 0;
	}
	/*绑定端口号*/
	struct sockaddr_in addr;
	addr.sin_family=AF_INET;//IPV4
	addr.sin_port=htons(SERVER_PORT);//端口号
	addr.sin_addr.s_addr=INADDR_ANY;//本地所有IP(0.0.0.0)
	if(bind(sockfd_UDP,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)))
	{
		printf("绑定端口号失败\n");
		close(sockfd_UDP);
		return 0;
	}
	ssize_t cnt;
	/*获取广播地址*/
	//ifconfig -a |grep broadcast|awk '{print $6}'|tr -d broadcast: -- ubuntu下获取本地广播地址
	//ifconfig -a |grep Bcast|awk '{print $3}'|tr -d Bcast:  --red hat下获取本地广播地址
	FILE *fp = popen("ifconfig -a |grep broadcast|awk '{print $6}'|tr -d broadcast:", "r");
	char ip_addr[20];
	cnt = fread(ip_addr, 1, sizeof(ip_addr) - 1, fp);
	ip_addr[cnt]='\0';
	printf("广播地址:%s\n",ip_addr);
	pclose(fp);
	addr.sin_addr.s_addr=inet_addr(ip_addr);//广播IP地址
	//设置该套接字为广播类型
	const int opt = 1;
	int nb = 0;
	nb = setsockopt(sockfd_UDP, SOL_SOCKET, SO_BROADCAST, (char *)&opt, sizeof(opt));
	if(nb == -1)
	{
		printf("设置广播类型错误.\n");
		close(sockfd_UDP);
		return 0;
	}	

	/*-----------------------------接收探测信息-----------------------------*/
	fd_set readfds;//读事件
	struct timeval timeout;
	int res;
	struct sockaddr_in src_addr;			//保存其它用户的ip地址和端口号
	socklen_t addrlen = sizeof(struct sockaddr_in);
	
	int count = 0;
	memset(&recv_msg, 0, sizeof(recv_msg));
	memset(&send_msg, 0, sizeof(send_msg));
	/* 循环等待 5秒 探测信息 */
	while(1)
	{
		FD_ZERO(&readfds);
		timeout.tv_sec=0;
		timeout.tv_usec=0;
		FD_SET(sockfd_UDP,&readfds);
		res = select(sockfd_UDP+1, &readfds, NULL, NULL, &timeout);
		if(res>0)
		{
			cnt = recvfrom(sockfd_UDP, &recv_msg, sizeof(struct UDP_Test), 0, (struct sockaddr*)&src_addr, &addrlen);
			if(cnt > 0)
			{
				if(recv_msg.flag == 1)		//接收到其它用户发出的探测信息,得到对方的IP地址
				{
					printf("user addr:%s \t user port:%d\n", inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port));
					printf("接收到数据: name:%s \t data:%s\n", recv_msg.name, recv_msg.buff);			//接收到了对方的用户名
					
					/* 当接收到探测信息后,返回确认收到信息给该用户 */
					strncpy(send_msg.name, argv[1], sizeof(send_msg.name));//用户名
					send_msg.flag = 2;//确认收到探测信息标志
					cnt = sendto(sockfd_UDP, &send_msg, sizeof(struct UDP_Test), 0, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in));
					printf("确认消息发送成功\n");
					/*--------------------------------------------------------------*/
					
					printf("搜索到在线用户 %s\n", recv_msg.name);
					client_flag = 3;		//接收到探测信息,该用户当作客户端
					//qlink_Add(&s, , pthread_t phid);
					close(sockfd_UDP);
					break;
				}			
			}
		}
		else if(res < 0)
		{
			printf("错误!\n");
		}
		usleep(100);
		count++;
		if(count >= 50000)		//若是5秒后还没有接收到探测信息,则跳出循环
		{
			count = 0;
			server_flag = 3;	//设置成服务器
			break;
		}
	}
	/*----------------------------------END--------------------------------*/
/****************************************************客户端***********************************************************/	
	if(client_flag == 3)		//标志在此次是客户端
	{
		/*1.创建套接字*/
		int sockfd_c = socket(AF_INET, SOCK_STREAM, 0);
		if(sockfd_c == -1)
		{
			printf("创建网络套接字失败\n");
			return 0;
		}
		/*2.连接服务器*/
		struct sockaddr_in t_addr;
		char buff[20];
		strcpy(buff, inet_ntoa(src_addr.sin_addr));
		int n = strlen(buff);
		buff[n] = '\0';
		printf("buff:%s\n", buff);
		t_addr.sin_family = AF_INET;//IPV4
		t_addr.sin_port = htons(8089);//端口号
		t_addr.sin_addr.s_addr = inet_addr(buff);//服务器IP
	
		while(1)
		{
			printf("准备连接服务器:connect addr:%s \t user port:%d\n", inet_ntoa(t_addr.sin_addr), ntohs(t_addr.sin_port));
			
			printf("sockfd_c:%d\n",sockfd_c);
			if(connect(sockfd_c, (const struct sockaddr *)&t_addr, sizeof(struct sockaddr_in)))
			{
				printf("连接失败:%s,%d\n",strerror(errno),errno);
				sleep(2);
			}
			else
			{
				printf("服务器连接成功\n");
				break;
			}
		}
		
		
		/*-----------------------将自己的用户信息发送过去----------------------*/
		memset(&send_msg, 0, sizeof(send_msg));
		strncpy(send_msg.name,argv[1],sizeof(send_msg.name));//用户名
		write(sockfd_c, &send_msg, sizeof(send_msg));		//将用户信息发送过去
		/*----------------------------------END--------------------------------*/
		while(1);
		
	}
/****************************************************服务器***********************************************************/
	else if(server_flag = 3)		//标志在此次是服务器
	{
		memset(&send_msg, 0, sizeof(send_msg));
		pthread_t phid_1;
		pthread_create(&phid_1, NULL, Receive_probe_retur, NULL);		//创建线程 用于接收探测返回信息
		/************************************主线程循环发送探测消息******************************************/
		while(1)
		{
			strncpy(send_msg.name, argv[1], sizeof(send_msg.name));//用户名
			//strncpy(send_msg.buff, "UDP_TEST", sizeof(send_msg.buff));//探测消息内容
			send_msg.flag = 1;//探测消息标志
			cnt = sendto(sockfd_UDP, &send_msg, sizeof(struct UDP_Test), 0, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in));
			printf("探测消息发送成功\n");
			if(Find_user == 1)		//标志在子线程内找到了在线用户
			{
				break;
			}
			sleep(1);
		}
		/*****************************开始创建TCP服务器********************************/
		/* 创建套接字 */
		int sockfd_TCP = socket(AF_INET, SOCK_STREAM, 0);
		if(sockfd_TCP == -1)
		{
			printf("网络套接字创建失败\n");
			return 0;
		}
		/*允许绑定已使用的端口号*/
		int on = 1;/*  */
		setsockopt(sockfd_TCP, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
		
		/* 绑定端口号 */
		struct sockaddr_in s_addr;
		s_addr.sin_family = AF_INET;		//设置为IPV4
		s_addr.sin_port = htons(8089);//端口号
		s_addr.sin_addr.s_addr = INADDR_ANY;     //IP地址,该参数是让系统随机分配
		if(bind(sockfd_TCP, (const struct sockaddr *)&s_addr, sizeof(s_addr)))
		{
			printf("绑定端口号失败!\n");
			return 0;
		}
		//设置监听数量
		listen(sockfd_TCP, 100);
		/* 等待客户端连接 */
		int i;
		pthread_t phid[2];
		int *p;
		printf("已跳出循环\n");
		while(1)
		{
			struct sockaddr_in c_addr;	//客户端属性信息
			socklen_t c_addr_len = sizeof(struct sockaddr_in);
			f_dp = accept(sockfd_TCP, (struct sockaddr *)&c_addr, &c_addr_len);
			printf("%d客户端连接成功,ip=%s:%d\n",f_dp,inet_ntoa(c_addr.sin_addr),ntohs(c_addr.sin_port));
			read(f_dp, &recv_msg, sizeof(recv_msg));		//第一次读取用户信息
			printf("user name = %s\n", recv_msg.name);
			for (i = 1; i <= 2; i++)
			{
				p = malloc(sizeof(int));
				*p = i;
				pthread_create(&phid[i - 1], NULL, start_routine, p);			//创建线程	-- i == 1是读, i ==2写
				pthread_detach(phid[i - 1]);//设置为分离属性
			}		
		} 
	}
	/*----------------------------------END--------------------------------*/
	#if 0
	pthread_t phid;
	int *p = malloc(sizeof(int));
	*p = sockfd_c;
	pthread_create(&phid, NULL, start_routine, p);		//创建线程
	pthread_detach(phid);					//设置分离属性
	/*-------------------------------主线程读------------------------------*/
	struct UDP_Test re_msg;	//存储读到的数据
	fd_set readfds_2;//读事件
	while(1)
	{
		FD_ZERO(&readfds_2);//初始化读事件
		FD_SET(sockfd_c,&readfds_2);//添加要监测的描述符到读事件集合中
		timeout.tv_sec=0;
		timeout.tv_usec=0;
		//printf("用户 \n");
		res = select(sockfd_c + 1, &readfds_2, NULL, NULL, &timeout);
		if(res > 0)	//当有数据的时候进行上锁
		{
			memset(&re_msg, 0, sizeof(re_msg));
			pthread_mutex_lock(&mutex);  //互斥锁上锁(带阻塞)
			int n = read(sockfd_c, &re_msg, sizeof(re_msg));		//读取数据
			if(n <= 0)
			{
				printf("好友下线!\n");
				break;
			}
			else
			{
				printf("用户 %s\t 消息 %s\n", re_msg.name, re_msg.buff);
			}
			pthread_mutex_unlock(&mutex);//互斥锁解锁
		}
		else if(res < 0)
		{
			printf("select函数出错!\n");
			break;
		}
		usleep(100);
	}
	#endif
	/*----------------------------------END--------------------------------*/
	return 0;
}

image-20211202143124060