UDP通信实现

embedded/2025/3/26 6:07:15/

一、Socket简介(套接字)

        TCP/IP 五层网络模型的应用层编程接口称为Socket API,  Socket( 套接字 ) ,它是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议>网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议>网络协议栈是应用程序通过网络协议>网络协议进行网络通信的接口
Socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概
念。
为什么需要Socket
1. 普通的 I/O 操作
        打开文件->读/ 写操作->关闭文件
2. 网络通信
        TCP/IP协议被集成到操作系统的内核中,引入了新型的 “I/O” 操作.
        既然是文件,那么我们可以使用文件描述符引用套接字。与管道类似,Linux 系统将其封装成文件的目的
        是为了统一接口,使得读写套接字和读写文件的操作一致
流式套接字(SOCKET_STREAM)
流式套接 (SOCKET_STREAM):
        提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制, 避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字 (SOCK_DGRAM):
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字 (SOCK_RAW):
可以对较低层次协议如 IP ICMP 直接访问。

二、网络通信

        本质就是:网络通信的本质是不同主机,不同进程之间的通信。

1. 网络通信的核心要素

要素说明
IP地址设备的唯一标识(如IPv4的192.168.1.1,IPv6的2001:db8::1)。
端口号区分同一设备上的不同服务(范围0~65535,如HTTP=80,SSH=22)。
协议规定数据格式和传输规则(如TCP、UDP、HTTP)。
套接字(Socket)编程接口,用于实现网络通信(如Python的socket模块)。

        进程A 需要给进程 B 发送信息,就必须知道进程 B IP+端口号   ,因此每个 Socket 都与端口号和协议有关。

三, UDP通信创建流程

        UDP是一个传输层的无连接的协议,我们编写代码一般是分为两个端,发送端和接收端。正常一般是接收端先运行,然后等待发送端发送数据

1. 流程图

(1) 服务端(接收端)流程

  1. 创建UDP Socket

  2. 绑定IP和端口bind()

  3. 接收数据recvfrom()

  4. 处理数据并响应(可选)

  5. 关闭Socket

(2) 客户端(发送端)流程

  1. 创建UDP Socket

  2. 发送数据sendto()

  3. 接收响应(可选,recvfrom()

  4. 关闭Socket

2、UDP通信相关API函数

1. 创建Socket套接字,实质类似于对文件的操作

#include <sys/types.h> 
#include <sys/socket.h>
函数原型
int socket(int domain, int type, int protocol);
描述
创建一个通信的终端节点,并返回一个指向该端点的文件描述符。
参数
domain:选择用于通信的协议族,目前Linux内核可以理解的格式包括AF_UNIX Local communication(本地通信) AF_LOCAL Synonym for AF_UNIX(AF_UNIX的同义词)AF_INET IPv4 Internet protocols(IPv4互联网协议)......
type:套接字类型,指定通信语义,目前定义的类型有SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte
streams.SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a
fixed maximum length)....... 
protocol(/ˈprəʊtəkɒl/协议):协议编号,0表示让系统自动识别

2. 发送数据

头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct
sockaddr *dest_addr, socklen_t addrlen);
描述
将消息发送到另一个套接字
参数
sockfd:发送套接字
buf:发送数据的起始地址
len:实际发送的数据大小
flags:操作方式,0表示默认操作
dest_addr:发送的目标地址
addrlen:发送的目标地址的大小
返回值
成功:返回发送的字节数
失败:-1并设置errno
【man 7 ip】
struct sockaddr{__SOCKADDR_COMMON (sa_); /* unsigned short int sa_family (协议族) */char sa_data[14]; /* Address data. */};/* Structure describing an Internet socket address. */
struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. (端口号字节序) */struct in_addr sin_addr; /* Internet address.(IP网络字节序) *//* 填充到 `struct sockaddr'的大小 */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZEsizeof (in_port_t)- sizeof (struct in_addr)];};
struct in_addr {in_addr_t s_addr;
};
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

四, UDP客户端代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void send_data(int,struct sockaddr_in*,int);
int main(int argc,char* argv[])
{
if(argc !=3){
fprintf(stderr,"command: %s, ip port\n",argv[0]);
exit(EXIT_FAILURE);}
// 1、创建Socket套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
exit(EXIT_FAILURE);}
// 2、将接收地址的 IP+端口 封装成struct sockaddr_in类型
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(dest_addr.sin_addr));
// 3、发送数据
send_data(sockfd,&dest_addr,sizeof(dest_addr));
// 4、关闭文件描述符
close(sockfd);
}
void send_data(int sockfd,struct sockaddr_in* pdest_addr,int length)
{
// 循环发送
while(1){
//用户输入发送内容
putchar('>');
char buf[256]={0};
// 清空buf
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
// 将回车符转为'\0'
buf[strlen(buf)-1]='\0';
// 发送
int ret = sendto(sockfd,buf,strlen(buf),0,(struct
sockaddr*)pdest_addr,length);
if(ret == -1){
perror("sendto");
exit(EXIT_FAILURE);}
if(strncmp(buf,"quit",4)==0){
break;}}
}

可以使用软件模拟服务器,来判断UDP客户端的代码是否正确

1. 下载
链接: https://pan.baidu.com/s/1-FD4nYgZJlw9nOBrqFm_0A
提取码: 1234

大家可以实现一个小功能;

        编写一个UDP 发送方的代码,新建一个 log.txt 的文件,在文件中写入数据,用户读取 log.txt 文件的内容,通过sendto()发送给网络调试助手,并通过网络测试助手接收数据,判断读取的内容是否正确。

五, UDP服务端代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
void receive_message(int sockfd);
int main(int argc,char* argv[])
{
if(argc !=3){
fprintf(stderr,"command:%s ip port\n",argv[0]);
exit(EXIT_FAILURE);}
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
exit(EXIT_FAILURE);}
// 为socket绑定IP+端口号
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = bind(sockfd,(struct sockaddr*)&addr,(int)sizeof(addr));
if(ret == -1){
perror("bind");
goto release_resources;
exit(EXIT_FAILURE);}
// 接收消息
receive_message(sockfd);
goto release_resources;
return 1;
release_resources:
close(sockfd);
}
void receive_message(int sockfd)
{
char buf[512]={0};
struct sockaddr_in addr;
printf("wait......\n");
while(1){
// 清空
memset(buf,0,sizeof(buf));
socklen_t addrlen = sizeof(addr);
ssize_t bytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&addr,&addrlen);
if(bytes == -1){
perror("recvfrom");}
//打印接收到的消息
printf("src_ip=%s\n",inet_ntoa(addr.sin_addr));
printf("src_port=%d\n",ntohs(addr.sin_port));
printf("content=%s\n",buf);
if(strncmp(buf,"quit",4)==0){
break;}}
}

最终结果;

六, UDP并发服务器之多进程并发

网络程序里面,通常都是一个服务器处理多个客户端。为了处理多个客户端的请求 , 服务器端程序有不同的处理方式。

1、常见的服务器类型

1.1. 迭代服务器

大多数 UDP 都是迭代运行,服务器等待客户端的数据,收到数据后处理该数据,送回其应答,在等待下一个客户端请求。

1.2. 并发服务器

        并发服务器是指在同一个时刻可以响应多个客户端的请求
        本质是创建多线程/多进程   ,对多数用户的信息进行处理
        UDP协议一般默认是不支持多线程并发的,因为默认UDP服务器只有一个 sockfd ,所有的客户端都是通过同一个sockfd进行通信的,udp使用一个socket,如何做到高并发的呢?

1.3. UDP并发服务器使用的场景

        当UDP协议针对客户请求的处理需要消耗过长的时间时,我们期望 UDP 服务器具有某种形式的并发性。

2、UDP多进程并发服务器

1. 场景设计

        多个udp 客户端需要先验证密钥是否正确后,才允许进行数据交互。假设密钥为 "root" 。(类似于登录功能)
        服务器接收到客户端信息,需要考虑两种情况
                <1>A用户的密钥验证请求消息
                <2>B用户的数据交互接收消息

2. 框架图

所有的客户端都是通过同一个sockfd进行通 信的。udp使用一个 socket ,如何做到做并发呢?

3. 使用场景

        当UDP 服务器与客户端交互多个数据报。问题在于每个客户都是往服务器端的同一个的端口发送数据,并用的同一个sockfd 。并发服务器的每一个子进程如何正确区分每一个客户的数据报(涉及到进程的调度问题,如何避免一个子进程读取到不该它服务的客户发送来的数据报)。
        解决的方法是服务器(知名端口)等待客户的到来,当一个客户到来后,记下其IP port ,然后服务器 fork 一个子进程,建立一个socket bind 一个随机端口,然后建立与客户端的连接,并处理该客户的请求。父进程继续循环,等待下一个客户的到来。

3、代码实现

在tftpd中就是使用这种技术的

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#define LOGIN_KEY "root"
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
int init_socket(const char* ip,const char* port)
{
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
return -1;}
// 绑定IP + 端口
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(port));
inet_aton(ip,&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
if(bind(sockfd,(struct sockaddr*)&addr,addrlen)==-1){
fprintf(stderr,"bind failed\n");
return -1;}
return sockfd;
}
int authentication_key(const char* ip,const char* port)
{
int sockfd = init_socket(ip,port);
if(sockfd == -1){
return -1;}
char buf[512]={0};
int len = sizeof(buf);
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int new_sockfd;
// 循环验证用户密钥
while(1){
memset(buf,0,len);
ssize_t recvbytes = recvfrom(sockfd,buf,len,0,(struct
sockaddr*)&addr,&addrlen);
if(recvbytes == -1){
perror("redvfrom");
return -1;}
unsigned char loginstatus = (strncmp(buf,LOGIN_KEY,4)==0)?
LOGIN_SUCCESS:LOGIN_FAILURE;
if(loginstatus == LOGIN_SUCCESS){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;}
else if(pid == 0){
// 执行的是子进程
close(sockfd);//密钥验证成功,不需要sockfd文件描述符
new_sockfd = init_socket(ip,"0");// 绑定0端口,系统
会随机分配一个可用的端口号
sendto(new_sockfd,&loginstatus,sizeof(loginstatus),0,(struct
sockaddr*)&addr,addrlen);
break;}}
else{
// 登录失败,使用原端口回复信息
ssize_t ret =
sendto(sockfd,&loginstatus,sizeof(loginstatus),0,(struct sockaddr*)&addr,addrlen);}}
return new_sockfd;
}
// 接收数据
void recv_data(int sockfd)
{
char buf[512]={0};
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
while(1){
memset(buf,0,sizeof(buf));
ssize_t recvbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&client_addr,&addrlen);
if(recvbytes== -1){
perror("recvfrom");
exit(EXIT_FAILURE);}
printf("client ip:%s\n",inet_ntoa(client_addr.sin_addr));
printf("client port:%d\n",ntohs(client_addr.sin_port));
printf("client content:%s\n",buf);
if(strncmp(buf,"quit",4)==0){
break;}}
close(sockfd);
exit(EXIT_SUCCESS);
}
void signal_handler(int signum)
{
// 回收子进程的资源
waitpid(-1,NULL,WNOHANG);
printf("%s\n",strsignal(signum));
}
int main(int argc,char* argv[])
{
if(argc !=3){
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);}
// 回收僵尸态的子进程[子进程结束后,会发SIGCHLD信号]
if(signal(SIGCHLD,signal_handler)==SIG_ERR){
perror("signal error.");
exit(EXIT_FAILURE);}
// 验证秘钥
int sockfd = authentication_key(argv[1],argv[2]);
// recv data
recv_data(sockfd);
return 0;
}

客户端不需要进行修改,如此就实现了客户端和服务处理高并发的基础功能

结果:


http://www.ppmy.cn/embedded/176714.html

相关文章

MongoDB 面试备战指南

MongoDB 面试备战指南 一、基础概念 1. MongoDB是什么类型的数据库&#xff1f;和关系型数据库有什么区别&#xff1f; 答案&#xff1a; MongoDB是文档型NoSQL数据库&#xff0c;核心区别&#xff1a; 数据模型&#xff1a;存储JSON-like文档&#xff08;动态schema&#xf…

玄机-第六章 流量特征分析-蚂蚁爱上树的测试报告

目录 一、测试环境 二、测试目的 三、操作过程 Flag1 Flag2 Flag3 四、结论 一、测试环境 靶场介绍&#xff1a;国内厂商设置的玄机靶场&#xff0c;以应急响应题目著名。 地址&#xff1a;https://xj.edisec.net/challenges/44 靶机简介&#xff1a; 二、测试目的 …

基于 arco 的 React 和 Vue 设计系统

arco 是字节跳动出品的企业级设计系统&#xff0c;支持React 和 Vue。 安装模板工具 npm i -g arco-cli创建项目目录 cd someDir arco init hello-arco-pro? 请选择你希望使用的技术栈React❯ Vue? 请选择一个分类业务组件组件库Lerna Menorepo 项目❯ Arco Pro 项目看到以…

Apache SeaTunnel同步MySQL到Doris的优化策略

在数据仓库建设过程中&#xff0c;数据同步是一个关键环节。Apache SeaTunnel作为一个高性能的分布式数据集成工具&#xff0c;被广泛用于将MySQL数据同步到Doris等OLAP数据库。 然而&#xff0c;如何优化这个同步过程&#xff0c;提高效率并减少资源消耗&#xff0c;是每个数据…

C++11QT复习 (五)

文章目录 **Day6-2 成员访问运算符重载&#xff08;2025.03.25&#xff09;****1. 复习****2. 成员访问运算符重载****2.1 箭头运算符 (->) 重载****(1) 语法** **2.2 解引用运算符 (*) 重载****(1) 语法** **3. 代码分析****3.1 代码结构****3.2 代码解析****(1) Data 类**…

吾爱出品,文件分类助手,高效管理您的 PC 资源库

在日常使用电脑的过程中&#xff0c;文件杂乱无章常常让人感到困扰。无论是桌面堆积如山的快捷方式&#xff0c;还是硬盘中混乱的音频、视频、文档等资源&#xff0c;都急需一种高效的整理方法。文件分类助手应运而生&#xff0c;它是一款文件管理工具&#xff0c;能够快速、智…

Cmake创建一个QML

使用Qt Quick module.创建一个 QML demo cmake_minimum_required(VERSION 3.16)project(hello VERSION 1.0 LANGUAGES CXX)find_package(Qt6 6.3 COMPONENTS Quick Gui REQUIRED)qt_standard_project_setup(REQUIRES 6.5) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_…

基于Java的班级事务管理系统(源码+lw+部署文档+讲解),源码可白嫖!

摘要 随着世界经济信息化、全球化的到来和电子商务的飞速发展&#xff0c;推动了很多行业的改革。若想达到安全&#xff0c;快捷的目的&#xff0c;就需要拥有信息化的组织和管理模式&#xff0c;建立一套合理、畅通、高效的线上管理系统。当前的班级事务管理存在管理效率低下…