文章目录
摘要:这篇文章是对UDP传输协议的讲解,包括基础知识、主要函数,最后通过在linux上用C语言建立客户端和服务端实例对UDP的认识更加深刻。
关键词:UDP、sendto、recvfrom。
1.UDP简介
1.UDP(User Datagram protocol)传输协议是TCP/IP网络协议下,传输层的一中面向报文传输方式。
1.1特点
1.无连接特性:传输时不用建立连接,服务端也不用做出响应。
2. 不可靠传输:没有TCP那样复杂,发送端只管发送数据,不会管接收端的感受,真是个无情的man。
3. 快速传输:由于 UDP 没有连接建立和可靠传输机制带来的开销,它可以非常快速地发送数据。
4.面向数据报:应用层发送多少,UDP传输层就给网络层发送多少数据。
1.2 UDP协议头部格式
1.2.1 UDP头部:
UDP头部没有TCP头部那么复杂,只占了8个字节,而TCP头部最小20各字节,根据可选项参数还能扩展到60个字节,TCP头部长度标识只占4位,但是TCP头部的每一个值表示4字节数目,所以最大长度2^4*4=60。
1.2.2 头部意义:
UDP头部和TCP头部表示的意思不同,UDP头部是整个数据报的长度,因为UDP头部固定8个字节,所以不用标识多长。
更多TCP信息请查看:
TCP三次握手,四次挥手,通俗易懂!!!
Linux C语言TCP协议实战
1.2.3 头部参数:
源端口号:这个可有可无,数据报是无连接的,只需给数据报指定发送方就行了(sendto函数后面会介绍)。由于不需要消耗额外的资源消耗在连接、同步、拥塞控制、流量控制等等机制上,这也是UDP传输快速的原因。
目的端口号:数据报的的接收端必须指定,网络层和链路层通过路由、链路等方法,将数据迅速的发送到指定端口。
UDP长度:指定了数据报的长度,UDP头部固定长度无需指定。
校验和:确认数据是否错误。
1.3 UDP数据长度控制
下面这两张图的意思是设置了IP_MTU_DESCOVER套接字选项,发送数据端会追踪目标IP的最大传输单元(MTU)的大小,而不是SOCK_STREAM数据形式的(也就是不是TCP协议)传输协议会默认使用MTU追踪,默认选项可以在/proc/sys/net/ipv4/ip_no_pmtu_disc文件下设置,从最后一张图中默认为0,默认使用IP_PMTUDISC_WANT选项,这个选项会根据追踪到的目的端MTU的大小来控制数据报的尺寸,关闭这个选项数据超过MTU(加协议头部)UDP会进行数据分片,但是不建议关闭,这样会影响到UDP数据的发送速度,这些是IP层的知识,不多讲,我也懂的不多。
1.4 UDP协议建立框架
UDP协议没有一对一建立连接,只需要确定服务器的IP和端口就可以了,所以不用多线程和和多进程进行处理。
2. 函数介绍
2.1 sendto函数
向指定端口发送数据,在函数中,出现sockaddr结构体,发送端可以不用绑定端口,直接发送数据。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr*dest_addr, socklen_t addrlen);
/*
sockfd: socket函数返回的文件描述符
buf: 数据
len: 数据长度
flags: 可选参数一般填0,更多选项可在man sendto中查看
dest_addr: 网络通用结构体
addrlen: 结构体长度
*/
2.2 recvform函数
阻塞等待接收数据,接收端可以不用绑定端口直接接收数据。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd: socket函数返回的文件描述符
buf: 数据
len: 数据长度
flags: 可选参数一般填0,更多选项可在man sendto中查看
src_addr: 网络通用结构体
addrlen:结构体长度
2.3 其他函数
关于端口和IP地址的主机字节序和网络字节序可以在这篇文章查找:TCP/IP传输协议,易懂!!!
实战代码中用到的其他网络相关函数可以在这篇文章中查找:Linux C语言TCP协议实战
3.实例
3.1 通用结构体、IPV4结构体、IPV6结构体
struct sockaddr {
unsigned short sa_family; // 地址族,用于指定地址类型,例如 AF_INET(IPv4)、AF_INET6(IPv6)等
char sa_data[14]; // 地址数据,具体格式和长度取决于地址族的不同
};
IPV6结构体
struct sockaddr_in6 {
sa_family_t sin6_family; /* 地址族,这里是 AF_INET6,表示 IPv6 地址 */
in_port_t sin6_port; /* 端口号,使用网络字节序 */
uint32_t sin6_flowinfo; /* IPv6 流信息 */
struct in6_addr sin6_addr; /* IPv6 地址 */
uint32_t sin6_scope_id; /* IPv6 范围 ID */
};
IPV4结构体
struct sockaddr_in {
sa_family_t sin_family; // 地址族(address family),通常是 AF_INET,表示使用 IPv4 地址
in_port_t sin_port; // 16 位的端口号,需要使用 `htons()` 函数转换为网络字节序
struct in_addr sin_addr; // 存储 IPv4 地址的结构体
char sin_zero[8]; // 填充字节,为保持与 `struct sockaddr` 一致,一般设置为 0
};
struct in_addr{
uint32_t s_addr;
}
3.2 框架搭建
3.2.1 client
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> //POSIX.1不需要包含,一些旧版本(BSD)需要包含
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
int main(int argc, char const *argv[])
{
int sockfd;
//初始化网络结构体
struct sockaddr_in XXX;
......
//1.创建套接字
socket()
...
//客户端绑定端口,客户端在sendto发送数据的时候,操作系统会随机配置一个端口给客户端,bind一般不用
#if COND_COMPILLE
...
bind()
...
#endif
while(1)
{
//2.通信
...
sendto() //发送数据
...
recvfrom() //阻塞接收
...
}
//3.关闭套接字
close(sockfd);
return 0;
}
3.2.2 server
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int sockfd;
//初始化网络结构体
struct sockaddr_in XXX;
......
//1.创建套接字
socket()
...
//2.绑定端口,用于接收数据
bind()
...
char readBuf[BUF_SIZE] = {0};
while(1)
{
//3.数据处理
...
recvfrom() //阻塞接收数据
...
sendto() //做出响应
...
}
//4.关闭套接字
close(sockfd);
return 0;
}
3.3 服务端和客户端编写
3.3.1 client_udp.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#define BUF_SIZE 256
#define COND_COMPILE 0
#define QUIT_FLAG "quit"
int main(int argc, char const *argv[])
{
if(argc !=3 )
{
printf("input para is incorrent\n");
exit(1);
}
//1.创建套接字
int sockfd;
//初始化网络结构体
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
bzero(&serveraddr, addrlen);
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//客户端绑定端口,一般不用
#if COND_COMPILLE
struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
clientaddr.sin_addr.s_addr = inet_addr(argv[3]);
clientaddr.sin_port = htons(atoi(argv[4]));
if(bind(sockfd, (struct sockaddr *)&clientaddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
#endif
char readBuf[BUF_SIZE] = {0};
char writeBuf[BUF_SIZE] = {0};
while(1)
{
//3.通信
fgets(writeBuf, BUF_SIZE-1, stdin);
size_t len = strlen(writeBuf);
writeBuf[len-1] = '\0';
if(sendto(sockfd, writeBuf, (len-1), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr))==-1)
{
perror("sendto");
exit(1);
}
printf("send complete\n");
if(!strncasecmp(writeBuf, QUIT_FLAG, strlen(QUIT_FLAG)))
{
printf("the client exit\n");
break;
}
if(recvfrom(sockfd, readBuf, BUF_SIZE, 0,(struct sockaddr*)&serveraddr, &addrlen)==-1)
{
perror("recvfrom");
exit(1);
}
printf("%s\n",readBuf);
bzero(readBuf, BUF_SIZE);
bzero(writeBuf, BUF_SIZE);
}
//关闭套接字
close(sockfd);
return 0;
}
3.3.2 sever_udp.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define HOST_PORT 5001
#define BUF_SIZE 256
#define QUIT_FLAG "QUIT"
#define RESP ",RESP"
int main(int argc, char const *argv[])
{
int sockfd;
//初始化网络结构体
struct sockaddr_in serveraddr,cin;
socklen_t addrlen = sizeof(cin);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = INADDR_ANY;
serveraddr.sin_port = htons(HOST_PORT);
//1.创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//2.绑定端口,用于接收数据
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
char readBuf[BUF_SIZE] = {0};
while(1)
{
//3.数据处理
if(recvfrom(sockfd, readBuf, BUF_SIZE, 0, (struct sockaddr*)&cin, &addrlen)==-1)
{
perror("recvform");
exit(0);
}
printf("the client port: %d, IP: %s\n",
ntohs(cin.sin_port), inet_ntoa(cin.sin_addr));
printf("readBuf: %s\n", readBuf);
if(!strncasecmp(readBuf, QUIT_FLAG, strlen(QUIT_FLAG)))
{
printf("the client exit\n");
break;
}
strncat(readBuf, RESP, strlen(RESP));
if(sendto(sockfd, readBuf, BUF_SIZE, 0, (struct sockaddr*)&cin, addrlen)==-1)
{
perror("sendto");
exit(1);
}
printf("send complete\n");
}
close(sockfd);
return 0;
}
3.3.3 终端结果
4.总结
这篇文章着重在UDP实操上,概念上涉及的不是很多。
文章有错误请指出。