网络编程之UDP协议
一.网络编程
1. 网络基础知识
1.1 网络的作用?
网络是用于远距离,跨主机的数据通讯。
1.2 网络的体系结构?
网络数据的传输过程往往是比较复杂的,为了数据的完整性,完全性送达到目标主机,
计算机网络将数据传输的过程进行了分段,分层管理,每一层都有该层的功能,同时每一层
都会向下一个分层提供服务,也可以使用上一层提供的服务,这样的分层结构以及每一层
提供的功能协议的集合,称为网络体系结构。
1.3 两类非常重要的网络体系结构模型
1. ISO提供的OSI七层协议模型
物理层
数据链路层
网络层
传输层
会话层
表示层
应用层
2. TCP/IP 四层协议模型
网络接口层
网络层
传输层
应用层
1.4 传输层协议
1) TCP(传输控制协议)
TCP: 面向连接可靠的数据传输服务
面向连接:通讯的双方在数据传输之前,必须先建立网络连接。
把握原则: 网络程序,实际应用中,往往是基于 C / S 或者 B / S 架构构建。
网络连接的建立过程:
三次握手过程:
第一次握手:客户端发送SYN(握手信号)给服务器,客户端进入 SYN_SEND,等待服务器响应
第二次握手:服务端发送SYN(握手信号)+ ACK(响应信号)给客户器,服务端进入 SYN_RECV,
等待客户端连接
第三次握手:客户端发送ACK(响应信号)给客户器给服务器,建立连接,双方均进入ESTABLISHED
状态,等待用户数据的收发。
网络连接的断开过程:
四次挥手过程
第一次挥手:客户端发送FIN(结束信号)给服务器,客户端进入 FIN_WAIT1,等待服务器确认响应
第二次挥手:服务端发送ACK(响应信号)给客户端,客户端进入 CLOSE_WAIT,客户端接收到信号后
结束FIN_WAIT1,进入FIN_WAIT2,
第三次挥手:服务端发送FIN(结束信号)+ACK(响应信号)给客户端,告知客户端进行反方向的数据断开;
第四次挥手:客户端发送ACK(响应信号)给服务器,完成了TCP网络断开。
为什么TCP建立连接需要3次握手,断开需要4次挥手呢?
因为TCP的连接是双向的,每个方向都要单独的进行数据传输的关闭。
2) UDP(用户数据报协议)
UDP: 面向无连接不可靠的数据传输服务
UDP协议的特点:
1. 无连接
2. 资源消耗少,
3. 无连接就不需要系统维护连接状态
4. 实时性高
5. UDP可以实现一对多通讯的。
UDP协议应用场景:
如果在网络应用中对数据的可靠性要求不高,但要求数据传输达到实时传输,挥着
一对多通讯,则应该选择UDP协议。
典型应用: 网络视频,网络电话,局域网数据传输...
2. 网络编程
2.1 相关网络术语:
1) IP (网际协议): 通俗就叫IP地址
IP: 网络中主机的唯一标识。本质是一个无符号的整数
表现形式:
1. 二进制表示的整数
2. 点分十进制字符串
IP组成:
1. 网络ID: 用于标识该主机处于哪一个网络
2. 主机ID: 用于标识同一个网络中的不同主机
IP的分类:
A:
B
C
D: (组播通讯地址)
E: (保留)
特殊IP:
环回/环路 IP: 127.0.0.1 INADDR_LOOPBACK
任一IP: 0.0.0.0 INADDR_ANY
本地广播IP: 255.255.255.255 INADDR_BROADCAST
2) 端口(Port):
端口: 如果说IP是用于说明网络数据应该由哪台主机接收,则端口就是用来说明由一台主机中的
哪一个网络程序来接收数据。
本质: 16位的无符号整数
3) 网络字节序:
网络字节序:主要用于解决不同主机因主机字节序不同,而造成网络数据解析错误的问题,
网络字节序规定,不同主机统一采用大端字节序的数据存储方式来进行网络
数据存储,避免解析错误.
4) 套接字:
套接字是计算机网络系统提供的网络编程的统一接口。
套接字常用类型:
SOCK_STREAM
SOCK_DGRAM
2.2 网络编程:
2.2.1 基于UDP协议的网络编程
1) 基础网络编程(单播 unicast)
服务器端编程流程:
1.创建socket;
socket
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int socket(int domain,int type,int protocol);
函数功能: 创建初始化套接字
函数参数: domain: 协议族/地址族,常用取值:
AF_INET: //IPv4
AF_INET6: //IPv6
type: 套接字类型,常用取值:
SOCK_STREAM: 流式套接字
SOCK_DGRAM: 数据报套接字
protocol:使用的协议,0 让系统决定
函数返回值: 成功返回 套接字描述符
失败返回 -1 错误码存在errno
2.绑定自己的地址信息
bind
头文件: #include <sys/types.h> #include <netinet/in.h>
#include <sys/socket.h> #include <arpa/inet.h>
函数原型: int bind(int sockfd, struct sockaddr* addr, int addrlen);
函数功能: 绑定地址信息到套接字
函数参数:
sockfd: 套接字描述符
addr: 用于指定地址信息的结构体指针,通常使用
strcut sockaddr_in 结构体替代.
addrlen:地址结构体的长度
函数返回值: 成功返回 0;
失败返回-1,错误码放在errno中
衍伸的操作: 字节序转换与IP转换:
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint16_t ntohs(uint16_t hostshort);
uint32_t ntohl(uint32_t hostlong);
uint32_t inet_addr(const char* stringip);
char* inet_ntoa(struct in_addr addr);
3.接收客户端的信息
recvfrom
头文件: #include <sys/types.h>
#include <sys/socket.h>
函数原型: int recvfrom(int sockfd,void* buf,int size,int flags,
struct sockaddr* fromaddr, int* fromlen);
函数功能: 接收网络数据
函数参数:
sockfd: 套接字描述符
buf: 内存缓冲区地址,用于获取读取到的数据
size: 内存缓冲区大小
flags: 操作方式,一般写0
fromaddr: 用于获取对方地址信息的结构体指针,
fromlen: 用于获取对方地址结构体的长度的指针
函数返回值: 成功返回 实际接收的字节数;
失败返回-1,错误码放在errno中
4.发送消息给客户端
sendto
头文件: #include <sys/types.h>
#include <sys/socket.h>
函数原型: int sendto(int sockfd,const void* buf,int size,int flags,
struct sockaddr* toaddr, int tolen);
函数功能: 接收网络数据
函数参数:
sockfd: 套接字描述符
buf: 内存缓冲区地址,用于存储待发送的数据
size: 待发送数据的长度
flags: 操作方式,一般写0
toaddr: 目标主机的地址信息的结构,
tolen: 目标主机地址结构体的长度
函数返回值: 成功返回 实际接收的字节数;
失败返回-1,错误码放在errno中
5.关闭套接字 close
客户端编程流程:
1.创建socket;(socket)
2.设定对方的地址信息;
3.发送消息给服务端(sendto)
4.接收服务端的信息(recvfrom)
5.关闭套接字 close
2) 广播通讯 broadcast
原则: UDP广播通讯需要借助特殊IP(广播地址)
广播地址:
1. 直接广播地址:
主机ID各个二进制位均为1.
xxx.xxx.xxx.255
2. 本地广播地址
网络ID,主机ID各个二进制位均为1.
255.255.255.255
发送端的实现流程:
1. 创建数据报套接字
2. 设置套接字广播属性
setsockopt
头文件: #include <sys/types.h>
#include <sys/socket.h>
函数原型: int setsockopt(int sockfd,int level,int optname,
const void* optval, int optlen);
函数功能: 设置套接字属性
函数参数:
sockfd: 套接字描述符
level: 指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.
optname: 属性名
optval:指向属性值的指针
optlen:属性值长度
函数返回值: 成功返回0;
失败返回-1,错误码放在errno中
3. 设置广播地址与广播端口
4. 向广播地址与端口发送网络数据
5. 接收回复的信息
6. 关闭套接字
接收端的实现流程:
1. 创建数据报套接字
2. 绑定任一地址和广播端口到套接字
3. 接收广播信息
4. 回复数据
5. 关闭套接字
示例:
// 头文件保护,防止重复包含
#ifndef _HEADER_H
#define _HEADER_H#include <unistd.h> // 提供 close() 等系统调用
#include <sys/types.h> // 数据类型定义
#include <sys/socket.h> // 套接字相关函数
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 提供 atoi() 等函数
#include <string.h> // 字符串处理
#include <netinet/in.h> // 互联网地址族
#include <arpa/inet.h> // 地址转换函数
#include <strings.h> // 字符串操作(bzero等)// 类型别名定义,简化代码
typedef struct sockaddr sa_t; // 通用套接字地址结构
typedef struct sockaddr_in sin_t; // IPv4套接字地址结构#endifint main(int argc, char** argv)
{// 检查命令行参数if(argc < 3){fprintf(stderr, "Usage: %s servIP servPort\n", argv[0]);return -1;}// 创建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(-1 == sockfd){perror("socket");return -1;}// 设置服务器地址信息sin_t server = {AF_INET}; // 初始化地址结构,设置地址族为IPv4server.sin_addr.s_addr = inet_addr(argv[1]); // 将IP字符串转换为网络字节序server.sin_port = htons(atoi(argv[2])); // 将端口号转换为网络字节序int len = sizeof(sin_t); // 地址结构长度// 向服务器发送数据const char* p = "hello,server!";sendto(sockfd, p, strlen(p), 0, (sa_t*)&server, len);// 接收服务器响应char szbuf[64] = {0}; // 接收缓冲区int n = recvfrom(sockfd, szbuf, sizeof(szbuf)-1, 0, NULL, NULL);if(n == -1){perror("recvfrom");close(sockfd);return -1;}// 处理接收到的数据szbuf[n] = 0; // 添加字符串结束符printf("[%s:%d]发来数据:%s\n", inet_ntoa(server.sin_addr), // 将IP转换为字符串ntohs(server.sin_port), // 将端口转换为主机字节序szbuf); // 打印接收到的数据// 关闭套接字close(sockfd);return 0;
}
关键函数详解
1. socket()
- 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
AF_INET
:IPv4地址族SOCK_DGRAM
:数据报套接字(UDP)0
:默认协议(UDP)
2. inet_addr()
- IP地址转换
server.sin_addr.s_addr = inet_addr("192.168.1.100");
将点分十进制IP转换为32位网络字节序整数
3. htons()
- 端口号转换
server.sin_port = htons(8080);
将16位主机字节序端口号转换为网络字节序
4. sendto()
- 发送数据
sendto(sockfd, data, len, flags, dest_addr, addrlen);
UDP是无连接的,每次发送都需要指定目标地址
5. recvfrom()
- 接收数据
recvfrom(sockfd, buffer, size, flags, src_addr, addrlen);
可以获取发送方的地址信息
程序执行流程
1. 检查命令行参数
2. 创建UDP套接字
3. 设置服务器地址信息
4. 向服务器发送"hello,server!"消息
5. 等待并接收服务器响应
6. 打印服务器信息和响应数据
7. 关闭套接字
编译和运行
编译命令:
gcc -o udp_client udp_client.c
运行命令:
./udp_client 服务器IP 服务器端口
示例:
./udp_client 127.0.0.1 8080
./udp_client 192.168.1.100 8888
对应的UDP服务器示例
为了完整测试,这里提供一个简单的UDP服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define BUFFER_SIZE 1024int main(int argc, char** argv) {if (argc < 2) {fprintf(stderr, "Usage: %s port\n", argv[0]);return -1;}// 创建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == -1) {perror("socket");return -1;}// 绑定地址struct sockaddr_in server_addr = {0};server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口server_addr.sin_port = htons(atoi(argv[1]));if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(sockfd);return -1;}printf("UDP服务器启动,监听端口 %d...\n", atoi(argv[1]));while (1) {char buffer[BUFFER_SIZE];struct sockaddr_in client_addr;socklen_t client_len = sizeof(client_addr);// 接收数据int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,(struct sockaddr*)&client_addr, &client_len);if (n == -1) {perror("recvfrom");continue;}buffer[n] = '\0';printf("收到来自 [%s:%d] 的消息: %s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buffer);// 发送响应char response[BUFFER_SIZE];snprintf(response, sizeof(response), "服务器已收到你的消息: %s", buffer);sendto(sockfd, response, strlen(response), 0,(struct sockaddr*)&client_addr, client_len);}close(sockfd);return 0;
}
网络字节序与主机字节序
转换函数:
函数 | 作用 | 示例 |
---|---|---|
htons() | Host to Network Short | htons(8080) |
ntohs() | Network to Host Short | ntohs(port) |
htonl() | Host to Network Long | htonl(ip_addr) |
ntohl() | Network to Host Long | ntohl(ip_addr) |
地址转换函数:
函数 | 作用 | 示例 |
---|---|---|
inet_addr() | 字符串→网络字节序 | inet_addr("192.168.1.1") |
inet_ntoa() | 网络字节序→字符串 | inet_ntoa(addr) |
inet_pton() | 字符串→网络字节序(IPv4/IPv6) | inet_pton(AF_INET, ip, &addr) |
inet_ntop() | 网络字节序→字符串(IPv4/IPv6) | inet_ntop(AF_INET, &addr, buf, len) |
程序特点
无连接:UDP不需要建立连接
简单快速:相比TCP开销小
不可靠:不保证数据送达和顺序
适合场景:实时应用、DNS查询、视频流等
改进建议
1. 添加错误处理
// 检查sendto返回值
if (sendto(sockfd, p, strlen(p), 0, (sa_t*)&server, len) == -1) {perror("sendto");close(sockfd);return -1;
}
2. 使用更现代的地址转换
// 使用inet_pton替代inet_addr(支持IPv6)
if (inet_pton(AF_INET, argv[1], &server.sin_addr) <= 0) {fprintf(stderr, "无效的IP地址: %s\n", argv[1]);return -1;
}
3. 添加超时设置
// 设置接收超时
struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
这个UDP客户端程序展示了基本的网络编程概念,是学习socket编程的良好起点。
server.sin_addr.s_addr = inet_addr(argv[1]);
这行代码用于将字符串形式的IP地址转换为网络字节序的二进制格式。让我详细解释:
函数原型
in_addr_t inet_addr(const char *cp);
参数说明
cp
:点分十进制格式的IP地址字符串(如:"192.168.1.1")返回值:32位网络字节序的IPv4地址,如果无效返回
INADDR_NONE
在代码中的使用
server.sin_addr.s_addr = inet_addr(argv[1]);
server
:struct sockaddr_in
结构体变量server.sin_addr
:struct in_addr
结构体(包含IP地址)server.sin_addr.s_addr
:32位网络字节序的IP地址argv[1]
:命令行参数,期望是IP地址字符串
实际转换过程
示例1:有效IP地址
// 输入字符串
char* ip_str = "192.168.1.100";// 转换过程
server.sin_addr.s_addr = inet_addr(ip_str);// 结果:
// ip_str = "192.168.1.100" → s_addr = 0x6401A8C0 (网络字节序)
示例2:本地回环地址
char* ip_str = "127.0.0.1";
server.sin_addr.s_addr = inet_addr(ip_str);
// 结果为:0x0100007F
字节序说明
网络字节序 vs 主机字节序
// 假设IP地址: 192.168.1.100
// 点分十进制: 192.168.1.100// 主机字节序(小端序):
// 内存布局: 100 . 1 . 168 . 192
// 十六进制: 0x64 0x01 0xA8 0xC0// 网络字节序(大端序):
// 内存布局: 192 . 168 . 1 . 100
// 十六进制: 0xC0 0xA8 0x01 0x64
// 整数值: 0xC0A80164
完整的地址设置流程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>int main() {struct sockaddr_in server;// 设置地址族server.sin_family = AF_INET;// 设置IP地址server.sin_addr.s_addr = inet_addr("192.168.1.100");// 设置端口server.sin_port = htons(8080);// 打印验证printf("IP地址(字符串): %s\n", "192.168.1.100");printf("IP地址(网络字节序): 0x%08X\n", server.sin_addr.s_addr);printf("IP地址(转换回字符串): %s\n", inet_ntoa(server.sin_addr));return 0;
}
输出:
IP地址(字符串): 192.168.1.100
IP地址(网络字节序): 0xC0A80164
IP地址(转换回字符串): 192.168.1.100
错误处理
inet_addr()
在遇到无效IP时会返回 INADDR_NONE
(通常是 0xFFFFFFFF
):
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>int main(int argc, char** argv) {if (argc < 2) {fprintf(stderr, "Usage: %s IP地址\n", argv[0]);return -1;}struct sockaddr_in server;server.sin_family = AF_INET;// 转换IP地址并检查错误server.sin_addr.s_addr = inet_addr(argv[1]);if (server.sin_addr.s_addr == INADDR_NONE) {fprintf(stderr, "错误: 无效的IP地址 '%s'\n", argv[1]);return -1;}printf("IP地址有效: %s -> 0x%08X\n", argv[1], server.sin_addr.s_addr);return 0;
}
特殊IP地址值
1. INADDR_ANY
// 监听所有网络接口
server.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
2. INADDR_LOOPBACK
// 本地回环地址
server.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1
3. INADDR_BROADCAST
// 广播地址
server.sin_addr.s_addr = htonl(INADDR_BROADCAST); // 255.255.255.255
现代替代方案:inet_pton()
inet_addr()
是较老的函数,现代编程推荐使用 inet_pton()
:
#include <stdio.h>
#include <arpa/inet.h>int main() {struct sockaddr_in server;server.sin_family = AF_INET;// 使用 inet_pton (更安全,支持IPv6)if (inet_pton(AF_INET, "192.168.1.100", &server.sin_addr) <= 0) {perror("inet_pton");return -1;}printf("转换成功: 0x%08X\n", server.sin_addr.s_addr);return 0;
}
inet_pton()
vs inet_addr()
特性 | inet_addr() | inet_pton() |
---|---|---|
标准 | 较老,已弃用 | POSIX标准 |
IPv6支持 | 否 | 是 |
错误处理 | 返回INADDR_NONE | 返回值表示成功/失败 |
安全性 | 较低 | 较高 |
实际应用示例
UDP客户端完整示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main(int argc, char** argv) {if (argc < 3) {fprintf(stderr, "Usage: %s IP端口 消息\n", argv[0]);return -1;}// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == -1) {perror("socket");return -1;}// 设置服务器地址struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));// 转换IP地址if (inet_pton(AF_INET, argv[1], &server.sin_addr) <= 0) {fprintf(stderr, "无效的IP地址: %s\n", argv[1]);close(sockfd);return -1;}// 发送数据const char* message = (argc > 3) ? argv[3] : "Hello, Server!";if (sendto(sockfd, message, strlen(message), 0, (struct sockaddr*)&server, sizeof(server)) == -1) {perror("sendto");close(sockfd);return -1;}printf("向 %s:%s 发送消息: %s\n", argv[1], argv[2], message);close(sockfd);return 0;
}
总结
server.sin_addr.s_addr = inet_addr(argv[1]);
的作用:
字符串转换:将人类可读的IP字符串转换为机器可读的二进制格式
字节序转换:自动转换为网络字节序(大端序)
地址设置:为套接字连接设置目标服务器地址
虽然inet_addr()
仍然广泛使用,但在新代码中建议使用更现代的inet_pton()
函数。