Linux c编程之UDP通信

一、说明

  UDP(User Datagram Protocol),由RFC 768规范定义,中文名为用户数据报协议。UDP 为应用程序提供了一种无需建立连接就可以发送网络数据包的方法。
  UDP是常用的网络传输协议之一,该协议是无连接、不可靠、面向数据报的协议。在Linux C网络程序中广泛使用,如音、视频媒体数据传输、DNS协议、SIP协议等。
  UDP通信分为客户端和服务端,其中服务端在指定的网络端口上读取数据,客户端将数据发给服务端绑定的网络端口,无需建立连接即可通信,反过来,服务端向客户端发送数据也是一样。

二、常用API介绍

2.1 socket()

   #include 
   #include 
   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 
#include 
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 
#include 
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 
#include 
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 
int close(int fd);

作用:
  关闭一个(文件/socket)描述符
参数说明:
  fd: 描述符

三、UDP实例分析

3.1 基本UDP通信示例

server_udp.c:

#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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:

#include 
#include 
#include 
#include 
#include 
#include 

#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;
}

测试 :

$ ./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,linux,c语言,sendto,recvfrom)