2024-02-29-Linux高级网络编程(2-UDP编程)
2. UDP编程
2.1 字节序概述
字节序是指多字节数据的存储顺序
分类:
- 小端格式:将低位字节数据存储在低地址(LSB)
- 大端格式:将高位字节数据存储在低地址

2.1.1 如何判断当前系统的字节序
#include <stdio.h> union un { /* data */ int a; char b; }; int main(int argc, char const *argv[]) { union un myun; myun.a = 0x12345678; printf("a = %#x\n", myun.a); printf("b = %#x\n", myun.b); if (myun.b == 0x78) { printf("小端对齐\n"); } else { printf("大端对齐\n"); } return 0; }
输出结果
a = 0x12345678 b = 0x78 小端对齐
2.1.2 字节序转换函数
特点
- 网络协议指定了通讯字节序大端
- 只有在多字节数据处理时才需要考虑字节序
- 运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
- 异构计算机之间通讯,需要转换自己的字节序为网络字节序
在需要字节序转换的时候一般调用特定字节序转换函数
host -> network 1. htonl #include <arpa/inet.h> uint32_t htonl(uint32_t hostint32) 功能:将32位主机字节序数据转换成网络字节序数据 参数: hostint32: 待转换的32位主机字节序数据; 返回值: 成功:返回网络字节序的值 2. htons #include <arpa/inet.h> uint16_t htons(uint16_t hostint16); 功能: 将16位主机字节序数据转换成网络字节序数据; 参数: uint16_t: unsigned short int hostint16: 待转换的16位主机字节序数据; 返回值: 成功: 返回网络字节序的值
networl -> host 3. ntohl #include <arpa/inet.h> uint32_t ntohl(uint32_t netint32); 功能: 将32位网络字节序数据转换成主机字节序数据 参数: uint32_t: unsigned int netint32: 待转换的32位网络字节序数据; 返回值: 成功: 返回主机字节序的值 4. ntohs uint16_t ntohs(uint16_t netint16); 功能: 将16位网络字节序数据转换成主机字节序数据; 参数: uint16_t: unsigned short int netint16:待转换的16位网络字节序数据 返回值: 成功:返回主机字节序的值
#include <stdio.h> #include <arpa/inet.h> int main(int argc, char const *argv[]) { int a = 0x12345678; short b = 0x1234; // ubuntu中以小端存储,转换后变成大端存储,以字节进行操作 printf("%#x\n", htonl(a)); printf("%#x\n", htons(b)); return 0; }
输出结果
0x78563412 0x3412
2.1.3 地址转换函数 - inet_pton、inet_ntop
人为识别的ip地址是点分十进制的字符串形式,但是计算机网络中识别的ip地址是整型数据。
// 字符串ip地址转换型数据 #include <arpa/inet.p> int inet_pton(int family,const char *strptr,void *addrptr) 功能:将点分十进制数串转换成 32 位无符号整数 参数: family: 协议族 strptr: 点分十进制数串 addrptr: 32 位无符号整数的地址; 返回值: 成功: 1; 失败: 非1
#include <stdio.h> #include <arpa/inet.h> int main() { char ip_str[] = "192.168.3.103"; unsigned int ip_int = 0; unsigned char *ip_p = NULL; // 将点分十进制ip地址转化为32位无符号整形数据 inet_pton(AF_INET, ip_str, &ip_int); printf("ip int = %d\n", ip_int); ip_p = (char *)&ip_int; printf("in_uint = %d,%d,%d,%d\n", *ip_p, *(ip_p + 1), *(ip_p + 2), *(ip_p + 3)); return 0; }
输出结果
ip int = 1728293056 in_uint = 192,168,3,103
// 整型数据转字符串格式ip 地址 const char *inet_ntop(int family, const void *addrptr,char *strptr,size_t len) 功能: 将 32 位无符号整数转换成点分十进制数串; 参数: family:协议族 :AF_INET // ipv4 addrptr:32 位无符号整数 strptr:点分十进制数串 len :strptr 缓存区长度 len的宏定义 : #define INET_ADDRSTRLEN 16 // ipv4 #define INET_ADDRSTRLEN 46 // ipv6 返回值: 成功:返回字符串首地址 失败:返回NULL
#include <stdio.h> #include <arpa/inet.h> int main() { unsigned char ip_int[] = {192, 168, 3, 103}; char ip_str[16] = ""; inet_ntop(AF_INET, &ip_int, ip_str, 16); printf("ip_s =%s\n", ip_str); return 0; }
输出结果
ip_s =192.168.3.103
inet_addr()和inet_ntoa()
这两个函数只能用在ipv4地址的转换,且用的比较多
#include<sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> in_addr_t inet_addr(const char *cp); 功能:将点分十进制ip地址转化为整形数据; 参数: cp:点分十进制的IP地址 返回值: 成功:整形数据; char *inet_ntoa(struct in_addr in); 功能:将整形数据转化为点分十进制的ip地址; 参数: in:保存ip地址的结构体; 返回值: 成功: 点分十进制的IP地址
2.2 UDP介绍、编程流程
2.2.1 UDP介绍
UDP 协议
面向无连接的用户数据报协议,在传输数据前不需要先建立连接:目地主机的运输层收到UDP 报文后,不需要给出任何确认
UDP 特点
- 相比 TCP 速度稍快些
- 简单的请求/应答应用程序可以使用 UDP
- 对于海量数据传输不应该使用 UDP
- 广播和多播应用必须使用 UDP
UDP 应用
DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
2.2.2 网络编程接口 socket
网络通信要解决的是不同主机进程间的通信
- 首要问题是网络间进程标识问题
- 多重协议的识别问题
20 世纪 80 年代初,加州大学 Berkeley 分校在 BSD(一个 UNIX OS 版本)系统内实现了 TCP/IP 协议,其网络程序编程开发接口为 socket。
随着 UNIX 以及类 UNIX 操作系统的广泛应用, socket 成为最流行的网络程序开发接口。
socket作用:提供不同主机之间的通信
socket特点:
- socket 也称“套接字”
- 是一种文件描述符,代表了一个通信管道的一个端点
- 类似对文件的操作一样,可以使用 read、write、close 等函数对 socket套接字进行网络数据的收取和发送等操作
- 得到 socket 套接字(描述符)的方法调用 socket()
socket分类:
- SOCK_STREAM,流式套接字,用于TCP
- SOCK_DGRAM,数据报套接字,用于UDP
- SOCK_RAW,原始套接字,对于其他层次的协议操作时需要使用这个类型
- ....
2.2.3 UDP编程C/S架构
服务器:
1. 创建套接字 socket(); 2. 将服务器的ip地址、端口号与套接字进行绑定 bind(); 3. 接受数据 recvfrom(); 4. 发送数据 sendto()
客户端
1. 创建套接字 socket(); 2. 发送数据 sendto(); 3. 接收数据 recvfrom(); 4. 关闭套接字 close();
2.3 UDP编程-创建套接字
2.3.1 创建套接字socket
#include <sys/socket.h> int socket(int family int type,int protocol); 功能:创建一个用于网络通信的socket套接字; 参数: family: 协议族(AF_INET,AF_INET6,PF_PACKET等); type: 套接字类(SOCK_STREAM,SOCK_DGRAM,SOCK_RAW等); protocol:协议类别(0,IPPROTO_TCP,IPPROTO_UDP等) 返回值: 套接字 特点: 创建套接字时,系统不会分配端口; 创建的套接字默认属性是主动的,即主动发起服务的请求; 当作为服务器时,往往需要修改为被动的
2.3.2 案例
#include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <stdlib.h> int main(int argc, char const *argv[]) { int sockfd; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("FAIL TO SOCKET"); exit(1); } printf("socketfd = %d\n", sockfd); return 0; }
输出结果
socketfd = 3
2.4 UDP编程-发送、绑定、接收数据
2.4.1 IPV4套接字地址结构
在网络编程中经常使用的结构体sockaddr_in
#include <netinet/in.h> struct in_addr{ in_addr_t s_addr;//4字节 } struct sockaddr_in{ sa_family_t sin_family;//协议族,2字节 in_port_t sin_port;//端口号,2字节 struct in_addr sin_addr;//ip地址,4字节 char sin_zero[8]//填充,8字节 }
通用结构体sockaddr
struct sockaddr{ sa_family_t sa_family; // 2字节 char sa_data[14]; // 14字节 }
为了使不同格式地址能被传入套接字函数,地址须要强制转换成通用套接字地址结构,原因是因为不同场合所使用的结构体不一样。但是调用的函数却是同一个,所以定义一个通用结构体,当在指定场合使用时,在根据要求传入指定的结构体即可。
2.4.2 发送数据 - sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);. 功能: 向 to 结构体指针中指定的 ip,发送 UDP 数据; 参数: sockfd: 套接字 buf: 发送数据缓冲区 nbytes: 发送数据缓冲区的大小 flags: 一般为 0阻塞,MSG_DONT_WAIT非阻塞 to: 指向目的主机地址结构体的指针; addrlen: dest_addr的长度; 注意: 通过 to 和 addren 确定目的地址; 可以发送0长度的 UDP 数据包; 返回值: 成功:返回实际发送的字节数 失败:返回-1
2.4.3 绑定函数 - bind函数
UDP网络程序想要收取数据需什么条件?
确定的ip地址,确定的port
怎样完成上面的条件呢?
接收端使用bind
函数,来完成地址结构与socket
套接字的绑定,这样ip、port
就固定了发送端在sendto
函数中指定接收端的ip、port
,就可以发送数据了 。
#include<sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 功能:将套接字与网络信息结构体绑定; 参数: sockfd:文件描述符,socket的返回值; addr:网络信息结构体 通用结构体(一般不用)struct sockaddr 网络信息结构体 sockaddr_in #include <netinet/in.h> struct sockaddr_in addrlen: addr的长度 返回值: 成功:0 失败:-1
2.4.4 接收数据
#include <sys/types.h> #include<sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr,socklen_t *addrlen); 功能:接收数据 参数: sockfd:文件描述符,socket的返回值 buf: 保存接收的数据 len: buf的长度 flags: 标志位 0 阻塞 MSG_DONTWAIT 非阻塞 src_addr: 源的网络信息结构体(自动填充,定义变量传参即可) addrlen: src_addr的长度 返回值: 成功:0 失败:-1
2.5 UDP编程-Client、Server

2.5.1 UDP客户端注意点
1、本地IP、本地端口(我是谁)
2、目的IP、目的端口(发给谁)
3、在客户端的代码中,只设置了目的IP、目的端口
客户端的本地ip、本地port是我们调用sendto
的时候linux系统底层自动给客户端分配的;分配端口的方式为随机分配,即每次运行系统给的port
不一样
2.5.2 UDP服务器注意点
- 服务器之所以要 bind 是因为它的本地 port 需要是固定,而不是随机的
- 服务器也可以主动地给客户端发送数据
- 客户端也可以用 bind,这样客户端的本地端口就是固定的了,但一般不这样做
client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define BUF_SIZE 1024 #define SERVER_IP "127.0.0.1" #define SERVER_PORT 12345 int main() { int sockfd; struct sockaddr_in server_addr; socklen_t server_addr_len; char buffer[BUF_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket"); exit(1); } // 设置服务器地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(SERVER_PORT); server_addr_len = sizeof(server_addr); while (1) { printf("Enter a message to send (or 'q' to quit): "); fgets(buffer, BUF_SIZE, stdin); // 发送数据 if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, server_addr_len) == -1) { perror("sendto"); exit(1); } // 退出条件 if (buffer[0] == 'q' && (buffer[1] == '\n' || buffer[1] == '\0')) { break; } // 接收回复 int recv_len = recvfrom(sockfd, buffer, BUF_SIZE, 0, NULL, NULL); if (recv_len == -1) { perror("recvfrom"); exit(1); } // 打印回复 buffer[recv_len] = '\0'; printf("Received reply from server: %s\n", buffer); } // 关闭套接字 close(sockfd); return 0; }
server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define BUF_SIZE 1024 #define PORT 12345 int main() { int sockfd; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len; char buffer[BUF_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket"); exit(1); } // 设置服务器地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); // 绑定套接字到服务器地址 if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind"); exit(1); } printf("UDP server is running and listening on port %d...\n", PORT); while (1) { // 接收数据 client_addr_len = sizeof(client_addr); int recv_len = recvfrom(sockfd, buffer, BUF_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len); if (recv_len == -1) { perror("recvfrom"); exit(1); } // 打印接收到的数据 buffer[recv_len] = '\0'; printf("Received message from client: %s\n", buffer); // 发送回复 if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, client_addr_len) == -1) { perror("sendto"); exit(1); } printf("Sent reply to client: %s\n", buffer); } // 关闭套接字 close(sockfd); return 0; }
本文来自博客园,作者:Yasuo_Hasaki,转载请注明原文链接:https://www.cnblogs.com/hasaki-yasuo/p/18042960
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步