Linux下UDP简介及程序设计

时间:2021-05-30 00:22:55

一、UDP简介

UDP(User Datagram Protocol),用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP提供了无连接通信,且不对传送数据包进行可靠性保证,适合于一次传输少量数据,UDP传输的可靠性由应用层负责。常用的UDP端口号有:

应用协议 端口号
DNS 53
TFTP 69
SNMP 161

UDP是无连接的、不可靠的数据报协议;既然他不可靠为什么还要用呢?其一:当应用程序使用广播或多播时只能使用UDP协议;其二:由于他是无连接的,所以数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序,如DNS、TFTP、SNMP等。因为UDP套接字是无连接的,如果一方的数据报丢失,那另一方将无限等待,解决办法是设置一个超时。

UDP报头由4个域组成,其中每个域各占用2个字节,具体如下:

Linux下UDP简介及程序设计

可以看到,UDP其实就是在IP报文中添加了端口信息,使数据到达主机后送达至相应端口的应用程序。下面是通过wireshark抓的一个UDP数据包:

Linux下UDP简介及程序设计

UDP服务器

与基于TCP的应用程序相同的是,基于UDP的服务器应用程序也被分配了公认端口或已注册的端口。当上述应用程序或进程运行时,它们就会接受与所分配端口相匹配的数据。当UDP收到用于某个端口的数据报时,它就会按照应用程序的端口号将数据发送到相应的应用程序。

UDP客户端

对于TCP而言,客户端/服务器模式的通信初始化采用由客户端应用程序向服务器进程请求数据的形式。而UDP客户端进程则是从动态可用端口中随机挑选一个端口号,用来作为会话的源端口。而目的端口通常都是分配到服务器进程的公认端口或已注册的端口。

udp不一定要有服务端和客户端

udp不一定要有服务端和客户端,因为任何一端的程序完全不知道对方的IP,解决方案就是添加组播或广播,接收到对方的数据包了你就知道对方的ip地址了,然后就可以传输了。所谓的服务器和客户端本质是两个客户端,但当做认为是服务端和客户端,这样只是为了方便区分两个客户端以及便于理解。

UDP传输与IP传输的区别

UDP传输与IP传输非常类似,协议都是以数据包(datagram)的方式传输。那么,我们为什么不直接使用IP协议而要额外增加一个UDP协议呢? 一个重要的原因是IP协议中并没有端口(port)的概念。IP协议进行的是IP地址到IP地址的传输,这意味者两台计算机之间的对话。但每台计算机中需要有多个通信通道,并将多个通信通道分配给不同的进程使用。一个端口就代表了这样的一个通信通道。UDP协议实现了端口,从而让数据包可以在送到IP地址的基础上,进一步可以送到某个端口。

二、端口与socket

端口(port)是伴随着传输层诞生的概念。它可以将网络层的IP通信分送到各个通信通道。UDP协议和TCP协议尽管在工作方式上有很大的不同,但它们都建立了从一个端口到另一个端口的通信。

Linux下UDP简介及程序设计随着我们进入传输层,我们也可以调用操作系统中的API,来构建socket。Socket是操作系统提供的一个编程接口,它用来代表某个网络通信。应用程序通过socket来调用系统内核中处理网络协议的模块,而这些内核模块会负责具体的网络协议的实施。这样,我们可以让内核来接收网络协议的细节,而我们只需要提供所要传输的内容就可以了,内核会帮我们控制格式,并进一步向底层封装。因此,在实际应用中,我们并不需要知道具体怎么构成一个UDP包,而只需要提供相关信息(比如IP地址,比如端口号,比如所要传输的信息),操作系统内核会在传输之前会根据我们提供的相关信息构成一个合格的UDP包(以及下层的包和帧)。

注:上图中的互相通信的两个端口号可以相同,也可以不同,注意本地端口与目标端口的去吧。

三、UDP常用函数讲解

1、socket函数

#include <sys/types.h>
#include <sys/socket.h> //函数原型
int socket(int domain, int type, int protocol);

为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。

第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议)。

第二个参数指明套接字类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);TCP一般选择SOCK_STREAM,而UDP选择SOCK_DGRAM。

2、bind函数

为套接字分配一个本地IP和本地端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。

#include <sys/types.h>
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第一个参数是socket函数返回的套接字的文件描述符。第二和第三个参数分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。

sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:

struct sockaddr
{
sa_family_t sa_family; //地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
}

sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:

struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port ; //16位 TCP/IP 端口号
struct in_addr sin_addr; //32位 IP 地址
char sin_zero[8]; //不使用
};

sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。 sockaddr_in 是internet环境下套接字的地址形式。

所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用强制类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

若套接字使用了bind函数,则绑定的端口号,即为该套接字的本地端口。

注:sin_addr赋值htonl(INADDR_ANY),则绑定的IP地址即任何主机上的地址。

3、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 *src_addr, socklen_t *addrlen);
//返回发送数据的长度 -非0成功   -1-失败

第一个参数为一个绑定本地IP和本地端口的套接字的文件描述符(接收方),第二个参数为接收数据缓冲区,第三个参数为缓冲区长度,第四个参数flag是传输控制标志。第五个参数是表示发送方IP和发送方端口的sockaddr_in,最后一个参数为发送方sockaddr_in的长度。

4、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 *dest_addr, socklen_t addrlen);
//返回发送数据的长度 -非0成功   -1-失败

第一个参数为一个绑定本地IP和本地端口的套接字的文件描述符(发送方),第二个参数为接收数据缓冲区,第三个参数为缓冲区长度,flags参数是传输控制标志。参数dest_addr指明数据将发往的目标地址和目标端口(接收方),他的大小由addrlen参数来指定。

四、参考程序

使用UDP套接字编程可以实现基于TCP/IP协议的面向无连接的通信,它分为服务器端和客户端两部分,其主要实现过程如下图所示:

Linux下UDP简介及程序设计

UDP服务器程序

UDP编程的服务器端一般步骤是:

(1)使用 socket() 来建立一个UDP socket。
(2)初始化 sockaddr_in 结构的变量,并赋值。
(3)使用 bind() 把上面的socket和定义的IP地址和端口绑定。这里检查 bind() 是否执行成功,如果有错误就退出。这样可以防止服务程序重复运行的问题。
(4)进入无限循环程序,使用recvfrom()进入等待状态,直到接收到客户端发送的数据,就处理收到的数据,并向客户端发送反馈。

Linux下UDP服务器套接字程序,服务器接收客户端发送的信息并显示,同时显示客户的IP地址、端口号,并向客户端发送信息。如果服务器接收的客户信息为“bye”,则退出循环,并关闭套接字。

/* udpserver.c */
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h> #define PORT 31188 //端口号
#define MAXDATASIZE 100 int main()
{
int sockfd;
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t addrlen;
int num;
char buf[MAXDATASIZE]; //使用 socket() 来建立一个UDP socket
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("Creatingsocket failed.");
exit(1);
} //初始化 sockaddr_in 结构的变量,并赋值
bzero(&server,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(PORT); //服务器本地端口为31188
server.sin_addr.s_addr= htonl (INADDR_ANY); //使用 bind() 把上面的socket和定义的IP地址和端口绑定。
if(bind(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{
perror("Bind()error.");
exit(1);
}
addrlen=sizeof(client); //进入无限循环程序,使用recvfrom()进入等待状态,直到接收到客户端发送的数据
while(1)
{
num =recvfrom(sockfd,buf,MAXDATASIZE,0,(struct sockaddr*)&client,&addrlen);
//处理收到的数据
if (num < 0)
{
perror("recvfrom() error\n");
exit(1);
}
buf[num] = '\0';
printf("You got a message (%s%) from client.\nIt's ip is%s, port is %d.\n",buf,inet_ntoa(client.sin_addr),htons(client.sin_port)); //服务器本地端口为31188,并向目标端口31188(客户端端口)发送反馈
sendto(sockfd,"Welcometo my server.\n",22,0,(struct sockaddr *)&client,addrlen); if(!strcmp(buf,"bye"))
break;
}
close(sockfd);
}
//执行命令./ udpserver,观察结果

UDP客户端程序

UDP编程的客户端端一般步骤是:

(1)使用 socket() 来建立一个UDP socket。
(2)初始化 sockaddr_in 结构的变量,并赋值。
(3)使用 bind() 把上面的socket和定义的IP地址和端口绑定。也可以使用connect()来建立与服务程序的连接。
(4)发送数据,用函数sendto()。如果使用了连接的UDP,要使用write()来替代sendto()。

Linux下UDP服务器套接字程序,服务器接收客户端发送的信息并显示,同时显示客户的IP地址、端口号,并向客户端发送信息。如果服务器接收的客户信息为“bye”,则退出循环,并关闭套接字。

/* udpclient.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h> #define PORT 31188 //端口号
#define MAXDATASIZE 100 int main(int argc, char *argv[])
{
int sockfd, num;
char buf[MAXDATASIZE]; struct hostent *he; //终端输入IP地址
struct sockaddr_in server,peer; if (argc !=3)
{
printf("Usage: %s <IP Address><message>\n",argv[0]);
exit(1);
} //获得argv[1]-终端输入IP地址
if ((he=gethostbyname(argv[1]))==NULL)
{
printf("gethostbyname()error\n");
exit(1);
} //使用 socket() 来建立一个UDP socket
if ((sockfd=socket(AF_INET, SOCK_DGRAM,0))==-1)
{
printf("socket() error\n");
exit(1);
} //初始化 sockaddr_in 结构的变量,并赋值
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT); //客户端本地端口为31188
server.sin_addr= *((struct in_addr *)he->h_addr); sendto(sockfd, argv[2],strlen(argv[2]),0,(struct sockaddr *)&server,sizeof(server));
socklen_t addrlen;
addrlen=sizeof(server); //进入无限循环程序,使用recvfrom()进入等待状态,直到接收到客户端发送的数据
while (1)
{
//客户端本地端口为31188(即服务器发送的目标端口),接收来自服务器的反馈
if((num=recvfrom(sockfd,buf,MAXDATASIZE,0,(struct sockaddr *)&peer,&addrlen))== -1)
{
printf("recvfrom() error\n");
exit(1);
} //处理收到的数据
if (addrlen != sizeof(server) ||memcmp((const void *)&server, (const void *)&peer,addrlen) != 0)
{
printf("Receive message from otherserver.\n");
continue;
} buf[num]='\0';
printf("Server Message:%s\n",buf);
break;
} close(sockfd);
}
//执行命令./ udpclient 127.0.0.1 hello

实验结果

服务器端:

Linux下UDP简介及程序设计

客户端:

Linux下UDP简介及程序设计

注:发送方的发送函数的目标端口要求是接收方(接收函数)的本地端口。

参考:**

UDP协议学习

UDP程序设计

sockaddr和sockaddr_in详解

TCP和UDP的区别还有服务器和客户端的执行步骤