本篇文章简单描述了UDP传输协议的工作原理及特点。

理解UDP

UDP和TCP一样同属于TCP/IP协议栈的第二层,即传输层。

UDP套接字的特点

UDP的工作方式类似于传统的信件邮寄过程。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然信件邮寄过程可能会发生丢失,我们也无法随时知晓对方是否已收到信件。也就是说信件是一种不可靠的传输方式,同样的,UDP所提供的也是一种不可靠的数据传输方式(以信件类比UDP只是通信形式上一致性,之前也以电话通信的方式类比了TCP的通信方式,而实际上从通信速度上来讲UDP通常是要快于TCP的;每次交换的数据量越大,TCP的传输速率就越接近于UDP)。因此,如果仅考虑可靠性,TCP显然由于UDP;但UDP在通信结构上较TCP更为简洁,通常性能也要优于TCP。

区分TCP和UDP最重要的标志是流控制,流控制赋予了TCP可靠性的特点,也说TCP的生命在于流控制。

UDP内部工作原理

与TCP不同,UDP不会进行流控制,其在数据通信中的作用如下图所示。可以看出,IP的作用就是让离开主机B的UDP数据包准确传递到主机A,而UDP则是把UDP包最终交给主机A的某一UDP套接字。UDP最重要的作用就是根据端口号将传输到主机的数据包交付给最终的UDP套接字。

数据包传输过程UDP和IP的作用

UDP的高效使用

TCP用于对可靠性要求较高的场景,比如要传输一个重要文件或是压缩包,这种情况往往丢失一个数据包就会引起严重的问题;而对于多媒体数据来说,丢失一部分数据包并没有太大问题,因为实时性更为重要,速度就成为了重要考虑因素。TCP慢于UDP主要在于以下两点:

  • 收发数据前后进行的连接及清理过程
  • 收发数据过程中为保证可靠性而添加的流控制

因此,如果收发的数据量小但需要频繁的连接时,UDP比TCP更为高效。

基于UDP的服务器端/客户端

和TCP不同,UDP服务器端/客户端并不需要在连接状态下交换数据,UDP的通信只有创建套接字和数据交换的过程。TCP套接字是一对一的关系,且服务器端还需要一个额外的TCP套接字用于监听连接请求;而UDP通信中,无论服务器端还是客户端都只需要一个套接字即可,且可以实现一对多的通信关系。下图展示了一个UDP套接字与两台主机进行数据交换的过程。

UDP套接字通信模型

基于UDP的数据I/O函数

TCP套接字建立连接之后,数据传输过程便无需额外添加地址信息,因为TCP套接字会保持与对端的连接状态;而UDP则没有这种连接状态,因此每次数据交换过程都需要添加目标地址信息。下面是UDP套接字数据传输函数,与TCP传输函数最大的区别在于,该函数需要额外添加传递目标的地址信息。

#include <sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
-> 成功时返回传输的字节数,失败时返回-

由于UDP数据的发送端并不固定,因此,UDP套接字的数据接收函数定义了存储发送端地址信息的数据结构。

#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t addrlen);
-> 成功时返回接收的字节数,失败时返回-

基于UDP的回声服务器端/客户端

UDP通信函数调用流程

UDP不同于TCP,不存在请求连接和受理连接的过程,因此某种意义上并没有明确的服务器端和客户端之分。如下是示例源码。

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz; struct sockaddr_in serv_adr, clnt_adr;
if(argc!=){
printf("Usage : %s <port>\n", argv[]);
exit();
} serv_sock=socket(PF_INET, SOCK_DGRAM, );
if(serv_sock==-)
error_handling("UDP socket creation error"); memset(&serv_adr, , sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[])); if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-)
error_handling("bind() error"); while()
{
clnt_adr_sz=sizeof(clnt_adr);
str_len=recvfrom(serv_sock, message, BUF_SIZE, ,
(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
sendto(serv_sock, message, str_len, ,
(struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
close(serv_sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

uecho_server

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz; struct sockaddr_in serv_adr, from_adr;
if(argc!=){
printf("Usage : %s <IP> <port>\n", argv[]);
exit();
} sock=socket(PF_INET, SOCK_DGRAM, );
if(sock==-)
error_handling("socket() error"); memset(&serv_adr, , sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[]);
serv_adr.sin_port=htons(atoi(argv[])); while()
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break; sendto(sock, message, strlen(message), ,
(struct sockaddr*)&serv_adr, sizeof(serv_adr));
adr_sz=sizeof(from_adr);
str_len=recvfrom(sock, message, BUF_SIZE, ,
(struct sockaddr*)&from_adr, &adr_sz); message[str_len]=;
printf("Message from server: %s", message);
}
close(sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

uecho_client

示例代码运行结果

UDP客户端套接字的地址分配

从上述示例源码来看,服务器端UDP套接字需要手动bind地址信息,而客户端UDP套接字则无此过程。我们已经知道,客户端TCP套接字是在调用connect函数的时机,由操作系统为我们自动绑定了地址信息;而客户端UDP套接字同样存在该过程,如果没有手动bind地址信息,则在首次调用sendto函数时自动分配IP和端口号等地址信息。和TCP一样,IP是主机IP,端口号则随机分配(客户的临时端口是在第一次调用sendto 时一次性选定,不能改变;然而客户的IP地址却可以随客户发送的每个UDP数据报而变动(如果客户没有绑定一个具体的IP地址到其套接字上)。其原因在于如果客户主机是多宿的,客户有可能在两个目的地之间交替选择)。

UDP数据传输特性和connect函数调用

之前我们介绍了TCP传输数据不存在数据边界,下面将会验证UDP数据传输存在数据边界的特性,并介绍UDP传输调用connect函数的作用。

存在数据边界的UDP套接字

UDP协议具有数据边界,这就意味着数据交换的双方输入函数和输出函数必须一一对应,这样才能保证可完整接收数据。如下是验证UDP存在数据边界的示例源码。

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
struct sockaddr_in my_adr, your_adr;
socklen_t adr_sz;
int str_len, i; if(argc!=){
printf("Usage : %s <port>\n", argv[]);
exit();
} sock=socket(PF_INET, SOCK_DGRAM, );
if(sock==-)
error_handling("socket() error"); memset(&my_adr, , sizeof(my_adr));
my_adr.sin_family=AF_INET;
my_adr.sin_addr.s_addr=htonl(INADDR_ANY);
my_adr.sin_port=htons(atoi(argv[])); if(bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr))==-)
error_handling("bind() error"); for(i=; i<; i++)
{
sleep(); // delay 5 sec.
adr_sz=sizeof(your_adr);
str_len=recvfrom(sock, message, BUF_SIZE, ,
(struct sockaddr*)&your_adr, &adr_sz); printf("Message %d: %s \n", i+, message);
}
close(sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
} /*
root@my_linux:/home/swyoon/tcpip# gcc bound_host1.c -o host1
root@my_linux:/home/swyoon/tcpip# ./host1
Usage : ./host1 <port>
root@my_linux:/home/swyoon/tcpip# ./host1 9190
Message 1: Hi!
Message 2: I'm another UDP host!
Message 3: Nice to meet you
root@my_linux:/home/swyoon/tcpip# */

bound_hostA

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
struct sockaddr_in my_adr, your_adr;
socklen_t adr_sz;
int str_len, i; if(argc!=){
printf("Usage : %s <port>\n", argv[]);
exit();
} sock=socket(PF_INET, SOCK_DGRAM, );
if(sock==-)
error_handling("socket() error"); memset(&my_adr, , sizeof(my_adr));
my_adr.sin_family=AF_INET;
my_adr.sin_addr.s_addr=htonl(INADDR_ANY);
my_adr.sin_port=htons(atoi(argv[])); if(bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr))==-)
error_handling("bind() error"); for(i=; i<; i++)
{
sleep(); // delay 5 sec.
adr_sz=sizeof(your_adr);
str_len=recvfrom(sock, message, BUF_SIZE, ,
(struct sockaddr*)&your_adr, &adr_sz); printf("Message %d: %s \n", i+, message);
}
close(sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
} /*
root@my_linux:/home/swyoon/tcpip# gcc bound_host1.c -o host1
root@my_linux:/home/swyoon/tcpip# ./host1
Usage : ./host1 <port>
root@my_linux:/home/swyoon/tcpip# ./host1 9190
Message 1: Hi!
Message 2: I'm another UDP host!
Message 3: Nice to meet you
root@my_linux:/home/swyoon/tcpip# */

bound_hostB

示例代码运行结果

调用connect函数的UDP套接字

TCP套接字需要手动注册传输数据的目标IP和端口号,而UDP则是调用sendto函数时自动完成目标地址信息的注册,该过程如下

  • 第一阶段:向UDP套接字注册目标IP和端口号
  • 第二阶段:传输数据
  • 第三阶段:删除UDP套接字中注册的目标地址信息

每次调用sendto函数都会重复执行以上过程,这也是为什么同一个UDP套接字可和不同目标进行数据交换的原因。像UDP这种未注册目标地址信息的套接字称为未连接套接字,而TCP这种注册了目标地址信息的套接字称为已连接connected套接字。当需要和同一目标主机进行长时间通信时,UDP的这种无连接的特点则会非常低效。通过调用connect函数使UDP变为已连接套接字则会有效改善这一点,因为上述的第一阶段和第三阶段会占用整个通信过程近1/3的时间。

已连接的UDP套接字不仅可以使用之前的sendto和recvfrom函数,还可以使用没有地址信息参数的write和read函数(需要注意的是,调用connect函数的已连接UDP套接字并非真的与目标UDP套接字建立连接,仅仅是向本端UDP套接字注册了目标IP和端口号信息而已)。修改之前的ucheo_client代码为已连接UDP套接字如下。

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz; struct sockaddr_in serv_adr, from_adr;
if(argc!=){
printf("Usage : %s <IP> <port>\n", argv[]);
exit();
} sock=socket(PF_INET, SOCK_DGRAM, );
if(sock==-)
error_handling("socket() error"); memset(&serv_adr, , sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[]);
serv_adr.sin_port=htons(atoi(argv[])); connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); while()
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
/*
sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&serv_adr, sizeof(serv_adr));
*/
write(sock, message, strlen(message)); /*
adr_sz=sizeof(from_adr);
str_len=recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_adr, &adr_sz);
*/
str_len=read(sock, message, sizeof(message)-); message[str_len]=;
printf("Message from server: %s", message);
}
close(sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

uecho_con_client

关于recvfrom函数的思考

recvfrom是一个阻塞函数,那么该函数的返回时机是怎样的?

显然如果客户端正常收到应答数据,recvfrom自然可以返回。但如果发生其他情况呢?

对端调用close函数关闭UDP套接字时是否会发送EOF信息,本端recvfrom函数又会有什么动作吗?是否会像TCP套接字的read函数那样收到EOF信息而返回0?

由于UDP套接字无连接的特性,即使对端调用close函数关闭套接字,本端也不会有任何感知,recvfrom自然不会返回。那如果是调用了connect函数的已连接UDP套接字呢,服务端的close函数调用是否会使客户端的recvfrom函数退出阻塞状态?

仍然不会。因为调用connect函数的已连接UDP套接字并非真的像TCP套接字那样建立了连接,仅仅是为了数据交换的便利性向本端UDP套接字注册了目标地址信息而已;而对端并不能感知到这些,close函数自然也不会向TCP那样向本端发送文件结束标志EOF。因此,正常情况下,只有接收到发送端消息的recvfrom函数才会退出阻塞状态而返回。

如果一个客户端数据报丢失(譬如说,被客户主机与服务主机之间的某个路由器丢弃),客户端将永远阻塞于recvfrom 调用,等待一个永远不会到达的服务器应答。类似地,如果客户端数据报到达服务器,但是服务器的应答丢失了,客户端也将永远阻塞于recvfrom 调用。防止这样永久阻塞的一般方法是给客户端的recvfrom 调用设置一个超时时间。

关于recvfrom函数和UDP协议的进一步内容可以参考《UNIX网络编程》等相关书籍。

【TCP/IP网络编程】:06基于UDP的服务器端/客户端的更多相关文章

  1. TCP/IP网络编程之基于UDP的服务端/客户端

    理解UDP 在之前学习TCP的过程中,我们还了解了TCP/IP协议栈.在四层TCP/IP模型中,传输层分为TCP和UDP这两种.数据交换过程可以分为通过TCP套接字完成的TCP方式和通过UDP套接字完 ...

  2. TCP/IP网络编程之基于TCP的服务端/客户端(二)

    回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...

  3. TCP/IP网络编程之基于TCP的服务端/客户端(一)

    理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流(stream)的套接字.TCP是Transmissi ...

  4. TCP/IP网络编程之套接字的多种可选项

    套接字可选项进而I/O缓冲大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性.但是,理解这些特性并根据实际需要进行更改也十分重要.之前我们写的程序在创建好套接字后都是未经特别操 ...

  5. 《TCP/IP网络编程》

    <TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...

  6. TCP/IP网络编程系列之四(初级)

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

  7. TCP/IP网络编程之多播与广播

    多播 多播方式的数据传输是基于UDP完成的,因此,与UDP服务端/客户端的实现非常接近.区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机.换言之,采用多播方式时 ...

  8. TCP/IP网络编程 读书笔记1

    本篇主干内容是TCP/IP网络编程1-9章学习笔记 1. linux文件描述符 描述符从3开始以由小到大的顺序编号,0,1,2,分配给标准I/O用作标准输入.标准输出和标准错误. 2. 协议族与套接字 ...

  9. TCP/IP网络编程系列之三(初级)

    TCP/IP网络编程系列之三-地址族与数据序列 分配给套接字的IP地址和端口 IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值.端口号并非赋予计算机值, ...

随机推荐

  1. Java-POJ1010-STAMP

    说良心话,题目不难,但是题目真的很不好懂,解读一下吧 题意: 读入分两行,第一行为邮票面额(面额相同也视为种类不同)以0结束,第二行为顾客要求的面额,以0结束 要求:每个顾客最多拿4张邮票,并求最优解 ...

  2. AcWing 826. 单链表

    https://www.acwing.com/activity/content/problem/content/863/1/ #include <iostream> using names ...

  3. 图的最小生成树prim算法模板

    用prim算法构建最小生成树适合顶点数据较少而边较多的图(稠密图) prim算法生成连通图的最小生成树模板伪代码: G为图,一般为全局变量,数组d为顶点与集合s的最短距离 Prim(G, d[]){ ...

  4. 关于BaiduPSC-Go的一些bug的更正

    首先说下操作步骤 下载是在GutHub,这个不赘述,网上很多资料 下载之后配置环境变量,在path的后面加上一个分号,然后加上你下载的目录,目录名最好为英文 然后通过命令行CMD工具,输入BaiduP ...

  5. js的一些基础

    事件对象: 就是用来存储事件相关的信息 事件对象存储信息有: 事件的类别,如:click,keydown等等 点击事件的位置 点击的哪一个键 等等 用于阻止事件流,用于阻止浏览器默认动作(表单提交.a ...

  6. linux下删除空行的几种方法

    在查看linux下的配置文件时,为了便于一目了然的查看,经常会删除空行和#头的行.而linux在删除空行的方法很多,grep.sed.awk.tr等工具都能实现.现总结如下: 1.grep grep ...

  7. JavaScript.Array.some() 方法用法

    定义和用法:some() 方法用于检测数组中的元素是否满足指定条件(函数提供). some() 方法会依次执行数组的每个元素: 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检 ...

  8. MinGW dll导入导出类

    dll不仅可以导入导出函数,还可以导入导出类.这篇文章就来介绍如何将类导入dll中并导出. 首先我们建立一个名为dll.cpp的文件(又是这种破名字),里面写上: #include <iostr ...

  9. linux 压测jmeter24h稳定性测试

    环境准备: 安装jmeter,JDK: wget  https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.1.tgz cd  ...

  10. 可以使用的一些API(转存)

    聚合数据 juhe.com 转存的格式不如原文的好看,可以直接访问原文 https://www.jianshu.com/p/9a0acf69b789 api接口应该会越来越火,上个全的,楼主自己找找吧 ...