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