LV06-03-网络编程-05-UDP协议编程

本文主要是网络编程——UDP协议下的socket编程的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、UDP协议简介

1. 简介

UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议,是一种无连接、不可靠的协议,同样它也是工作在传顺层。它只是简单地实现从一端主机到另一端主机的数据传输功能,这些数据通过 IP 层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理,这就带来了很大的不方便,此外, UDP 协议更没有流量控制、拥塞控制等功能,在发送的一端, UDP 只是把上层应用的数据封装到 UDP 报文中,在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到 IP 数据报中发送出去。而在接收端,无论是否收到数据,它都不会产生一个应答发送给源主机,并且如果接收到数据发送校验错误,那么接收端就会丢弃该UDP 报文,也不会告诉源主机,这样子传输的数据是无法保障其准确性的,如果想要其准确性,那么就需要应用程序来保障了。

2. UDP协议的特点

(1)无连接、不可靠;

(2)尽可能提供交付数据服务,出现差错直接丢弃,无反馈;

(3)面向报文,发送方的 UDP 拿到上层数据直接添加个 UDP 首部,然后进行校验后就递交给 IP 层,而接收的一方在接收到 UDP 报文后简单进行校验,然后直接去除数据递交给上层应用;

(4)速度快,因为 UDP 协议没有 TCP 协议的握手、确认、窗口、 重传、拥塞控制等机制, UDP 是一个无状态的传输协议,所以它在传递数据时非常快,即使在网络拥塞的时候 UDP 也不会降低发送的数据。

UDP 虽然有很多缺点,但也有自己的优点,所以它也有很多的应用场合,因为在如今的网络环境下,UDP 协议传输出现错误的概率是很小的,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,这也不是什么大不了的事情,所以, UDP协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。

3. TCPUDP的对比

首先,我们先再次了解一下这两种协议。

TCP面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。

UDP无连接的传输协议没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要 ACK 包确认。UDP 传输数据就好像我们邮寄包裹,邮寄前需要填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP 协议也是如此,它只管把数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。

既然如此,这点上看,TCP 应该是更加优质的传输协议嘛?其实可能不然。

如果只考虑可靠性,TCP 的确比 UDP 好。但 UDP 在结构上比 TCP 更加简洁,不会发送 ACK 的应答消息,也不会给数据包分配 seq 序号,所以 UDP 的传输效率有时会比 TCP 高出很多,编程中实现 UDP 也比 TCP 简单。

UDP 的可靠性虽然比不上TCP,但也不会像想象中那么频繁地发生数据损毁,在更加重视传输效率而非可靠性的情况下,UDP 是一种很好的选择。比如视频通信或音频通信,就非常适合采用 UDP 协议;通信时数据必须高效传输才不会产生卡顿现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现雪花,音频可能会夹带一些杂音,这些很多情况下其实都是可以忽略的。

UDP 相比,TCP 的生命在于流控制,这保证了数据传输的正确性。

TCP 的速度无法超越 UDP,但在收发某些类型的数据时有可能接近 UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近于 UDP

二、UDP编程流程

1. 一些说明

  • (1)UDP中的服务器端和客户端没有连接

UDP 不像 TCP,无需在连接状态下交换数据,因此基于 UDP 的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen()accept() 函数。UDP 中只有创建套接字的过程和数据交换的过程。

  • (2)UDP服务器端和客户端均只需1个套接字

TCP 中,套接字是一对一的关系。如要向 10 个客户端提供服务,那么除了负责监听的套接字外,还需要创建 10 套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。就像之前邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为 UDP 套接字,只要有 1 个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需 1UDP套接字就可以向任意主机传送数据。

  • (3)基于UDP的接收和发送函数

创建好 TCP 套接字后,传输数据时无需再添加地址信息,因为 TCP 套接字将保持与对方套接字的连接。也就是说,TCP 中的用于数据传输的socket描述符是知道目标IP信息和端口的。但 UDP 套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。

所以,我们在UDP编程中会使用到recvfrom()/sendto()这一组函数,这两个函数说明可以看这篇笔记:《LV06-07-网络编程-socket》

2. UDP编程步骤

使用UDP协议编程的一般步骤如下:

(1)创建 UDP协议的 socket套接字,用socket()函数,注意套接字类型选择SOCK_DGRAM

(2)用sendto() 函数往指定的IP发送信息。

(3)关闭socket套接字。

(1)创建UDP协议的 socket套接字,用socket()函数,注意套接字类型选择SOCK_DGRAM

(2)设置socket的属性,用setsockopt()函数(可选)。

(3)socket绑定包含 IP地址信息和端口号的 struct sockaddr_in(IPv4)结构体,用bind()函数。

(4)循环接收消息,用recvfrom()函数。

(5)关闭socket套接字。

3. UDP编程流程图

基本流程图如下:

image-20220630065858343

三、UDP循环服务器

1. 服务器模型

循环服务器程序模型如下:

1
2
3
4
5
6
7
8
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}

2.使用实例

【注意】

(1)socket() 函数第二个参数要换成SOCK_DGRAM,以指明使用 UDP 协议。

(2)为方便更换服务器IP以便于测试,这里还是使用绑定0.0.0.0这种形式,这其实与INADDR_ANY 效果是一样的。个人感觉这样更灵活一些。

2.1server服务器端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——UDP
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen recvfrom sendto */
#include <sys/socket.h> /* socket inet_addr bind listen recvfrom sendto */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
/* 参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 1.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)/* SOCK_STREAM,使用UDP协议 */
{
perror ("socket");
exit(-1);
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 2.将套接字与指定端口号和IP进行绑定 */
/* 2.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 2.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
printf ("Server starting....OK!\n");
/* 3.数据读写 */
/* 3.1客户端IP地址及端口号获取的相关变量 */
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
char ipv4_addr[16];
/* 3.2数据读写相关变量 */
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
while(1)
{
/* 4.接收来自客户端的数据 */
bzero(buf, BUFSIZ);
bzero(replay, BUFSIZ);
if(recvfrom(socket_fd, buf, BUFSIZ-1, 0,(struct sockaddr *)&cin, &addrlen ) < 0)
{
perror("recvfrom");
continue;
}
/* 5.打印客户端的IP信息 */
if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin)))
{
perror ("inet_ntop");
exit(-1);
}
printf("Recived from(%s:%d), data:%s", ipv4_addr, ntohs(cin.sin_port), buf);
/* 6.对客户端做出应答 */
strcat(replay, buf);
if(sendto(socket_fd, replay, strlen(replay), 0, (struct sockaddr *)&cin, addrlen) < 0)
{
perror("sendto");
continue;
}
/*7.判断是否需要退出 */
if(!strncasecmp(buf, "quit", strlen("quit"))) //用户输入了quit字符
{
printf ("Client(%s:%d) is exiting!\n", ipv4_addr, ntohs(cin.sin_port));
// break; /* 防止有一个客户端的退出而导致服务器端退出 */
}
}
/* 8.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

2.2 client客户端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——UDP
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */

#include <sys/types.h> /* socket recvfrom sendto */
#include <sys/socket.h>/* socket inet_addr recvfrom sendto */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h> /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
/* 参数判断及端口号处理 */
int port = -1;
int portClient = -1;
if (argc != 4)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
portClient = atoi(argv[3]);
if (port < 5000 || portClient < 5000 || (port == portClient))
{
usage(argv[0]);
exit(-1);
}

/* 1.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) /* SOCK_DGRAM 表示UDP协议 */
{
perror ("socket");
exit(-1);
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 2.连接服务器 */
/* 2.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 绑定固定的IP 和 端口号 */
struct sockaddr_in sinClient;
bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
sinClient.sin_family = AF_INET; /* 协议族 */
sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
{
perror("bind");
exit(-1);
}

printf("Client staring...OK!\n");
/* 3.数据读写 */
struct sockaddr_in cin;
char ipv4_addr[16];
socklen_t addrlen1 = sizeof(sin);
socklen_t addrlen2 = sizeof(cin);
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
while (1)
{
/* 3.1标准输入获取发送数据 */
bzero (buf, BUFSIZ);
bzero (replay, BUFSIZ);
printf(">");
if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
{
perror("fgets");
continue;
}
/* 3.2发送数据 */
if(sendto(socket_fd, buf, strlen(buf), 0, (struct sockaddr *)&sin, addrlen1) < 0)
{
perror("sendto");
continue;
}
/* 3.3接收服务器的回馈数据 */
if(recvfrom(socket_fd, replay, BUFSIZ-1, 0,(struct sockaddr *)&cin, &addrlen2 ) < 0)
{
perror("recvfrom");
continue;
}
if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin)))
{
perror ("inet_ntop");
exit (1);
}
printf("server(%s:%d) replay:%s\n", ipv4_addr, ntohs(cin.sin_port), replay);
/* 3.3判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 4.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

2.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

点击查看 Makefile 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

clean_out :
@rm -vf *.out

2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

1
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

1
2
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall server.c -o server

对于服务器端,我们执行以下命令启动服务器进程:

1
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

1
2
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后再发送一些数据,我们就会看到如下现象:

image-20220630114716390