UDP是面向无连接的用户数据报协议,在传输前不需要先建立连接,且UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,目的主机的传输层收到UDP报文后,不需要给出任何确认信息,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。在TCP/IP协议层次模型中,UDP位于IP层之上。应用程序访问UDP层然后使用IP层传送数据报。IP层的报头指明了源主机和目的主机地址,而UDP层的报头指明了主机上的源端口和目的端口。
UDP报文包括报头和数据两个部分,其中报头部分包括以下四个域组成,每个域各占用2个字节,对于该四个域的概念解释摘自网络。
UDP源端口号、目的端口号:数据发送一方(可以是客户端或服务器端)将UDP数据包通过源端口发送出去,而数据接收一方则通过目标端口接收数据。有的网络应用只能使用预先为其预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为UDP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。一般来说,大于49151的端口号都代表动态端口。
UDP报文长度:数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节。不过,一些实际应用往往会限制数据报的大小,有时会降低到8192字节。
UDP校验和:UDP协议使用报头中的校验值来保证数据的安全。校验值首先在数据发送方通过特殊的算法计算得出,在传递到接收方之后,还需要再重新计算。如果某个数据报在传输过程中被第三方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算值将不会相符,由此UDP协议可以检测是否出错。这与TCP协议是不同的,后者要求必须具有校验值。许多链路层协议都提供错误检查,包括流行的以太网协议,也许你想知道为什么UDP也要提供检查和校验。其原因是链路层以下的协议在源端和终端之间的某些通道可能不提供错误检测。虽然UDP提供有错误检测,但检测到错误时,UDP不做错误校正,只是简单地把损坏的消息段扔掉,或者给应用程序提供警告信息。
1. UDP编程整体流程(Client/Server架构)
1.1 客户端Client程序流程
(1) 利用socket()创建一个UDP网络套接字
(2) 使用connect()来建立与服务程序的连接,这点可能和很多人的做法不一样,因为很多人平常写UDP程序可能并不会去建立连接,注意这与TCP协议不同,UDP的connect()并没有与服务程序三次握手,上面说了UDP是非连接的,实际上也可以是连接的,这样做的原因主要是使用连接的UDP,kernel可以直接返回错误信息给用户程序,从而避免由于没有接收到数据而导致调用recvfrom()一直等待下去,看上去好像客户程序没有反应一样。
(3) 向服务程序发送数据,因为使用连接的UDP,所以使用write()来替代sendto()。这里的数据直接从标准输入读取用户输入。
(4) 接收服务程序发回的数据,同样使用read()来替代recvfrom()。
(5) 处理接收到的数据,这里根据需求来设计。
(6) 最后是关闭UDP套接字
1.2 服务器端Server程序流程
(1) 利用socket()创建一个UDP网络套接字
(2) 初始化服务器地址结构变量,主要包括服务器的IP和端口号,以便让客服端能够连接
(3) 使用bind()把上面的socket和定义的服务器地址结构变量绑定,这里要检查bind()是否执行成功,如果有错误就退出,这样可以防止服务程序重复运行的问题。
(4) 服务器进入循环等待程序,使用recvfrom()进入等待状态,直到接收到客户程序发送的数据,就处理收到的数据并解析,并向客户程序发送反馈,这里也是根据需求来设计。
2. 创建套接字
int socket(int family, int type, int protocol);
功能:创建一个用于网络通信的socket套接字(类似于文件描述符)。
参数:
int family:协议族,一般写AF_INET(ipv4协议),
int type:套接字类型,SOCK_STREAM用于TCP,SOCK_DGRAM用于UDP,SOCK_RAM用于原始套接字
int protocol:协议类型,一些写0表示系统根据前两个参数自动匹配
返回值:套接字描述符
特点:socket创建套接字时,系统不会分配端口,而且socket创建的是主动套接字;但作为服务器时,往往需要修改为被动的
3. 连接函数
int connect(int sockfd,const struct sockaddr *addr,socklen_t len);
功能:主动跟服务器建立链接,连接成功后才可以开始传输数据
参数:
sockfd:socket套接字 addr: 需连接的服务器地址结构
addrlen:地址结构体长度
返回值:成功:0;失败:其他
注意:connect函数建立连接之后不会产生新的套接字
3. 发送与接收数据
3.1 发送函数,可以用write替代
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);
功能:向to结构体指针中指定的目标,发送UDP数据
参数:
sockfd:套接字
buf: 发送数据缓冲区
nbytes: 发送数据缓冲区的大小
flags:一般为0
to: 指向目的主机地址结构体的指针
addrlen:to所指向内容的长度
返回值:成功:发送数据的字符数;失败:-1
注意:通过to和addrlen确定目的地址,可以发送0长度的UDP数据包
3.2 接收函数,可以用read替代
ssize_t recvfrom(int sockfd, void *buf, size_t nbytes,int flags,struct sockaddr *from, socklen_t *addrlen);
功能:接收UDP数据,并将源地址信息保存在from指向的结构中
参数:
sockfd:套接字
buf: 接收数据缓冲区
nbytes:接收数据缓冲区的大小
flags: 套接字标志(常为0)
from: 源地址结构体指针,用来保存数据的来源
addrlen: from所指内容的长度
返回值:成功:接收到的字符数;失败: -1
注意:通过from和addrlen参数存放数据来源from和addrlen可以为NULL, 表示不保存数据来源
4. 客户端Client编程实例
源代码:
运行结果:
5. 服务器端Server编程实例
服务器的编程基本上客户端一样,这里我只给出大概步骤不在给出所有代码,需要的可以留下邮箱,我会把client和server的源代码以及可执行程序都发给你。
编程步骤:
//step1:create socket fd
sockfd = socket(AF_INET ,SOCK_DGRAM ,0);
//step2:setup server addr
memset(&server_addr ,0 ,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//step3:bind the server address
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0)
{
perror("bind the server address");
exit(-1);
}
//step4:wait client enter
printf("###:UDP server is running,waitting client...\n");
while (1)
{
char recv_buf[50] = "";
int recv_len = 0;
char client_ip[INET_ADDRSTRLEN] = "";
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// receive from client
recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0,\
(struct sockaddr *)&client_addr, &client_addr_len);
inet_ntop(AF_INET ,&client_addr.sin_addr ,client_ip ,INET_ADDRSTRLEN);
printf("###:client_ip=%s \n" ,client_ip);
printf("###:recv_buf=%s \n" ,recv_buf);
// echo server
sendto(sockfd, recv_buf, recv_len, 0,\
(struct sockaddr *)&client_addr ,sizeof(client_addr));
}
//step5:close socket
运行效果: