一、概述
UDP(User Datagram Protocol) : 用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可用于进行高效率的传输。 但不保证数据的可靠性。
特点 UDP 是无连接的协议 UDP 不保证数据可靠性 UDP 是面向报文的 UDP通信的实时性较高
缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。
使用场景 适合于广播/组播式通信中。 MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议 流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
二、UDP协议报文
用户数据报由两个部分组成:首部 + 数据部分。首部部分很简单,只有 8 个字节,由四个字段组成,每个字段的长度都是两个字节。
源端口号: 源端口号,需要对方回信时选用,不需要时全部置0. 目的端口号:目的端口号,在终点交付报文的时候需要用到。 长度:UDP的数据报的长度(包括首部和数据)其最小值为8(只有首部) 校验和:检测UDP数据报在传输中是否有错,有错则丢弃
UDP和TCP协议对比 特点: tcp协议是面向连接、可靠、基于字节流 udp协议是无连接、不可靠、基于数据报文
性能: tcp协议传输效率慢,所需要资源多 udp协议传输效率快,所需要资源少
应用常用: tcp协议常用于文件,邮件传输 udp协议常用于语音,视频,直播等实时性要求较高的场所
三、C语言中UDP实现
常见服务器类型: 处理多客户端的通讯处理方式不同,就导致服务器分为以下几种类型
(一)、UDP并发服务器之多进程并发
场景设计 多个udp客户端需要先验证密钥是否正确后,才允许进行数据交互。假设密钥为"root"。(类似于登录功能) 服务器接收到客户端信息,需要考虑两种情况 <1>A用户的密钥验证请求消息 <2>B用户的数据交互接收消息
框架图
使用场景 当UDP服务器与客户端交互多个数据报。问题在于每个客户都是往服务器端的同一个的端口发送数据,并用的同一个sockfd。并发服务器的每一个子进程如何正确区分每一个客户的数据报(涉及到进程的调度问题,如何避免一个子进程读取到不该它服务的客户发送来的数据报)。 解决的方法是服务器(知名端口)等待客户的到来,当一个客户到来后,记下其IP和port,然后服务器fork一个子进程,建立一个socket再bind一个随机端口,然后建立与客户端的连接,并处理该客户的请求。父进程继续循环,等待下一个客户的到来。 在tftpd中就是使用这种技术的 。
代码实现
UDP服务器代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#define LOGIN_KEY "root"
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
int init_socket(const char* ip,const char* port)
{
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == –1)
{
perror("socket");
return –1;
}
// 绑定IP + 端口
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(port));
inet_aton(ip,&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
if(bind(sockfd,(struct sockaddr*)&addr,addrlen)==–1)
{
fprintf(stderr,"bind failed\\n");
return –1;
}
return sockfd;
}
int authentication_key(const char* ip,const char* port)
{
int sockfd = init_socket(ip,port);
if(sockfd == –1)
{
return –1;
}
char buf[512]={0};
int len = sizeof(buf);
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int new_sockfd;
// 循环验证用户密钥
while(1)
{
memset(buf,0,len);
ssize_t recvbytes = recvfrom(sockfd,buf,len,0,(struct
sockaddr*)&addr,&addrlen);
if(recvbytes == –1)
{
perror("redvfrom");
return –1;
}
unsigned char loginstatus = (strncmp(buf,LOGIN_KEY,4)==0)?
LOGIN_SUCCESS:LOGIN_FAILURE;
if(loginstatus == LOGIN_SUCCESS)
{
pid_t pid = fork();
if(pid == –1)
{
perror("fork");
return –1;
}
else if(pid == 0)
{
// 执行的是子进程
close(sockfd);//密钥验证成功,不需要sockfd文件描述符
new_sockfd = init_socket(ip,"0");// 绑定0端口,系统会随机分配一个可用的端口号
sendto(new_sockfd,&loginstatus,sizeof(loginstatus),0,(struct
sockaddr*)&addr,addrlen);
break;
}
}
else
{
// 登录失败,使用原端口回复信息
ssize_t ret =
sendto(sockfd,&loginstatus,sizeof(loginstatus),0,(struct sockaddr*)&addr,addrlen);
}
}
return new_sockfd;
}
// 接收数据
void recv_data(int sockfd)
{
char buf[512]={0};
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
while(1)
{
memset(buf,0,sizeof(buf));
ssize_t recvbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&client_addr,&addrlen);
if(recvbytes== –1)
{
perror("recvfrom");
exit(EXIT_FAILURE);
}
printf("client ip:%s\\n",inet_ntoa(client_addr.sin_addr));
printf("client port:%d\\n",ntohs(client_addr.sin_port));
printf("client content:%s\\n",buf);
if(strncmp(buf,"quit",4)==0)
{
break;
}
}
close(sockfd);
exit(EXIT_SUCCESS);
}
void signal_handler(int signum)
{
// 回收子进程的资源
waitpid(–1,NULL,WNOHANG);
printf("%s\\n",strsignal(signum));
}
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\\n",argv[0]);
exit(EXIT_FAILURE);
}
// 回收僵尸态的子进程[子进程结束后,会发SIGCHLD信号]
if(signal(SIGCHLD,signal_handler)==SIG_ERR)
{
perror("signal error.");
exit(EXIT_FAILURE);
}
// 验证秘钥
int sockfd = authentication_key(argv[1],argv[2]);
// recv data
recv_data(sockfd);
return 0;
}
UDP客户端代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
// 发送交互消息
void send_message(int sockfd,struct sockaddr_in* addr,int addrlen)
{
char buf[512]={0};
while(1)
{
memset(buf,0,sizeof(buf));
printf("client:");
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)–1]='\\0';
int ret = sendto(sockfd,buf,strlen(buf),0,(struct
sockaddr*)addr,addrlen);
if(ret == –1)
{
perror("client sendto");
exit(EXIT_FAILURE);
}
if(strncmp(buf,"quit",4)==0)
{
break;
}
}
}
void send_authentication_key(int sockfd,struct sockaddr_in* addr,struct
sockaddr_in* server_data_addr,int addrlen)
{
char buf[512]={0};
unsigned char flag = LOGIN_FAILURE;
while(1)
{
putchar('>');
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin); //接收用户的输入
buf[strlen(buf)–1]='\\0';
int ret = sendto(sockfd,buf,strlen(buf),0,(struct
sockaddr*)addr,addrlen);
if(ret == –1)
{
perror("client sendto");
exit(EXIT_FAILURE);
}
// 接收服务器返回的消息
ssize_t recvbytes = recvfrom(sockfd,&flag,sizeof(flag),0,(struct
sockaddr*)server_data_addr,&addrlen);
if(recvbytes == –1)
{
perror("client recvfrom");
exit(EXIT_FAILURE);
}
if(flag == LOGIN_SUCCESS)
{
break;
}
}
}
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\\n",argv[0]);
exit(EXIT_FAILURE);
}
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == –1)
{
perror("socket");
return –1;
}
// 服务器端的地址
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
// 处理数据的服务端信息
struct sockaddr_in server_data_addr;
memset(&server_data_addr,0,sizeof(struct sockaddr_in));
server_data_addr.sin_family =AF_INET;
// 发送密钥信息
send_authentication_key(sockfd,&addr,&server_data_addr,addrlen);
// 发送交互消息
send_message(sockfd,&server_data_addr,addrlen);
//关闭文件描述符
close(sockfd);
return 0;
}
(二)、UDP并发服务器之多线程并发
多线程并发服务器和多进程并发服务器的思路大同小异,都是服务器端等待客户端,当一个客户端到来后,记录其IP和port,然后同理,创建子线程,建立一个socket再bind一个随机端口,然后建立与客户端的连接,并处理该客户端的请求。父线程继续循环,等待下一个客户端的到来。 一个进程是可以绑定多个端口。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define LOGIN_KEY "root"
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
typedef struct{
char ip[256];// ip
unsigned char flag; // 登录标识
struct sockaddr_in addr;// 数据交互的地址
}packet_t;
int init_socket(const char* ip,const char* port)
{
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == –1)
{
perror("socket");
return –1;
}
// 绑定IP + 端口
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(port));
inet_aton(ip,&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
if(bind(sockfd,(struct sockaddr*)&addr,addrlen)==–1)
{
fprintf(stderr,"bind failed\\n");
return –1;
}
return sockfd;
}
void* message_handle(void* argv)
{
void recv_data(int sockfd);
// 接收参数
packet_t* packet = (packet_t*)argv;
//创建新的socket
int new_sockfd = init_socket(packet->ip,"0");
if(new_sockfd == –1)
{
perror("init_socket");
return NULL;
}
// 给客户端响应登录成功
sendto(new_sockfd,&(packet->flag),sizeof(packet->flag),0,(struct
sockaddr*)&(packet->addr),sizeof(struct sockaddr));
// 数据交互
recv_data(new_sockfd);
pthread_exit(NULL);
}
void authentication_key(const char* ip,const char* port)
{
int sockfd = init_socket(ip,port);
if(sockfd == –1)
{
return –1;
}
char buf[512]={0};
int len = sizeof(buf);
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int new_sockfd;
pthread_t pid;
// 循环验证用户密钥
while(1)
{
memset(buf,0,len);
ssize_t recvbytes = recvfrom(sockfd,buf,len,0,(struct
sockaddr*)&addr,&addrlen);
if(recvbytes == –1)
{
perror("redvfrom");
return –1;
}
unsigned char loginstatus = (strncmp(buf,LOGIN_KEY,4)==0)?
LOGIN_SUCCESS:LOGIN_FAILURE;
if(loginstatus == LOGIN_SUCCESS)
{
// 创建子线程
packet_t packet;
strcpy(packet.ip,ip);
packet.flag = loginstatus;
packet.addr = addr;
pthread_create(&pid,NULL,message_handle,&packet);
// 线程分离
pthread_detach(pid);
}
else
{
// 登录失败,使用原端口回复信息
ssize_t ret =
sendto(sockfd,&loginstatus,sizeof(loginstatus),0,(struct sockaddr*)&addr,addrlen);
}
}
}
// 接收数据
void recv_data(int sockfd)
{
char buf[512]={0};
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
while(1)
{
memset(buf,0,sizeof(buf));
ssize_t recvbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&client_addr,&addrlen);
if(recvbytes== –1)
{
perror("recvfrom");
exit(EXIT_FAILURE);
}
printf("client ip:%s\\n",inet_ntoa(client_addr.sin_addr));
printf("client port:%d\\n",ntohs(client_addr.sin_port));
printf("client content:%s\\n",buf);
if(strncmp(buf,"quit",4)==0)
{
break;
}
}
close(sockfd);
}
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\\n",argv[0]);
exit(EXIT_FAILURE);
}
// 验证秘钥
authentication_key(argv[1],argv[2]);
return 0;
}
客户端代码与进程那块一样
评论前必须登录!
注册