22. UDP网络编程
一、什么是UDP协议
相对于 TCP 协议,UDP 协议则是面向无连接的协议。使用 UDP 协议时,不需要建立连接,只需要知道对象的 IP 地址和端口号,就可以直接发数据包。但是,数据无法保证一定到达。虽然用 UDP 传输数据不可靠,但它的优点是比 TCP 协议的速度快。对于不要求可靠到达的数据而言,就可以使用 UDP 协议。
UDP 每次发送数据的时候,都需要写上接收方的 IP 和 PORT;
二、TCP编程的API
2.1、创建套接字
我们可以使用 socket()
函数 创建一个套接字。在 TCP 连接中,客户端和服务端都需要创建对应的套接字。
/**
* @brief 创建一个套接字
*
* @param __domain 指定要创建套接字的通信域
* @param __type 指定要创建的套接字类型
* @param __protocol 指定要与socket一起使用的特定协议
* @return int 成功返回文件描述符,失败返回-1
*/
int socket(int __domain, int __type, int __protocol);
参数 __domain
用来 指定要创建套接字的通信域。这里我们使用 AF_INET
表示 使用 IPv4 互联网协议,如果要 使用 IPv6 协议,我们可以指定为 AF_INET6
。
参数 __type
用来 指定要创建的套接字类型。如果我们要 使用 TCP 协议,可以指定为 SOCK_STREAM
。如果我们要 使用 UDP 协议,可以指定为 SOCK_DGRAM
。
参数 __protocol
用来 指定要与 socket 一起使用的特定协议。如果我们将指定协议设置为 0,会导致 socket() 函数的使用会自动适用于所请求的 socket 类型的未指定的默认协议。
2.2、绑定地址和端口号
创建完套接字之后,客户端和服务端都使用 bind()
函数 绑定地址和端口号。
/**
* @brief 绑定地址和端口号
*
* @param __fd 套接字文件描述符
* @param __addr 指定的地址
* @param __len __addr指向地址结构的大小(以字节为单位)
* @return int 成功返回0,失败返回-1
*/
int bind(int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
当我们使用 socket()
函数创建套接字时,它存在于一个名称空间(地址族)中,但没有为其分配地址。bind()
函数将有 __addr
指定的地址分配给文件描述符 __fd
所一i你用的套接字。__len
指定了 __addr
指向的地址结构的大小(以字节为单位)。
在网络编程中,地址族(Address Family)指定了套接字使用网络协议类型以及地址的格式。简而言之,地址族决定了网络通信的范围和方式。每种地址族都支持特定类型的通信协议和地址格式。
2.3、发送数据
在客户端连接上服务器之后,客户端和服务端都可以使用 sendto()
函数向对方 发送数据。
/**
* @brief 向指定地址发送缓冲区中的数据
*
* @param __fd 套接字文件描述符
* @param __buf 缓冲区指针
* @param __n 要发送的字节数
* @param __flags 通信标志
* @param __addr 目标地址
* @param __addr_len 目标地址长度
* @return ssize_t 发送的消息大小,失败返回-1
*/
ssize_t sendto(int __fd, const void *__buf, size_t __n, int __flags, __CONST_SOCKADDR_ARG __addr, socklen_t __addr_len);
2.4、接收数据
当一端发送数据之后,另一端可以使用 recvfrom()
函数来 接收数据。
/**
* @brief 将接收到数据存入缓冲区中
*
* @param __fd 套接字文件描述符
* @param __buf 缓冲区指针
* @param __n 缓冲区大小
* @param __flags 通信标志
* @param __addr 保存发送方地址信息的结构体指针,可以为NULL
* @param __addr_len 保存发送方地址信息的结构体大小的指针,可以为NULL
* @return ssize_t 实际收到的消息大小,如果失败返回-1
*/
ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n, int __flags, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
2.5、关闭连接
我们可以使用 close()
函数直接 关闭套接字的所有连接。
/**
* @brief 关闭套接字全部连接
*
* @param __fd 套接字文件描述符
* @return int 成功返回0,失败返回-1
*/
int close(int __fd);
2.6、网络字节序和主机字节序的转换
字节序 指的是多字节数据在内存中的存储顺序,主要有两种字节序。
- 大端字节序(Big Endian):高位字节存储在内存的低地址处,低位字节存储在高位地址处。这种字节序也称为 网络字节序。
- 小端字节序(Little Endian):低位字节存储在内存的低地址处,高位字节存储在高地址处。这种字节序也称为 主机字节序。
我们可以使用 htonl()
函数或 htons()
函数 将主机字节序转换为网络字节序。
uint32_t htonl(uint32_t __hostlong);
uint16_t htons(uint16_t __hostshort);
如果我们想要 从网络字节序转换为主机字节序,可以使用 ntohl()
函数或 htohs()
函数。
uint32_t ntohl(uint32_t __netlong);
uint16_t ntohs(uint16_t __netshort));
上面的函数只是将字节转换好了,后面我们还需要手动填入 struct in_addr
结构体中。此时我们可以使用 inet_aton()
函数直接将 IPv4 点分十进制表示法的主机地址的字符串转换为二进制形式(以网络字节序),并将其存储在 __inp
指向的结构体中。该函数转换成功返回 1,失败则返回 0。
int inet_aton(const char *__cp, struct in_addr *__inp);
inet_aton()
函数只能用于 IP 协议,如果我们想要使用其它协议,可以使用 inet_pton()
函数。
/**
* @brief 将字符串转换为 sockadd_in 结构体格式
*
* @param __af 指定协议类型, AF_INET 表示 IPv4 协议,AF_INET6 表示 IPv6 协议
* @param __cp 包含IP地址字符串的字符数组,
* 如果是 IPv4 协议,格式为点分十进制(如 "192.168.1.1"),
* 如果是 IPv6 协议,格式为冒号分隔的16进制数(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
* @param __buf 指向一个缓冲区,
* 如果是 IPv4 协议,该缓冲区应该是一个 struct in_addr 结构体指针,
* 如果是 IPv6 协议,该缓冲区应该是一个 struct in6_addr 结构体指针
* @return int 成功转换返回0,输入地址错误返回1,发生错误返回-1
*/
int inet_pton (int __af, const char *__restrict __cp, void *__restrict __buf);
如果我们想要返过来,可以使用 inet_ntoa()
函数。
char *inet_ntoa (struct in_addr __in);
三、UDP网络编程
3.1、创建UDP服务器
创建 UDP 服务器的伪代码如下:
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define UDP_SERVER_IP INADDR_ANY
#define UDP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0, client_socket_fd = 0;
socklen_t client_address_length = sizeof(client_address);
char data[1024] = {0};
ssize_t length = 0;
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(UDP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(UDP_SERVER_PORT); // 填写服务端的端口号
// 创建socket
server_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 绑定地址
bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address));
while (1)
{
// 服务端接收客户端发送的数据
length = recvfrom(server_socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client_address, &client_address_length );
// 服务端向客户端返回的数据
sendto(server_socket_fd, data, strlen(data), 0, (struct sockaddr *)&client_address, client_address_length );
}
// 关闭服务端
close(server_socket_fd);
return 0;
}
UDP 和 TCP 服务器之间的一个显著差异是,因为数据报套接字是无连接的,所以就没有为了通信成功而使一个客户端连接到一个独立的套接字 “转换” 的操作。这些服务器仅仅接收消息,并有可能回复数据。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#define UDP_SERVER_IP INADDR_ANY
#define UDP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0;
socklen_t client_address_length = sizeof(client_address);
char data[1024] = {0};
ssize_t length = 0;
time_t raw_time = 0;
struct tm *time_info= NULL;
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(UDP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(UDP_SERVER_PORT); // 填写服务端的端口号
// 创建socket
if ((server_socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("create client socket failed.");
exit(EXIT_FAILURE);
}
// 绑定地址
if (bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
{
perror("bind server address failed.");
exit(EXIT_FAILURE);
}
while (1)
{
// 服务端接收客户端发送的数据
length = recvfrom(server_socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client_address, &client_address_length);
// 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
if (length == 0)
{
printf("client (%s:%d) requested to disconnect.\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
break;
}
data[length] = '\0';
printf("receiving the data sent by the client (%s:%d): \n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
printf("%s\n", data);
time(&raw_time);
time_info = localtime(&raw_time);
char date[128] = {0};
sprintf(date,
"(%d-%d-%d %d:%d:%d)\n",
time_info->tm_year + 1900, time_info->tm_mon + 1, time_info->tm_mday,
time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
int date_length = strlen(date);
for (int i = length; i >= 0; i--)
{
data[i + date_length] = data[i];
}
memcpy(data, date, strlen(date));
// 服务端向客户端返回的数据
sendto(server_socket_fd, data, strlen(data), 0, (struct sockaddr *)&client_address, client_address_length);
}
// 关闭服务端
close(server_socket_fd);
return 0;
}
对 socket() 的调用的不同之处仅仅在于,我们现在需要一个 数据报/UDP 套接字类型,但是 bind() 的调用方式与 TCP 服务器版本的相同。因为 UDP 是无连接的,所以这里没有调用 “监听传入的连接”。
3.2、创建UDP客户端
创建 UDP 服务器的伪代码如下:
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define UDP_SERVER_IP INADDR_ANY
#define UDP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0, client_socket_fd = 0;
socklen_t server_address_length = sizeof(server_address);
char data[1024] = {0};
ssize_t length = 0;
// 创建socket
client_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 绑定地址
bind(client_socket_fd, (struct sockaddr *)&client_address, sizeof(client_address));
while (1)
{
// 服务端接收客户端发送的数据
length = recvfrom(client_socket_fd, data, sizeof(data), 0, (struct sockaddr *)&server_address, &server_address_length);
// 服务端向客户端返回的数据
sendto(client_socket_fd, data, strlen(data), 0, (struct sockaddr *)&server_address, server_address_length);
}
// 关闭客户端
close(server_socket_fd);
return 0;
}
UDP 客户端循环工作方式几乎和 TCP 客户端一样。唯一的区别是,事先不需要建立与 UDP 服务器的连接,只是简单的发送一条消息并等待服务器的回复。
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define UDP_SERVER_IP INADDR_ANY
#define UDP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0, client_socket_fd = 0;
socklen_t server_address_length = sizeof(server_address);
char data[1024] = {0};
ssize_t length = 0;
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(UDP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(UDP_SERVER_PORT); // 填写服务端的端口号
// 创建socket
client_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 客户端可以不绑定,系统会自动分配一个空闲的端口给客户端
while (1)
{
memset(data, 0, sizeof(data));
printf("please enter the data to be send.\n");
fgets(data, sizeof(data), stdin);
if (data == NULL || data == "")
{
break;
}
if (data[0] == '\n')
{
continue;
}
// 客户端向服务端发送数据
sendto(client_socket_fd, data, strlen(data), 0, (struct sockaddr *)&server_address, server_address_length);
memset(data, 0, sizeof(data));
// 客户端接收服务端返回的数据
recvfrom(client_socket_fd, data, sizeof(data), 0, NULL, NULL);
printf("received data returned by the server (%s:%d):\n",
inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
printf("%s\n", data);
}
// 关闭客户端
close(client_socket_fd);
return 0;
}
3.3、执行UDP服务器和客户端
如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动的等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说,首先启动服务器(在任何客户端试图连接之前)。
我们在终端中输入 gcc 命令编译文件,生成对应的可执行程序。
gcc udp_server.c -o udp_server
gcc udp_client.c -o udp_client
然后,我们在新建一个终端,在第一个终端中先运行服务端的可执行程序。
./udp_server
然后,我们在第二个终端中先运行客户端的可执行程序。
./udp_client
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
2024-02-20 08. µCOS-Ⅲ的内嵌消息队列和信号量