写点什么

Linux 下网络编程 -UDP 协议探测在线好友

作者:DS小龙哥
  • 2022 年 4 月 01 日
  • 本文字数:10090 字

    阅读完需:约 33 分钟

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; //线程IDint 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;}
复制代码



发布于: 2022 年 04 月 01 日阅读数: 23
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
Linux下网络编程-UDP协议探测在线好友_4月月更_DS小龙哥_InfoQ写作平台