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