LV025-UDP协议编程
本文主要是网络编程——UDP协议下的socket编程的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
一、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. TCP与UDP的对比
首先,我们先再次了解一下这两种协议。
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 个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需 1 个UDP套接字就可以向任意主机传送数据。
- (3)基于
UDP的接收和发送函数
创建好 TCP 套接字后,传输数据时无需再添加地址信息,因为 TCP 套接字将保持与对方套接字的连接。也就是说,TCP 中的用于数据传输的socket描述符是知道目标IP信息和端口的。但 UDP 套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。
所以,我们在UDP编程中会使用到recvfrom()/sendto()这一组函数,这两个函数说明可以看这篇笔记:《LV06-07-网络编程-socket》
2. UDP编程步骤
使用UDP协议编程的一般步骤如下:
2.1 客户端
(1)创建 UDP协议的 socket套接字,用socket()函数,注意套接字类型选择SOCK_DGRAM。
(2)用sendto() 函数往指定的IP发送信息。
(3)关闭socket套接字。
2.2 服务器端
(1)创建UDP协议的 socket套接字,用socket()函数,注意套接字类型选择SOCK_DGRAM。
(2)设置socket的属性,用setsockopt()函数(可选)。
(3)socket绑定包含 IP地址信息和端口号的 struct sockaddr_in(IPv4)结构体,用bind()函数。
(4)循环接收消息,用recvfrom()函数。
(5)关闭socket套接字。
3. UDP编程流程图
基本流程图如下:

三、UDP循环服务器
1. 服务器模型
循环服务器程序模型如下:
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}2.使用实例
【注意】
(1)socket() 函数第二个参数要换成SOCK_DGRAM,以指明使用 UDP 协议。
(2)为方便更换服务器IP以便于测试,这里还是使用绑定0.0.0.0这种形式,这其实与INADDR_ANY 效果是一样的。个人感觉这样更灵活一些。
2.1server服务器端
/** =====================================================
* 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客户端
/** =====================================================
* 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来进行。
## =====================================================
# 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 *.out2.4 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
make然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client:
gcc -g -O2 -Wall client.c -o client
gcc -g -O2 -Wall server.c -o server对于服务器端,我们执行以下命令启动服务器进程:
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口对于客户端,我们执行以下命令启动客户端进程:
./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003然后再发送一些数据,我们就会看到如下现象:
