UDP发送端
1、创建套接字
socket函数:
是用于创建网络套接字(socket)的系统调用,它是进行网络通信的基础。通过 socket()
函数,程序可以创建一个网络套接字,用于后续的数据发送和接收操作。这个函数通常用于客户端和服务器程序中,在数据交换前需要先创建一个套接字。
int socket(int domain, int type, int protocol);
参数说明:
domain(协议族):
用于指定套接字使用的协议族(Address Family),它决定了套接字支持哪种网络通信协议。常用的协议族有:
AF_INET
:IPv4 地址族(用于 Internet 网络)。AF_INET6
:IPv6 地址族(用于 Internet 网络,支持更大的地址空间)。AF_UNIX
或AF_LOCAL
:用于本地通信(同一台机器上的进程之间通信)。AF_PACKET
:直接操作链路层数据(通常用于原始套接字)。
type(套接字类型):
套接字的类型决定了数据传输的方式和协议。常见的套接字类型有:
SOCK_STREAM
:流式套接字,基于 TCP 协议,提供面向连接、可靠、双向的字节流通信。SOCK_DGRAM
:数据报套接字,基于 UDP 协议,提供无连接、不可靠、面向消息的通信。SOCK_RAW
:原始套接字,允许直接访问网络协议栈的低层数据,通常用于自定义协议或者网络分析。
protocol(协议类型):
用于指定协议。在大多数情况下,如果选择了
SOCK_STREAM
或SOCK_DGRAM
,protocol
可以设置为0
,系统会自动选择合适的协议(如 TCP 或 UDP)。常用的协议有:IPPROTO_TCP
:TCP 协议(用于SOCK_STREAM
)。IPPROTO_UDP
:UDP 协议(用于SOCK_DGRAM
)。IPPROTO_IP
:IP 协议(用于原始套接字)。
返回值:
如果成功,
socket()
返回一个 非负整数,这个整数值代表新创建的套接字的文件描述符。如果失败,返回 -1,并且设置
errno
变量来说明错误的原因。
2.定义和初始化目标地址结构体
使用
struct sockaddr_in
来定义接收端的 IP 地址和端口号。通过
inet_pton()
函数将接收端的 IPv4 地址字符串(点分十进制)转换为二进制形式并存储在地址结构体中。使用
htons()
函数将端口号转换为网络字节序(大端字节序)。
struct sockaddr_in
结构体
struct sockaddr_in dest_addr;
socklen_t socklen = sizeof(struct sockaddr_in);
memset(&host_addr, 0, socklen); // 清零结构体
dest_addr.sin_family = AF_INET; // 设置协议族为 IPv4
dest_addr.sin_port = htons(DEST_PORT); // 转换端口号为网络字节序
inet_pton(AF_INET, DEST_IP_ADDR, &(dest_addr.sin_addr.s_addr)); // 转换 IP 地址为网络字节序
inet_pton()函数
int inet_pton(int af, const char *src, void *dst);
函数功能:
inet_pton()
是一个用于将文本格式的 IP 地址(如 "192.168.1.1"
)转换为二进制网络地址的函数。它是 网络编程 中非常常用的函数,特别是在使用 socket
编程时,通常需要将 IP 地址字符串转换为可以在网络上传输的二进制格式。
参数解释:
af
(Address Family 地址族):该参数指定了 IP 地址的格式,也就是使用哪种地址族。
AF_INET
:表示 IPv4 地址族。对应的 IP 地址应该是标准的点分十进制格式(如192.168.1.1
)。AF_INET6
:表示 IPv6 地址族。对应的 IP 地址应该是标准的 IPv6 地址格式(如2001:0db8:85a3:0000:0000:8a2e:0370:7334
)。
该参数用于告诉函数要转换的 IP 地址是 IPv4 还是 IPv6。
src
(源 IP 地址字符串):这是一个指向字符串的指针,字符串表示要转换的 IP 地址。字符串的格式根据
af
参数的值决定:对于
AF_INET
,该字符串应该是一个 IPv4 地址,形式如"192.168.1.1"
。对于
AF_INET6
,该字符串应该是一个 IPv6 地址,形式如"2001:0db8::ff00:42:8329"
。
dst
(目标二进制地址缓冲区):该参数是一个指向内存的指针,用于存储转换后的二进制地址。
对于
AF_INET
,该地址应该是一个大小为 4 字节的缓冲区,用于存储转换后的 IPv4 地址。对于
AF_INET6
,该地址应该是一个大小为 16 字节的缓冲区,用于存储转换后的 IPv6 地址。
返回值:
如果转换成功,返回 1。
如果遇到错误(例如,输入的 IP 地址格式无效),返回 0。
如果输入的地址族不匹配(如给定的地址字符串不是该地址族支持的格式),则返回 -1,并设置
errno
以提供错误信息。
htons()函数
unsigned short htons(unsigned short hostshort);
功能:将 主机字节序(Host Byte Order)转换成 网络字节序
why?在计算机系统中,数据的存储顺序有不同的字节序方式(字节顺序)。大端字节序(Big-endian)和小端字节序(Little-endian)是常见的两种字节序。网络字节序采用的是 大端字节序,也就是说,数据的高位字节存储在低地址位置,低位字节存储在高地址位置。为了保证不同计算机系统之间的数据能够正确传输和解析,我们需要在不同字节序的系统间进行转换。
参数解释:
hostshort
:这是一个 16 位的短整型数,通常用于表示端口号等数据。在调用htons()
函数时,传入的hostshort
参数是主机字节序下的端口号。主机字节序:指的是本地计算机的字节序。
网络字节序:指的是网络上传输数据时所使用的字节序,通常是大端字节序。
返回值:
htons()
函数会返回一个 16 位无符号短整型数,该数值已经被转换成网络字节序。如果主机字节序是大端字节序,
htons()
将直接返回原值。如果主机字节序是小端字节序,
htons()
会将字节顺序进行转换,确保结果是大端字节序。
3.准备发送的数据内容:
定义一个字符串(如 "Hello UDP Receiver!"
)作为发送的数据。你可以根据需要修改发送内容。
4.使用 sendto()
函数发送数据:
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数解释:
1.sockfd
(套接字文件描述符):
这是通过
socket()
函数创建的套接字文件描述符,标识当前用于通信的 UDP 套接字。这个套接字用于指定发送的数据流。类型:
int
2.buf
(发送缓冲区):
这是一个指向要发送的数据的指针,数据的内容将通过这个缓冲区发送到目标地址。
类型:
const void *
3.len
(发送数据的长度):
这是要发送的字节数。通常,长度是
buf
中数据的长度,单位是字节。类型:
size_t
4.flags
(控制标志):
这是控制发送的行为的标志位。通常在 UDP 中,设置为
0
即可,但也可以设置一些特殊标志,常见的有:MSG_CONFIRM
:请求确认。MSG_DONTROUTE
:不通过路由表发送数据。MSG_NOSIGNAL
:发送时不会产生SIGPIPE
信号(当发送到已关闭的套接字时)。
类型:
int
5.dest_addr
(目标地址):
这是指向
struct sockaddr
类型的指针,包含目标地址(即接收方的 IP 地址和端口号)。UDP 协议需要明确目标地址,以便发送数据包到正确的位置。类型:
const struct sockaddr *
6.addrlen
(目标地址结构的长度):
这是目标地址结构体的长度(单位:字节)。对于
struct sockaddr_in
,长度通常是sizeof(struct sockaddr_in)
。类型:
socklen_t
返回值:
成功:
sendto()
返回成功发送的字节数。失败:返回
-1
,并设置errno
来表示错误。
UDP接收端
接收端同发送端一样第一步申请UDP支持socket
第二步 准备本机用于接收数据的地址结构,和发送端一样使用struct sockaddr_in
结构体来设置本地的地址和端口号。由于 UDP 是无连接的协议,我们只需要绑定本地的地址即可。
第三步:使用bind()函数绑定本地地址到套接字
通过调用 bind()
函数,将本地地址(host_addr
)与套接字 sock_fd
进行绑定。绑定操作是确保接收端能够正确地接收到目标端口的数据。
bind()函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()
是一个用于将本地的 IP 地址 和 端口号 绑定到一个 套接字(socket)上的函数,通常在 UDP 和 TCP 网络编程中使用。它是 UDP 套接字、TCP 套接字以及 原始套接字 必须进行的操作之一。绑定操作使得一个套接字与本地的某个地址(IP 地址和端口号)关联起来,使得程序能够接收该地址上的数据。
参数解释:
sockfd
(套接字文件描述符):这是之前通过
socket()
函数创建的套接字文件描述符,标识你要绑定的套接字。类型:
int
该参数用于指定要与本地地址绑定的套接字。
addr
(本地地址):这是指向一个
struct sockaddr
结构体的指针,结构体中包含了要绑定的本地地址(包括本地 IP 地址和端口号)。类型:
const struct sockaddr *
由于
bind()
是一个通用函数,struct sockaddr
结构体需要具体化为协议相关的结构体。对于 IPv4 地址,通常使用struct sockaddr_in
(用于存储 IPv4 地址和端口),对于 IPv6 地址,使用struct sockaddr_in6
(用于存储 IPv6 地址和端口)。
addrlen
(地址结构的长度):这是结构体addr
的大小,通常是使用sizeof()
操作符来获取地址结构体的大小。对于struct sockaddr_in
,它的大小通常是sizeof(struct sockaddr_in)
,对于 IPv6 地址,它通常是sizeof(struct sockaddr_in6)
。 类型:socklen_t
它告诉bind()
函数该结构体有多大,这样函数才知道应该读取多大内存来解析地址信息。
成功返回0,失败返回-1。
第四步:准备接收数据的缓冲区和本机用于存储发送端 IP 地址数据结构体
通常定义一个256位的buffer
char buffer[BUFFER_SIZE] = "";
struct sockaddr_in src_addr;
memset(&src_addr, 0, socklen);
第五步:接收数据
使用 recvfrom()
函数来接收数据。recvfrom()
会从套接字中读取数据,并将数据存储到指定的缓冲区,同时也会提供发送端的地址信息。
recvfrom()
是一个用于 接收数据 的函数,常用于 UDP 和 原始套接字 的编程中,允许接收数据并获取 发送端的地址信息。与 recv()
函数不同,recvfrom()
不仅接收数据,还返回发送端的地址(如 IP 地址和端口号)。这对于 UDP 或其他无连接协议尤其重要,因为数据的发送者和接收者在通信前没有建立连接。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数解释:
sockfd
(套接字文件描述符):这是由
socket()
函数返回的套接字文件描述符,表示接收数据的套接字。类型:
int
在调用
recvfrom()
时,指定要接收数据的套接字。
buf
(数据缓冲区):这是一个指向内存的指针,接收到的数据将被存储在该缓冲区中。
类型:
void *
需要确保这个缓冲区足够大,能够容纳接收到的数据。通常,它是一个字符数组。
len
(缓冲区大小):这是缓冲区
buf
的大小,即最大可以接收的字节数。这个大小应该与buf
的实际大小匹配。类型:
size_t
传递的
len
参数应小于或等于缓冲区的实际大小。
flags
(标志位):这是一个整数,用于控制接收行为。通常设置为
0
,表示默认行为,但也可以使用一些标志来调整数据接收方式:MSG_OOB
:接收带外数据(Out-of-Band Data)。MSG_PEEK
:窥视数据,不将数据从缓冲区中移除。MSG_WAITALL
:接收所有数据,直到数据完整。MSG_DONTWAIT
:非阻塞模式,接收操作不会阻塞。
类型:
int
src_addr
(发送端地址):这是一个指向
struct sockaddr
结构体的指针,用于存储发送端的地址信息(例如 IP 地址和端口号)。类型:
struct sockaddr * 通常需要强转
(struct sockaddr *)&src_addr,在接收到数据后,该结构体将包含发送端的 IP 地址、端口号等信息。
addrlen
(地址结构的长度):这是
src_addr
结构体的大小,通常是sizeof(struct sockaddr_in)
,用于告诉函数该地址结构的大小。调用时,recvfrom()
会修改该参数来表示实际地址的长度。类型:
socklen_t *
成功返回接收的数据字节数,失败返回-1
第六步,处理数据
一旦 recvfrom()
成功接收数据,接收端通常会对数据进行处理。接收的数据包含发送端的 IP 地址和端口号,这可以通过 src_addr
结构体访问。同时,还可以将接收到的 IP 地址转换为字符串格式并打印。
代码:
// 用于临时存储数据发送端 IP 地址字符串
char src_ip_addr[16] = "";
if (count > 0)
{
printf("Data from %s:%u, Data : %s\n",
// 网络 4 字节 IP 地址数据,转换为 IPv4 本地点分十进制 IP 地址字符串
inet_ntop(AF_INET, &(src_addr.sin_addr.s_addr), src_ip_addr, 16),
// 网络 2 字节 Port 端口号数据转本地
ntohs(src_addr.sin_port),
// 收到的数据内容
buffer);
}
inet_ntop()
:将接收到的发送端的 IP 地址(网络字节序)转换为点分十进制的字符串格式。ntohs()
:将接收到的端口号(网络字节序)转换为主机字节序。buffer
:打印接收到的数据。
无论发送端还是接收端,最后都要关闭资源 close(sock_id);
代码案例:sender:
#include
#include
#include
#include
#include
#include
#include
//接收端ipv4点分十进制地址
#define DEST_IP_ADDR "192.168.16.58"
//接收端端口号
#define DEST_PORT (8848)
int main(int argc, char const *argv[])
{
//sock 套接字
int sock_id = socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
if (-1 == sock_id)
{
perror("socket failed!");
exit(1);
}
//明确发送内容
char *str = "你好,我是嵌入式高手!";
//准备目标接收端IP地址结构体数据,使用结构体 struct sockaddr_in
struct sockaddr_in dest_addr;
socklen_t socklen = sizeof(struct sockaddr_in);
memset(&dest_addr,0,socklen);
//sin_family 明确当前使用的IP协议
dest_addr.sin_family = AF_INET;
//端口号明确为8848,提供大端字节序
dest_addr.sin_port = htons(DEST_PORT);
//本地点分十进制IP地址字符串,转换为网络地址
inet_pton(AF_INET,DEST_IP_ADDR,&(dest_addr.sin_addr));
//发送
ssize_t ret = sendto(sock_id,str,strlen(str),0,(struct sockaddr *)&dest_addr,socklen);
if (-1 == ret)
{
perror("sendto failed!");
close(sock_id);
exit(1);
}
close(sock_id);
return 0;
}
reveive:
#include
#include
#include
#include
#include
#include
#include
//本机ipv4点分十进制地址
#define HOST_IP_ADDR "192.168.16.58"
//接收端端口号
#define HOST_PORT (8848)
//buff大小
#define BUFFER_SIZE (256)
int main(int argc, char const *argv[])
{
//申请socket
int sock_id = socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
// 2.【准备本机用于接收数据 IP 和 端口号 结构体】
struct sockaddr_in host_addr;
socklen_t socklen = sizeof(struct sockaddr_in);
memset(&host_addr,0,socklen);
host_addr.sin_family = AF_INET;
host_addr.sin_port = htons(HOST_PORT);
inet_pton(AF_INET,HOST_IP_ADDR,&(host_addr.sin_addr));
int ret = bind(sock_id,(const struct sockaddr *)&host_addr,socklen);
if (ret)
{
perror("bind failed!");
close(sock_id);
exit(1);
}
char buffer[BUFFER_SIZE] = " ";
//本机用于存储发送端IP地址数据结构体
struct sockaddr_in src_addr;
memset(&src_addr,0,socklen);
//接收数据
ssize_t count = recvfrom(
sock_id,
buffer,
BUFFER_SIZE,
0,
(struct sockaddr *)&src_addr,
&socklen);
//临时存放数据发送端IP 地址字符串
char src_ip_addr[16] = "";
if (count > 0)
{
printf("数据发送端:%s:%u,内容:%s\n",inet_ntop(AF_INET,&(src_addr.sin_addr.s_addr),src_ip_addr,16),
ntohs(src_addr.sin_port),
buffer);
}
close(sock_id);
return 0;
}