基本UDP套接字编程

UDP 概述

流程图

recvfrom 和 sendto

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
                struct sockaddr * from, socklen_t * addrlen);

ssize_t sendto(int sockfd, const void * buff, size_t nbytes, int flags,
               const struct sockaddr *to, socklen_t addrlen);

参数说明

  • 前三个参数等同于 read 和 write 的三个参数:描述符、指向读或写缓冲区的指针、读写字节数
  • sendto 的 to 参数指向一个含有数据报接收者的协议地址的套接字,其长度由 addrlen 指定
  • recvfrom 的 from 参数指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构,注意长度是一个指针

注意

  • 长度为 0 的数据报是可以发送,也可以被接收到的
  • 如果 recvfrom 的 from 参数是一个空指针,那么相应的长度参数也必须是空指针,表示我们不关心发送者的协议地址
  • recvfrom 和 sendto 也可以用于 TCP,尽管通常没有理由这么做

程序代码

graph LR;A[标准输入/输出] --fgets--> B[UDP-Client] --sendto/recvfrom--> C[UDP-Server] C --recvfrom/sendto--> B --fputs--> A

服务器端

udpserv01.c

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));

    dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}

dg_echo.c

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen) {
  int n;
  socklen_t len;
  char mesg[MAXLINE];
  
  for (;;) {
    len = clilen;
    n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &clilen);
    Sendto(sockfd, mesg, n, 0, pcliaddr, len);
  }
}

服务模型迭代服务器

graph LR;subgraph 缓冲FIFO 数据报3 数据报2 数据报1 endsubgraph Client CliSocket --发送一个数据报--> 数据报3 endsubgraph UDPServer 数据报1 --从缓冲中取出数据报--> ServSocket end

因为没有连接的概念,无需维持状态,所以是来一个消费一个,有点像消费队列的感觉。

客户端

udpcli01.c

#include "unp.h"

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        err_quit("usage: udpcli <IPaddress>");

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr));

    exit(0);
}

dg_cli.c

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) {
  int n;
  char sendline[MAXLINE], recvline[MAXLINE + 1];
  while (Fgets(sendline, MAxLINE, fp) != NULL) {
    Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
    n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);
    recvline[n] = 0;
    Fputs(recvline, stdout);
  }
}

对于客户端而言,什么时候指派一个套接字:

  • TCP 下,connect 调用时进行绑定
  • UDP 下,首次调用 sendto 时绑定

当前的问题

数据报的丢失

如果客户端发送的请求在去或回来的途中丢失了,那么客户端将会永远阻塞于 Recvfrom

简单的解决方法是为该方法设置一个超时,但是这种方法不能判断是发送的时候丢失了还是返回的途中丢失了,因此不具备可靠性。

接收到非目标的响应

问题

前文的代码中,由于如下语句:

n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);

后两个参数为 NULL,则任何知道客户端临时端口的进程都能向客户发送数据报,与正常的服务器应答混淆。

解决方法

创建一个变量,接收发送方的协议地址,然后和我们期望的进行对比

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
    int n;
    char sendline[MAXLINE], recvline[MAXLINE + 1];
    socklen_t len;
    struct sockaddr *preply_addr;

    preply_addr = Malloc(servlen);

    while (Fgets(sendline, MAXLINE, fp) != NULL)
    {

        Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

        len = servlen;
        n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
        if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0)
        {
            printf("reply from %s (ignored)\n",
                   Sock_ntop(preply_addr, len));
            continue;
        }

        recvline[n] = 0; /* null terminate */
        Fputs(recvline, stdout);
    }
}

仍然存在缺陷

在多宿服务器上,即一台主机有多个接口,也即有多个 IP 地址,有可能接收到的消息是从另一个接口发出的,从而被我们意外地拦截。

强端系统模型、弱端系统模型

解决方法:

  • 客户通过在 DNS 中查找服务器主机的名字来验证该主机的域名(而不是 IP 地址)
  • 给服务器的所有 IP 地址创建一个套接字,用 select 监听他们(后面有例子)

服务器未运行

从客户端的程序上来看,如果服务器没有运行,那么程序就会阻塞在 recvfrom 方法上。

通过 tcpdump 可以看到如下内容:

  • 确实有 ICMP 的错误信息表示该协议地址 UNREACHABLE,但是这个错误不会返回给客户端

  • 我们称这个 ICMP 错误为 异步错误 ,该错误由 sendto 引起,但是 sendto 本身却成功返回

  • 一个基本规则是,除非它已连接,否则其引发的异步错误并不返回给它

  • 后文将给出一个使用自己的守护进程获取未连接套接字上这些错误的简便方法

connect

简介

和 TCP 的 connect 相比:

UDP 调用 connect 的话:

  • 仍然不会有三次我偶在的过程
  • 内核只是检查是否存在立即可知的错误
  • 以及记录对端的 IP 地址和端口号

和没有 connect 的 UDP 相比:

  • 不能给输出操作指定目的 IP 和端口号了,即不使用 sendto 而是使用 write/send,写入内容自动发送到指定的协议地址

  • 不必使用 recvfrom 获悉数据报的发送者,而改用 readrecvrecvmsg 。内核会过滤掉其他来源的数据报。

  • 由已连接 UDP 套接字引发的异步错误会返回给他们所在的进程

多次调用 connect

两个作用

  • 指定新的协议地址:和 TCP 的 connect 只能调用一次不同,UDP 可以多次调用以切换不同的协议地址
  • 断开套接字:将地址簇设为 AF_UNSPEC

性能

当要给同一目的地址发送多个数据报时,显式连接套接字效率更高

graph LR;subgraph 建立连接 A(连接套接字) --> A1(发送1) --> A2(发送2) --> A3(断开连接) endsubgraph 没有建立连接 B(连接套接字) --> B1(发送) --> B2(断开套接字) -.- B3(连接套接字) --> B4(发送) --> B5(断开套接字) end

改写 dg_cli

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
    int n;
    char sendline[MAXLINE], recvline[MAXLINE + 1];

    Connect(sockfd, (SA *)pservaddr, servlen);

    while (Fgets(sendline, MAXLINE, fp) != NULL)
    {

        Write(sockfd, sendline, strlen(sendline));

        n = Read(sockfd, recvline, MAXLINE);

        recvline[n] = 0; /* null terminate */
        Fputs(recvline, stdout);
    }
}

此时当我们建立连接时并不会报错,在我们用 Write 发送数据时会发现网络不可达的错误

流量控制

如果发送方的能力很强,且不断发数据报,而接收方处理的速度比较慢,则有可能将接收方的缓冲区冲垮

可以通过如下命令查看 UDP 数据的统计信息

netstat -s -p -u

可以通过如下方式增加缓冲区的大小:

n = 220 * 1024;
Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

UDP 外出端口的确定

通过 connect 应用到 UDP 的一个副作用来获取:

int main(int argc, char **argv)
{
    int sockfd;
    socklen_t len;
    struct sockaddr_in cliaddr, servaddr;

    if (argc != 2)
        err_quit("usage: udpcli <IPaddress>");

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));

    len = sizeof(cliaddr);
    Getsockname(sockfd, (SA *)&cliaddr, &len);
    printf("local address %s\n", Sock_ntop((SA *)&cliaddr, len));

    exit(0);
}

select 结合 TCP 和 UDP

我们想做一个既能接收 TCP 请求,又能接收 UDP 请求的服务器。

思路是:把之前的并发 TCP 服务器和本章中的迭代 UDP 服务器结合成使用 select 来复用 TCP 和 UDP 套接字的程序。

具体来说,就是:

  • UDP 的描述符绑定 TCP 的端口
  • select 监听之前 TCP 提到的描述符,以及 UDP 的描述符
  • 在事件到达后判断是哪一种,然后触发相关的处理流程
int main(int argc, char **argv)
{
    int listenfd, connfd, udpfd, nready, maxfdp1;
    char mesg[MAXLINE];
    pid_t childpid;
    fd_set rset;
    ssize_t n;
    socklen_t len;
    const int on = 1;
    struct sockaddr_in cliaddr, servaddr;
    void sig_chld(int);

    /* 4create listening TCP socket */
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    /* 4create UDP socket */
    udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Bind(udpfd, (SA *)&servaddr, sizeof(servaddr));
    /* end udpservselect01 */

    /* include udpservselect02 */
    Signal(SIGCHLD, sig_chld); /* must call waitpid() */

    FD_ZERO(&rset);
    maxfdp1 = max(listenfd, udpfd) + 1;
    for (;;)
    {
        printf("Hello UDP!\n");
        FD_SET(listenfd, &rset);
        FD_SET(udpfd, &rset);
        if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0)
        {
            printf("NREADY, restart!\n");
            if (errno == EINTR)
                continue; /* back to for() */
            else
                err_sys("select error\n");
        }

        if (FD_ISSET(listenfd, &rset))
        {
            printf("listenfd selected!\n");
            len = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *)&cliaddr, &len);

            if ((childpid = Fork()) == 0)
            {                     /* child process */
                Close(listenfd);  /* close listening socket */
                str_echo(connfd); /* process the request */
                exit(0);
            }
            Close(connfd); /* parent closes connected socket */
        }

        if (FD_ISSET(udpfd, &rset))
        {
            printf("udpfd selected!\n");
            len = sizeof(cliaddr);
            n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len);

            Sendto(udpfd, mesg, n, 0, (SA *)&cliaddr, len);
        }
    }
}
/* end udpservselect02 */
作者:|樵仙|,原文链接: https://www.cnblogs.com/lymtics/p/16319994.html

文章推荐

SpringBoot集成支付宝 - 少走弯路就看这篇

GRPC与 ProtoBuf 的理解与总结

【Redis】常用命令介绍

服务器遭受攻击之后的常见思路

图的存储

C 语言版线程池

Bread 面包屑

结构优于制度,软件开发中的康威定律

2023-03-15 用Node.js开发一个http代理服务器

nacos初探

【clickhouse专栏】单机版的安装与验证

一文通吃:从 ZooKeeper 一致性,Leader选举讲到 ZAB