当前位置: 首页 > news >正文

网络编程之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 Shorthtons(8080)
ntohs()Network to Host Shortntohs(port)
htonl()Host to Network Longhtonl(ip_addr)
ntohl()Network to Host Longntohl(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)

程序特点

  1. 无连接:UDP不需要建立连接

  2. 简单快速:相比TCP开销小

  3. 不可靠:不保证数据送达和顺序

  4. 适合场景:实时应用、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]);
  • serverstruct sockaddr_in 结构体变量

  • server.sin_addrstruct 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]); 的作用:

  1. 字符串转换:将人类可读的IP字符串转换为机器可读的二进制格式

  2. 字节序转换:自动转换为网络字节序(大端序)

  3. 地址设置:为套接字连接设置目标服务器地址

虽然inet_addr()仍然广泛使用,但在新代码中建议使用更现代的inet_pton()函数。

http://www.dtcms.com/a/435424.html

相关文章:

  • Java_new关键字使用区别详解
  • 拓和科技有限公司网站蜜雪冰城网站建设策划方案
  • 时序数据库高基数问题(二):Java + InfluxDB解决方案
  • win8怎么建设网站江苏昨天出大事
  • 泰山派rk3566中使用交叉编译工具编译测试程序
  • 网站seo教程在线广告设计制作
  • 【自然语言处理】文本表示知识点梳理与习题总结
  • 嘉兴网站建设低价推荐制作企业网站步骤
  • 电容器充放电原理
  • 数组与字典解决方案第三十讲:如何将记录集的数据记入数组
  • 互联网网站建设价格中山如何建设网站
  • 提供网站建设制作做网站攻略
  • 北京seo网站诊断一个人做网站时间
  • 【密码学实战】openHiTLS enc命令行:数据加解密
  • 做网站汉狮网络wordpress多站点
  • Android 四大组件全面解析
  • 【读书笔记】《C陷阱与缺陷》第7章:可移植性陷阱解析 | 编写跨平台C程序
  • 成都专业做网站公司展示型网站案例
  • 大语言模型中的“推理”:基本原理与实现机制解析
  • 成都网站营销推广公司十大网游人气排行榜
  • 单北斗GNSS在桥梁和地质灾害中的变形监测应用与技术发展
  • 郑州网站顾问网上有做logo的网站吗
  • 企业电子商务网站平台建设百度竞价广告的位置
  • 第三类笔记
  • 深圳做律师网站公司百度联盟的网站怎么做
  • Netflix 推荐系统 | 从百万美元挑战赛到个性化体验升级的技术演进
  • 安徽省建设干部学校网站玉林博白网站建设
  • 关于GESP8级题目有疑问
  • JVM参数速查
  • STM32定时器的整体概述