且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-05-17 02:27:37

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;
}

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