一、说明
UDP(User Datagram Protocol),由RFC 768规范定义,中文名为用户数据报协议。UDP 为应用程序提供了一种无需建立连接就可以发送网络数据包的方法。
UDP是常用的网络传输协议之一,该协议是无连接、不可靠、面向数据报的协议。在Linux C网络程序中广泛使用,如音、视频媒体数据传输、DNS协议、SIP协议等。
UDP通信分为客户端和服务端,其中服务端在指定的网络端口上读取数据,客户端将数据发给服务端绑定的网络端口,无需建立连接即可通信,反过来,服务端向客户端发送数据也是一样。
二、常用API介绍
2.1 socket()
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
作用:创建一个通信的终端
参数说明:
domain: 协议族,常用AF_INET表示IPv4
type: 传输方式,常用的有以下两种:
SOCK_STREAM: TCP
SOCK_DGRAM: UDP
protol: 特殊协议,实际应用中都是写为0
返回值:
成功时返回一个socket文件描述符,失败时返回-1,errno会被设置,可以通过errno值获取错误码
2.2 bind()
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
作用:
绑定网络地址(IP/PORT)到socket
参数说明:
sockfd: socket()返回的描述符
addr: 绑定的地址
addrlen: 绑定的地址结构长度
返回值:
成功时返回0,失败时返回-1,errno会被设置,可以通过errno值获取错误码
2.3 sendto()
#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: socket()创建的socket描述符
buf: 发送的内容(起始地址)
len:发送内容的长度
flags: 发送选项,一般为0
dest_addr:目的地址
addrlen:目的地址长度
返回值:
成功返回发送的字符个数,失败返回-1,errno会被设置
2.4 recvfrom()
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len,
int flags, struct sockaddr *src_addr,
socklen_t *addrlen);
作用:
接收消息
参数说明:
sockfd: socket()创建的socket描述符
buf: 接收的内容(起始地址)
len:接收内容的长度
flags: 接收选项,一般为0
dest_addr:源地址
addrlen:源地址长度
返回值:
成功时返回接收的字节数,错误时返回-1,并设置errno值。如果对端关闭时,返回0
2.5 close()
#include <unistd.h>
int close(int fd);
作用:
关闭一个(文件/socket)描述符
参数说明:
fd: 描述符
三、UDP实例分析
3.1 基本UDP通信示例
server_udp.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_PORT 9999
int server_udp()
{
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
char buf[1024] = {0};
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr_len = sizeof(server_addr);
ret = bind(socket_fd, (const struct sockaddr *)&server_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
while (1) {
memset(buf, 0, sizeof(buf));
addr_len = sizeof(client_addr);
ret = recvfrom(socket_fd, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &addr_len);
if (ret > 0) {
printf("recv len:%d, data:[%s] from %s:%d\n", ret, buf,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
} else if (0 == ret) {
printf("ret:%d\n", ret);
} else {
printf("ret:%d\n", ret);
}
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
server_udp();
return 0;
}
client_udp.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9999
int client_udp(char *data)
{
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
addr_len = sizeof(server_addr);
ret = sendto(socket_fd, data, strlen(data), 0,
(struct sockaddr *)&server_addr, addr_len);
if (ret > 0) {
printf("send data: [%s] to %s:%d\n", data, inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port));
} else {
printf("ret:%d\n", ret);
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
client_udp("hello world");
} else {
client_udp(argv[1]);
}
return 0;
}
Makefile:
all:
gcc -o server server_udp.c
gcc -o client client_udp.c
clean:
-@rm server client
编译方式:
make
测试运行:
$ ./client
send data: [hello world] to 127.0.0.1:9999
$ ./server
recv len:11, data:[hello world] from 127.0.0.1:44180
$ ./client "I can do it"
send data: [I can do it] to 127.0.0.1:9999
$ ./server
recv len:11, data:[I can do it] from 127.0.0.1:43607
3.2 客户端端口变化问题
在3.1的例子中,客户端重复调用时,服务端收到的数据是从客户端不同的端口发来的,如下:
$ ./server
recv len:1, data:[1] from 127.0.0.1:43113
recv len:1, data:[2] from 127.0.0.1:46748
recv len:1, data:[3] from 127.0.0.1:43452
客户端端口变化是由于客户端的socket没有绑定固定的端口,系统每次随机分配了一个端口。可以使用bind函数绑定固定端口,如下代码所示:
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(CLIENT_PORT);
client_addr.sin_addr.s_addr = inet_addr(CLIENT_IP);
addr_len = sizeof(client_addr);
ret = bind(socket_fd, (const struct sockaddr *)&client_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
客户端绑定端口后,再次测试验证:可以看到客户端的端口一直是绑定的端口10000
$ ./server
recv len:1, data:[1] from 127.0.0.1:10000
recv len:1, data:[2] from 127.0.0.1:10000
recv len:1, data:[3] from 127.0.0.1:10000
3.3 服务器向客户端回写消息
server.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_PORT 9999
int server_udp()
{
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
char buf[1024] = {0};
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr_len = sizeof(server_addr);
ret = bind(socket_fd, (const struct sockaddr *)&server_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
while (1) {
memset(buf, 0, sizeof(buf));
addr_len = sizeof(client_addr);
ret = recvfrom(socket_fd, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &addr_len);
if (ret > 0) {
printf("recv len:%d, data:[%s] from %s:%d\n", ret, buf,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
addr_len = sizeof(client_addr);
ret = sendto(socket_fd, buf, ret, 0,
(struct sockaddr *)&client_addr, addr_len);
if (ret > 0) {
printf("send data: [%s] to %s:%d\n", buf, inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
} else {
printf("ret:%d\n", ret);
}
} else if (0 == ret) {
printf("ret:%d\n", ret);
} else {
printf("ret:%d\n", ret);
}
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
server_udp();
return 0;
}
client.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9999
#define CLIENT_IP "127.0.0.1"
#define CLIENT_PORT 10000
int client_udp(char *data)
{
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
char buf[1024] = {0};
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(CLIENT_PORT);
client_addr.sin_addr.s_addr = inet_addr(CLIENT_IP);
addr_len = sizeof(client_addr);
ret = bind(socket_fd, (const struct sockaddr *)&client_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
addr_len = sizeof(server_addr);
ret = sendto(socket_fd, data, strlen(data), 0,
(struct sockaddr *)&server_addr, addr_len);
if (ret > 0) {
printf("send data: [%s] to %s:%d\n", data, inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port));
} else {
printf("ret:%d\n", ret);
}
memset(buf, 0, sizeof(buf));
addr_len = sizeof(server_addr);
ret = recvfrom(socket_fd, buf, sizeof(buf), 0,
(struct sockaddr *)&server_addr, &addr_len);
if (ret > 0) {
printf("recv len:%d, data:[%s] from %s:%d\n", ret, buf,
inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));
} else if (0 == ret) {
printf("ret:%d\n", ret);
} else {
printf("ret:%d\n", ret);
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
client_udp("hello world");
} else {
client_udp(argv[1]);
}
return 0;
}
测试:
$ ./client "hello world"
send data: [hello world] to 127.0.0.1:9999
recv len:11, data:[hello world] from 127.0.0.1:9999
$ ./server
recv len:11, data:[hello world] from 127.0.0.1:10000
send data: [hello world] to 127.0.0.1:10000
3.4 服务端状态和行为
服务器不启动时,客户端发送消息时,会出现icmp的端口不可达报文,如下:
服务端启动时,可以通过netstat命令查看服务端接收消息的网络端口
$ ./server
$ netstat -anp | grep 9999
udp 0 0 0.0.0.0:9999 0.0.0.0:* 21845/server
3.5 非阻塞模式
socket默认是阻塞模式,因此调用recvfrom()函数时,如果没有客户端发送数据,则recvfrom会一直阻塞,导致程序挂住,不能处理其它事情。因此,在实际应用场景中,会将socket设置为非阻塞。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#define SERVER_PORT 9999
int server_udp()
{
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
char buf[1024] = {0};
int flag = 0;
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
flag = fcntl(socket_fd, F_GETFL, 0);
flag = flag | O_NONBLOCK;
fcntl(socket_fd, F_SETFL, flag);
addr_len = sizeof(server_addr);
ret = bind(socket_fd, (const struct sockaddr *)&server_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
while (1) {
memset(buf, 0, sizeof(buf));
addr_len = sizeof(client_addr);
ret = recvfrom(socket_fd, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &addr_len);
if (ret > 0) {
printf("recv len:%d, data:[%s] from %s:%d\n", ret, buf,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
} else if (0 == ret) {
printf("ret:%d\n", ret);
} else {
if (EAGAIN == errno) {
printf("ret:%d, errno:%d, %s\n", ret, errno, strerror(errno));
} else {
printf("ret:%d, errno:%d, %s\n", ret, errno, strerror(errno));
}
}
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
server_udp();
return 0;
}
测试:
$ ./server
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
ret:-1, errno:11, Resource temporarily unavailable
................................................................
通过轮询的方式调用recvfrom接口时,会出现循环打印日志的情况。
为了解决这个问题,使用IO复用函数epoll来处理socket,只在有数据的时候才调用,示例如下:
server.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/epoll.h>
#define SERVER_PORT 9999
int server_udp()
{
int i = 0;
int num = 0;
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
char buf[1024] = {0};
int flag = 0;
int epoll_fd = -1;
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
struct epoll_event event;
struct epoll_event events_array[10];
epoll_fd = epoll_create(10);
if (epoll_fd < 0) {
printf("epoll_create failure:%s\n", strerror(errno));
return -1;
}
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
flag = fcntl(socket_fd, F_GETFL, 0);
flag = flag | O_NONBLOCK;
fcntl(socket_fd, F_SETFL, flag);
addr_len = sizeof(server_addr);
ret = bind(socket_fd, (const struct sockaddr *)&server_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
event.events = EPOLLIN;
event.data.fd = socket_fd;
ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);
if(ret < 0) {
printf("epoll_trl failure:%s\n", strerror(errno));
close(epoll_fd);
return -1;
}
while (1) {
num = epoll_wait(epoll_fd, events_array, 10, 10000);
if(num < 0) {
printf("epoll_wait failure:%s\n", strerror(errno));
close(epoll_fd);
break;
} else if(num == 0) {
printf("eopll_wait timeout!\n");
continue;
}
for (i = 0; i < num; i++) {
if(events_array[i].events == EPOLLIN) {
ret = recvfrom(socket_fd, buf, sizeof(buf), 0,
(struct sockaddr *)&client_addr, &addr_len);
if (ret > 0) {
printf("recv len:%d, data:[%s] from %s:%d\n", ret, buf,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
} else if (0 == ret) {
printf("ret:%d\n", ret);
} else {
if (EAGAIN == errno) {
printf("ret:%d, errno:%d, %s\n", ret, errno, strerror(errno));
} else {
printf("ret:%d, errno:%d, %s\n", ret, errno, strerror(errno));
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events_array[i].data.fd, NULL);
}
}
}
}
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
server_udp();
return 0;
}
client.c:文章来源:https://www.toymoban.com/news/detail-732900.html
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9999
#define CLIENT_IP "127.0.0.1"
#define CLIENT_PORT 10000
int client_udp(char *data)
{
int ret = 0;
int socket_fd = -1;
int addr_len = 0;
char buf[1024] = {0};
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("%s: socket failed\n", __FUNCTION__);
return 0;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(CLIENT_PORT);
client_addr.sin_addr.s_addr = inet_addr(CLIENT_IP);
addr_len = sizeof(client_addr);
ret = bind(socket_fd, (const struct sockaddr *)&client_addr, addr_len);
if (ret < 0) {
printf("%s: bind failed\n", __FUNCTION__);
close(socket_fd);
return 0;
}
addr_len = sizeof(server_addr);
ret = sendto(socket_fd, data, strlen(data), 0,
(struct sockaddr *)&server_addr, addr_len);
if (ret > 0) {
printf("send data: [%s] to %s:%d\n", data, inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port));
} else {
printf("ret:%d\n", ret);
}
close(socket_fd);
return 0;
}
int main(int argc, char *argv[])
{
if (argc < 2) {
client_udp("hello world");
} else {
client_udp(argv[1]);
}
return 0;
}
测试 :文章来源地址https://www.toymoban.com/news/detail-732900.html
$ ./client 1
send data: [1] to 127.0.0.1:9999
$ ./client 2
send data: [2] to 127.0.0.1:9999
$ ./client 3
send data: [3] to 127.0.0.1:9999
$ ./client
send data: [hello world] to 127.0.0.1:9999
$ ./server
recv len:1, data:[1] from 127.0.0.1:10000
recv len:1, data:[2] from 127.0.0.1:10000
recv len:1, data:[3] from 127.0.0.1:10000
recv len:11, data:[hello world] from 127.0.0.1:10000
eopll_wait timeout!
四、关键说明
- UDP 是无连接的,即发送数据之前不需要像TCP那样建立连接,因此减少了建立连接带来的开销和发送数据的时延
- UDP 数据是面向报文传输的。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界,因此不涉及到粘包问题。不过应用程序必须选择合适大小的报文。
- UDP 没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。
- UDP适用于实时性要求很高,并且几乎不能容忍重传的应用场景,比如直播
- UDP适用于应用程序对传输的可靠性要求不高,但是对传输速度和延迟要求较高的场景。UDP适合于实时数据传输,如VoIP中的语音和视频通信,因为这种场景在偶尔丢失一两个或少量数据包时,对接收方也不会造成太大的影响
到了这里,关于Linux c编程之UDP通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!