在计算机网络中,UDP(User Datagram Protocol)是一种重要的传输层协议,与TCP(Transmission Control Protocol)一样位于 OSI 模型的传输层。但与TCP不同,UDP提供了一种无连接、轻量级的数据传输方式,适用于需要快速传输数据的应用场景。本文将深入探讨UDP协议的特点、用途以及与TCP的对比。
无连接性:UDP不需要在通信前建立连接,也不维护连接状态,因此它更加轻量级。这使得UDP适用于实时数据传输,如音频和视频流。
不可靠性:UDP不提供数据包的可靠性传输。这意味着数据包可能会丢失、重复或无序到达目标。因此,UDP通常用于那些可以容忍一些数据丢失的应用,如实时多媒体流。
高性能:由于不需要建立连接和维护状态信息,UDP的开销较低,具有较高的性能。这使得它成为一种适用于高吞吐量、低延迟的协议。
头部小:UDP的头部相对较小,只包含源端口、目标端口、长度和校验和等字段,这有助于减少数据传输时的开销。
多播和广播:UDP支持多播和广播,可以同时向多个接收方发送数据包,适用于一对多或多对多通信。
UDP在许多应用中发挥着重要作用,包括但不限于:
实时音视频传输:VoIP电话、视频会议和直播流都使用UDP来传输实时音视频数据,因为它具有低延迟和快速传输的特性。
DNS查询:域名系统(DNS)使用UDP来快速查询域名解析。
游戏数据传输:在线游戏经常使用UDP来传输游戏数据,以确保低延迟和实时性。
网络广播:UDP用于发送广播消息,例如局域网内的设备发现和服务广告。
在此之前我们已经学习掌握了TCP/IP实现服务器与客户端通信,链接我放这里:
【网络编程】TCP传输控制协议(Transmission Control Protocol)_祐言QAQ的博客-CSDN博客
那么现在让我们一起来学习一下UDP如何实现通信。
不难看出其中与TCP通信有几个不同点:
TCP使用 SOCK_STREAM 表示流式套接字,这意味着它提供面向连接的、可靠的、基于字节流的通信;
UDP使用 SOCK_DGRAM 表示数据报套接字,这表明它提供无连接的、不可靠的、基于数据报的通信。
在TCP中,通信需要经过 listen、accept 和 connect 过程,其中建立连接是必要的;
在UDP中,通信是无连接的,不需要建立连接,因此不需要进行 listen、accept 和 connect 步骤。
在TCP中,通常使用 recv 和 send 函数来进行数据的接收和发送;
在UDP中,通常使用 recvfrom 和 sendto 函数来进行数据的接收和发送。这些函数需要指定目标地址,因为UDP是无连接的,每个数据包都需要包含目标地址信息。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 定义一个结构体用于存储客户端信息
struct Client
{
struct sockaddr_in addr; // 客户端地址结构体
int ser_socket; // 服务器套接字
};
// 线程函数,用于发送数据
void *send_data(void *arg)
{
struct Client *cli = (struct Client *)arg;
char buf[1024];
while (1)
{
scanf("%[^\n]", buf); // 从用户输入读取数据
while (getchar() != '\n');
// 发送数据到客户端
sendto(cli->ser_socket, buf, strlen(buf), 0, (struct sockaddr *)&(cli->addr), sizeof(cli->addr));
}
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("./server \n");
return -1;
}
// 创建套接字socket
int ser_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (ser_socket == -1)
{
perror("socket");
return -1;
}
// 设置套接字属性,SO_REUSEADDR 允许地址端口重用
int on = 1;
if (setsockopt(ser_socket, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
{
perror("setsockopt");
return -1;
}
// 初始化地址结构体
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 地址簇
addr.sin_port = atoi(argv[1]); // 端口(一般以传参的传进来)
// addr.sin_addr.s_addr = inet_addr("192.168.1.128"); // IP地址
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 用特殊的"0.0.0.0"这个IP来绑定本机IP地址
// bind 绑定IP跟PORT
int b = bind(ser_socket, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
if(b == -1)
{
perror("bind");
return -1;
}
printf("绑定成功\n");
struct sockaddr_in c_addr; // IPV4地址结构体
int addrlen = sizeof(c_addr);
char buf[1024];
// 接收客户端的第一条消息
recvfrom(ser_socket, buf, sizeof(buf), 0, (struct sockaddr *)&c_addr, &addrlen);
printf("[%s] [%d]:%s\n", inet_ntoa(c_addr.sin_addr), c_addr.sin_port, buf);
struct Client cli;
cli.addr = c_addr;
cli.ser_socket = ser_socket;
// 创建一个线程用来发送数据
pthread_t pid;
pthread_create(&pid, NULL, send_data, &cli);
// 接收/发送数据 recvfrom/sendto
while(1)
{
bzero(buf, sizeof(buf));
// 每接收一条客户端发送的信息,保存一次客户端的IP+PORT
recvfrom(ser_socket, buf, sizeof(buf), 0, (struct sockaddr *)&c_addr, &addrlen);
// 第一次接收,创建线程发送数据,将套接字,对方的IP地址传递给线程任务函数
printf("[%s] [%d]:%s\n", inet_ntoa(c_addr.sin_addr), c_addr.sin_port, buf);
}
// 关闭套接字close
close(ser_socket);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 线程函数,用于接收数据
void *recv_data(void *arg)
{
int cli_socket = *(int *)arg;
struct sockaddr_in c_addr;
int addrlen = sizeof(c_addr);
char buf[1024];
while(1)
{
bzero(buf, sizeof(buf));
// 每接收一条客户端发送的信息,保存一次客户端的IP+PORT
recvfrom(cli_socket, buf, sizeof(buf), 0, (struct sockaddr *)&c_addr, &addrlen);
printf("[%s] [%d]:%s\n", inet_ntoa(c_addr.sin_addr), c_addr.sin_port, buf);
}
}
int main(int argc, char const *argv[])
{
if (argc != 3)
{
printf("./client \n");
return -1;
}
// (1) 创建套接字socket
int cli_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (cli_socket == -1)
{
perror("socket");
return -1;
}
// (2) 初始化地址结构体(服务器的)
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 地址簇
addr.sin_port = atoi(argv[2]); // 服务器端的端口(一般以传参的传进来)
addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器端的IP地址(一般以传参的传进来)
// 先发一条上线的消息给server
char buf[1024] = "on line";
sendto(cli_socket, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr));
// 创建线程用来接收数据
pthread_t pid;
pthread_create(&pid, NULL, recv_data, &cli_socket);
// (3) 发送数据
while(1)
{
scanf("%[^\n]", buf); // 从用户输入读取数据
while(getchar()!='\n');
// 发送数据buf
sendto(cli_socket, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr));
}
// (5) 关闭套接字close
close(cli_socket);
return 0;
}
在UDP套接字编程中,你可以使用 getsockopt
和 setsockopt
函数来获取和设置套接字的属性。以下是一些常见的UDP套接字属性以及如何使用这两个函数来处理它们:
获取套接字属性(使用 getsockopt
函数):
SO_RCVBUF 和 SO_SNDBUF:
int buffer_size;
socklen_t optlen = sizeof(buffer_size);
getsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &buffer_size, &optlen);
// 现在 buffer_size 中包含接收缓冲区的大小
SO_RCVTIMEO 和 SO_SNDTIMEO:
struct timeval timeout;
socklen_t optlen = sizeof(timeout);
getsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, &optlen);
// 现在 timeout 中包含接收超时时间
设置套接字属性(使用 setsockopt
函数):
SO_RCVBUF 和 SO_SNDBUF:
int buffer_size = 8192; // 设置缓冲区大小
setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size));
SO_RCVTIMEO 和 SO_SNDTIMEO:
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为5秒
timeout.tv_usec = 0;
setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
这些是一些常见的UDP套接字属性设置选项。可以根据需要使用 getsockopt
和 setsockopt
函数来获取和设置套接字的其他属性,具体选项取决于你的应用程序的需求。套接字属性设置允许你自定义套接字的行为,以满足不同的网络通信需求。在上面的服务端与客户端通信中也有用到一些示例。
UDP和TCP是两种不同的传输协议,它们在以下方面有所不同:
连接性:UDP无连接,TCP面向连接。
可靠性:UDP不提供可靠性传输,TCP提供可靠性传输。
开销:UDP开销较低,TCP开销较高。
适用场景:UDP适用于需要快速传输但可以容忍一些数据丢失的应用,而TCP适用于需要确保数据完整性和可靠性的应用。
UDP协议在网络通信中扮演着重要的角色,尤其是在需要实时性和低延迟的应用中。虽然它不提供可靠性传输,但在正确的应用场景下,UDP是一个强大的工具,能够满足快速数据传输的需求。了解UDP的特点和用途有助于网络工程师更好地选择合适的协议来满足应用程序的需求。
更多C/C++语言、Linux系统、数据结构和ARM板实战相关文章,关注专栏:
手撕C语言
玩转linux
脚踢数据结构
系统、网络编程
探索C++
6818(ARM)开发板实战
一键三连喔
~