第4章 使用UDP套接字

    UDP(User Datagram Protocol,用户数据报协议)提供了比TCP更简单的端到端服务。

    UDP只执行两种功能:(1)向IP层添加了另一个寻址(端口)层;(2)它会检测传输中可能发生的数据损坏,并丢弃任何损坏的数据报

    UDP套接字使用前不进行连接,它会保留消息边界。UDP提供的端到端服务是一种“尽力而为”的服务:不保证通过UDP套接字发送的消息将会到达其目的地。这意味着UDP套接字程序必须准备处理消息的丢失和重排

   

4.1 UDP客户

TCP的区别

(1) 不会调用connect

(2) 使用sentdto和recvfrom发送消息,而不是sent和recv

(3) 只要执行一次接受,UDP套接字会保留消息边界

UDP客户只与UDP服务器通信。

 

UDP应答服务器简单的把接受的任何消息发送回它们的任何源发地。

UDP应答客户端:

(1) 把应答字符串发送给服务器

(2) 接受应答

(3)关闭程序

 

UDP应答客户端udp_echo_client.c程序代码:

#include "practical.h"
#define MAXSTRLEN 1000 int main(int argc, char **argv) { if (argc < 3 || argc > 4) { err_quit("Usage: a.exe <Server Address/Name> <Echo Word> [<Server Prot>/Service]"); } char *server = argv[1]; char *echo_str = argv[2]; size_t echo_strlen = strlen(echo_str); if (echo_strlen > MAXSTRLEN) { err_quit("%s string too long", echo_str); } char *service = (argc == 4) ? argv[3] : "echo"; struct addrinfo hint; bzero(&hint, sizeof(hint)); hint.ai_family = AF_UNSPEC; hint.ai_socktype = SOCK_DGRAM; hint.ai_protocol = IPPROTO_UDP; struct addrinfo *servaddr; int rtnval = getaddrinfo(server, service, &hint, &servaddr); if (rtnval != 0) { err_quit("getaddrinfo() failed:%s", gai_strerror(rtnval)); } int sock = socket(servaddr->ai_family, servaddr->ai_socktype, servaddr->ai_protocol); if (sock < 0) { err_sys("socket() failed"); } ssize_t nbytes = sendto(sock, echo_str, echo_strlen, 0, servaddr->ai_addr, servaddr->ai_addrlen); if (nbytes < 0) { err_sys("sendto() failed"); } else if (nbytes != echo_strlen) { err_quit("sentto() error: sent unexpcted number of bytes"); } struct sockaddr_storage fromaddr; size_t fromaddrlen = sizeof(fromaddr); char buf[MAXSTRLEN + 1]; nbytes = recvfrom(sock, buf, MAXSTRLEN, 0, (struct sockaddr*)&fromaddr, &fromaddrlen); if (nbytes < 0) { err_sys("recvfrom() failed"); } else if (nbytes != echo_strlen) { err_quit("recvfrom() error: reveived unexpected number of bytes"); } if (!sockaddr_equal(servaddr->ai_addr, (struct sockaddr*)&fromaddr)) { err_quit("recvform(): received a packet form unknown source"); } freeaddrinfo(servaddr); buf[echo_strlen] = ''; printf("Received: %s ", buf); close(sock); exit(0); }

int sockaddr_equal(struct sockaddr *desaddr, struct sockaddr *srcaddr)


{


    return 1;


}


    这个程序大多数时间会正常工作,但是不适用于生产应用,因为发送到服务器或者从服务器发出的消息丢失,对recvfrom的调用会永远阻塞,并且程序不会终止。客户一般使用超时(timeout)来处理这个问题。

 

4.2 UDP服务器

程序永远循环,接收一条消息,然后把相同的消息发送回它的任何源发地。服务器只会接受并发送回消息的前255个字符,任何多余的字符都会被套接字实现悄悄的丢弃。

UDP服务器udp_echo_server.c程序代码:

socket(), bind(),  不需要listen(), sento(), recvfrom()。

#include "practical.h"

#define MAXSTRLEN    1000

int main(int argc, char **argv)
{
    if (argc != 2) {
        err_quit("Usage: a.exe <Server Port/Service>");
    }

    char *service = argv[1];

    struct addrinfo hint;
    memset(&hint, 0, sizeof(hint));
    hint.ai_flags    = AI_PASSIVE;
    hint.ai_family    = AF_UNSPEC;
    hint.ai_socktype = SOCK_DGRAM;
    hint.ai_protocol = IPPROTO_UDP;

    struct addrinfo *servaddr;
    int rtnval = getaddrinfo(NULL, service, &hint, &servaddr);
    if (rtnval != 0) {
        err_quit("getaddrinfo() failed: %s", gai_strerror(rtnval));
    }

    int sock = socket(servaddr->ai_family, servaddr->ai_socktype, servaddr->ai_protocol);
    if (sock < 0) {
        err_sys("socket() failed");
    }

    if (bind(sock, servaddr->ai_addr, servaddr->ai_addrlen)) {
        err_sys("bind() failed");
    }
    freeaddrinfo(servaddr);

    for(;;) {
        struct sockaddr_storage clntaddr;
        socklen_t clntaddrlen = sizeof(clntaddr);
        
        char buf[MAXSTRLEN];
        ssize_t nbytes = recvfrom(sock, buf, MAXSTRLEN, 0,
                                  (struct sockaddr*)&clntaddr, &clntaddrlen);
        if (nbytes < 0) {
            err_sys("recvfrom() failed");
        }

        fputs("Handle client: ", stdout);
        print_addrinfo((struct sockaddr*)&clntaddr, stdout);
        fputc('
', stdout);
    
        ssize_t numbytes = sendto(sock, buf, nbytes, 0, 
                                  (struct sockaddr*)&clntaddr, sizeof(clntaddr));
        if (numbytes < 0) {
            err_sys("sendto() failed");
        } else if (numbytes != nbytes) {
            err_sys("sendto(): sent unexpected number of bytes");
        }
    } //end for(;;)
}

4.3 使用UDP套接字发送和接收

    一旦创建了UDP套接字,就可以使用它向任何地址发送消息接收来自任何地址的消息,以及接连向许多不同的地址发送消息和接收来自不同地址的消息。

 

    #include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t nbytes,int flags,

const struct sockaddr *destaddr, socklen_t destlen);

               返回值:成功返回发送的字节数,出错返回-1

    函数sendto类似send,不过sendto允许在无连接的套接字上指定一个目标地址

对于面向连接的套接字,目的地址忽略,因为目的地址蕴含在连接中。对于

无连接的套接字不能使用send,除非调用connect时预设了目的地址,或者采用sendto来提供另一种发送报文方式。

   

    可以使用不止一个选择来通过套接字发送数据。可以调用带有msghdr结构的sendmsg来指定多重缓冲区传输数据,和writev类似 

    #include<sys/socket.h>

    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

                     返回值:成功返回发送字节数,出错返回-1

    msghdr至少有下列成员:

struct msghdr {

void       *msg_name;        /* optional address */

socklen_t  msg_namelen;     /* address size in bytes */

struct     *msg_iov;         /* array of I/O buffers */

int        msg_iovlen;      /* number of element in array */

void       *msg_control;     /* ancillary data */

socklen_t   msg_controllen; /* number of ancillary bytes */

int         msg_flags;       /* flags for reveived message */
};

 

    使用recvfrom来得到数据发送者的源地址

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,

struct sockaddr *restrict addr,

socklen_t *restrict addrlen);

返回值:以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,出错返回-1

若addr非空,它包含数据发送者的套接字端点地址。需要设置addrlen参数指向一个包含addr所指的套接字缓冲区字节大小的整数。返回时,该整数设为该地址的实际字节大小。

    Addrlen是一个输入/输出型参数:输入时指定地址缓冲区addr的大小,在与IP版本无关的代码中通常是struct sockaddr_storage。输出时,指向实际复制到缓冲区的地址的大小。

    通常易犯的错误:(1) 给addrlen传递一个整数而不是一个指针。(2)忘记初始化所指向的长度变量以包含合适的大小

    由于可以获得发送者的地址,revcfrom通常用于无连接的套接字否则就等同于recv

 

    UDP不会保留消息边界,每次调用recvform都会返回至多一个sendto调用的数据。不同的recvfrom永远不会返回来自不同的sendto调用的数据(除非设置MSG_PEEK标志);

    send()返回时,调用者知道数据已经复制到缓冲区中以进行传输。数据可能会也可能不会实际的进行传输。但是UDP不会缓冲数据已进行可能的重传,因为它不会从错误中恢复。意味着UDP调用sendto()调用返回时,就已经把消息传递给底层信道以进行传输,并且已经(或很快将要)发送出去

 

    在消息从网络到达的时间与通过recv()或recvfrom()返回其数据的时间之间,将把数据存储在先进先出(first-in,first-out,FIFO)接受缓冲区中。 

已连接的TCP套接字中,所有已接受但尚未递送的字节都视作一个连续的字节序列。

不过UDP套接字,来自不同消息的字节可能来自不同的发送者,缓冲区包含 “数据块”的FIFO序列,每个数据块都具有一个关联的源地址。调用recvfrom永远不会返回多个数据块。但是若缓冲区大小为n,并且接受FIFO中的第一个数据块的大小大于n时,则只会返回数据块的前n个字节。剩余的字节将悄悄的被丢弃,而不会向接受程序指示这一点

因此调用者要为recvfrom提供较大的缓冲区,使之能够存放程序协议允许的最大消息,这种技术保证不会丢失数据。UDP套接字可以返回最大数据量65507

MSG_PEEK标志:导致接收的数据保留在套接字的FIFO中,使得它可以被接受多次。适用于:内存不足,应用程序的消息大小差别很大,并且每条消息的前几个字节携带了其大小信息。

接收者可以利用MSG_PEEK和较小的缓冲区调用recvfrom,检测消息的前几个字节确定大小,然后利用足以存放整个消息的较大的缓冲区再次调用recvfrom(不使用MSG_PEEK);

内存足够时,使用足够大缓冲区是个好方法

 

4.4 连接UDP套接字

    UDP套接字可以调用connect()固定通过套接字发送的进来数据报的地址。一旦连接就可以使用send代替sendto传输数据,因为不需要指定目的地址。可以用类似的方法使用recv代替recvfrom,因为连接的UDP套接字只能接收来自关联的外部地址和端口的数据报,调用connect之后,就会知道任何进入的数据报的源地址。

    注意:使用UDP连接后,调用send和recv不会改变UDP的行为方式,消息边界仍会保留,数据报可能会丢失等等。可以利用AF_UNSPEC的地址族调用connect断开连接。

    UDP连接套接字的优点:它允许接收由套接字上以前的动作产生的错误指示。

原文地址:https://www.cnblogs.com/hancm/p/3668359.html