概述
在上一篇博客中我写到了TCP,既然学了TCP,那就不得不提到UDP了。TCP和UDP就相当于是两个形影不离的小伙伴了,总是被提到然后互相进行比较。至于两个协议各自的优缺点呢我这里就不一一叙述了。下面呢我们主要学习一下UDP服务器实现通信的写法。
UDP通信的实现方法
在上一篇博客中我们学习过socket编程的几个相关函数,但是我们也知道UDP和TCP还是存在一些差异的,所以这里就会导致我们在某些方面的实现方法不一样,下面我简单提一下它们之间的差异:
①调用socket函数创建socket的时候,TCP是基于字节流传输的,而UDP是基于数据包传输的,所以socket函数的第二个参数是SOCK_DGRAM。
②UDP服务器不需要通过三次握手来建立连接,所以UDP不需要listen去进行监听。
③在绑定之后可以直接进行读写,不过不同于TCP,而是自己特有的函数,recvfrom和sendto。
UDP的读写函数
数据接收函数recvfrom
其中参数sockfd表示的是当前的socket的fd;参数buf表示的是接受到数据指针;参数flags默认缺省 0;参数addr为输入输出型参数,输出的是发送方的协议地址;参数addrlen为输入输出型参数,填写的是addr的大小
数据发送函数sendto
其中参数的含义和上面的recvfrom函数的参数含义是一样的,可以直接参考上面的参数含义。
套接字UDP通信
服务器端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
void usage(const char* proc)
{
printf("Usage: %s, [udp_ip], [udp_port]\n", proc);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
return 1;
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);//创建套接字,因是数据包传输所以是SOCK_DGRAM
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0){//绑定端口号与IP
perror("bind");
return 3;
}
//绑定成功后不需要去监听,因为它是不可靠的,所以不需要建立连接和保存连接
while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
//发送数据
char buf[1024];
if(recvfrom(sock, buf, 1024, 0, (struct sockaddr*)&client_addr, &len) < 0){
perror("recvrom");
return 4;
}
printf("recv is %s\n", buf);
char buff[1024];
printf("server # ");
scanf("%s", buff);//从标准输入中读取数据
//接收数据
if(sendto(sock, buff, 1024, 0, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0){
perror("sendto");
return 5;
}
}
close(sock);
return 0;
}
客户端
#include<stdio.h>编写完成代码之后我们去运行一下代码:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
void usage(char* proc)
{
printf("Usage: %s, [udp_ip], [udp_port]\n", proc);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
return 1;
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
while(1){
char buff[1024];
printf("client # ");
scanf("%s", buff);//这里相当于从标准输入中读取数据
if(sendto(sock, buff, 1024, 0, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0){
perror("sendto");
return 3;
}
char buf[1024];
socklen_t len = sizeof(server_addr);
if(recvfrom(sock, buf, 1024, 0, (struct sockaddr*)&server_addr, &len) < 0){
perror("recvfrom");
return 4;
}
printf("recv is %s\n", buf);
}
close(sock);
return 0;
}
这是我们就可以去输入想要输入的内容了
我们可以看到两边进行互相间的通信了
这个时候我们可以通过命令netstat -nlup查看系统中的UDP服务
我们可以看到运行的ip为0,端口号为8080的UDP服务。
如何在用户空间实现UDP的可靠性
我们都知道相比TCP而言,UDP是不可靠的,那么如何让UDP变得更加可靠呢?
UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
实现确认机制、重传机制、窗口确认机制。
如果你不利用Linux协议栈以及上层socket机制,自己通过抓包和发包的方式去实现可靠性传输,那么必须实现如下功能:
发送:包的分片、包确认、包的重发
接收:包的调序、包的序号确认
目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。
【RUDP】
RUDP 提供一组数据服务质量增强机制,如拥塞控制的改进、重发机制及淡化服务器算法等,从而在包丢失和网络拥塞的情况下, RTP 客户机(实时位置)面前呈现的就是一个高质量的 RTP 流。在不干扰协议的实时特性的同时,可靠 UDP 的拥塞控制机制允许 TCP 方式下的流控制行为。
【RTP】
实时传输协议(RTP)为数据提供了具有实时特征的端对端传送服务,如在组播或单播网络服务下的交互式视频音频或模拟数据。应用程序通常在 UDP 上运行 RTP 以便使用其多路结点和校验服务;这两种协议都提供了传输层协议的功能。但是 RTP 可以与其它适合的底层网络或传输协议一起使用。如果底层网络提供组播方式,那么 RTP 可以使用该组播表传输数据到多个目的地。
RTP 本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于底层服务去实现这一过程。 RTP 并不保证传送或防止无序传送,也不确定底层网络的可靠性。 RTP 实行有序传送, RTP 中的序列号允许接收方重组发送方的包序列,同时序列号也能用于决定适当的包位置,例如:在视频解码中,就不需要顺序解码。
【UDT】
基于UDP的数据传输协议(UDP-basedData Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等。