《UNIX网络编程:套接字联网API》啃书笔记(第8UDP套接字编程、11章地址转换)

时间:2021-11-07 11:11:18

基本UDP套接字编程

下图为UDP客户/服务器程序的函数调用:
《UNIX网络编程:套接字联网API》啃书笔记(第8UDP套接字编程、11章地址转换)

注意客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地的地址作为参数。类似的,服务器不接受来自客户的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将与所接收的数据报一道返回客户的协议地址,因此服务器可以把响应发送给正确的客户。

recvfrom和sendto函数

#include<sys/socket.h>
ssize_t recvfrom(int sockfd,void *buff,size_t nbytes,int flags,struct sockaddr *from,soclen_t *addrlen);
ssize_t sendto(int sockfd,const void *buff,size_t nbytes,int flags, const struct sockaddr *to,socklen_t addrlen);

若成功则返回读或写的字节数,若出错则为-1。

  1. 前三个参数sockfd、buff、nbytes表示描述符、指向读入或写出缓冲区的指针和读写字节数。
  2. flags参数以后再谈,此时置为0。
  3. sendto的to参数指向一个含有数据报接收者的协议地址的套接字地址结构,其大小由addrlen参数指定。
  4. recvfrom的from参数指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构,而该套接字地址结构中填写的字节数则放在addrlen参数所指的整数中返回给调用者。

写一个长度为0的数据报是可行的。在UDP情况下,这会形成一个只baoh一个IP首部和一个8字节UDP首部而没有数据的IP数据报。这也就是说对于数据报协议,recvfrom返回值0是可接受的。

若recvfrom的from参数是一个空指针,那么相应的长度参数(addrlen)也必须是一个空指针,表示我们并不关心数据发送者的协议地址。

UDP回射服务器程序:

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

    sockfd=socket(AF_INET,SOKC_DGRAM,0);                  //创建一个IPv4的UDP套接字

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

    bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));

    dg_ehco(sockfd,(struct sockaddr *)&cliaddr,sizeof(cliaddr)); //服务器处理工作
}
void dg_echo(int sockfd,struct sockaddr *pcliaddr,socklen_t clilen){
    int n;
    socklen_t len;
    char mesg[MAXLINE];

    while(true){
        len=clilen;
        n=recvfrom(sockfd,mesg,MAXLINE,0,pcliaddr,&len);  //接收客户端发来的消息

        sendto(sockfd,mesg,n,0,pcliaddr,len);             //回送消息
    }
}

由于UDP是一个无连接的协议,没有EOF之类的终止符,所以该函数永不终止。其次该函数提供的是一个迭代服务器,单个服务器进程就得处理所有客户。一般来说,大多数TCP服务器是并发的,大多数UDP服务器是迭代的。

对于本套接字,UDP层中隐含有排队发生。事实上每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区。当进程调用recvfrom时,缓冲区中的下一个数据报以先入先出顺序返回给进程。

UDP回射客户程序

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(9877);
    inet_pton(AF_INET,argv[1],&servaddr.sin_addr);

    sockfd=socket(AF_INET,SOCK_DGRAM,0);
    //客户处理工作
    dg_cli(stdin,sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    exit(0);
}
void dg_cli(FIFE *fp,int sockfd,const struct sockaddr *pservaddr,socklen_t servlen){
    int n;
    char sendline[MAXLINE],recvline[MAXLINE+1];

    while(fgets(sendline,MAXLINE,fp)!=NULL){        //从fp读入数据到发送缓冲区
        sendto(sockfd,sendline,strlen(sendline),0,pservaddr,servlen);//从发送缓冲区发送数据

        n=recvfrom(sockfd,recvline,MAXLINE,0,NULL,NULL);//接收数据到接收缓冲区
        recvline[n]=0;                              //文本结束符
        fputs(recvline,stdout);
    }
}

UDP客户/服务器是不可靠的,若一个客户数据报丢失或服务器应答丢失,客户将永远阻塞与recvfrom调用。

对于一个UDP端口,客户必须给sendto调用指定服务器的IP地址和端口号,但若其进程首次调用sendto时它没有绑定一个本地端口,那么内核就在此时为它选择一个临时端口。客户的临时端口是在第一次调用sendto时一次性选定,不能改变,然而客户的IP地址却可以随客户发送的每个UDP数据报而变动。

另外调用recvfrom指定的第五和第六个参数是空指针,这告知内核我们并不关心应答数据报由谁发送。

这样的风险在于任何进程不论是在于本客户进程相同的主机上还是不同的主机上,都可以向本客户的IP地址和端口发送数据报,这些数据报将被客户读入并被认为是服务器的应答。

服务器可从到达的IP数据报中获取的信息:
《UNIX网络编程:套接字联网API》啃书笔记(第8UDP套接字编程、11章地址转换)

UDP的connect函数
给UDP套接字调用connect时,没有三路握手过程,内核只是检查是否存在立即可知的错误,记录对端的IP地址和端口号,然后立即返回到调用进程。

我们称对UDP套接字调用connect之后的套接字为已连接UDP套接字,之前的为未连接UDP套接字。对于已连接套接字:

  1. 不能给输出操作指定目的IP地址和端口号,写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址,也就是说改用write或send,但仍可使用sendto,只是不能指定目的地址,即第5、6个参数须为NULL和0。
  2. 不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在已连接套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。目的地为这个已连接UDP套接字的本地协议地址,发源地却不是该套接字早先connect到的协议地址的数据报,不会投递到该套接字,可能投递到其它某个UDP套接字,若无匹配的则丢弃。也就是说一个已连接套接字仅仅与一个IP地址交换数据报。
  3. 由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。

性能比较:

  • 在一个未连接的UDP套接字上给两个数据报调用sendto函数时,内核执行下列6个步骤:连接->输出第一个->断开->连接->输出第二个->断开。也就是说内核每发送一个数据报都需要重新连接断开套接字,并且需要复制若干次含有目的IP地址和端口号的套接字地址结构。
  • 但当进程知道自己要给同一目的地址发送多个数据报时,显式连接套接字效率更高,调用connect后调用两次write时内核执行:连接->发送第一个->发送第二个…。此时内核只负责一次,节省了约三分之一的开销。

再次调用connect:
对于TCP套接字,connect只能调用一次。但当我们需要指定新的IP地址和端口号或断开UDP套接字的时候可以再次调用connect。为了断开一个已连接UDP套接字,再次调用connect时把套接字地址结构的地址族成员设置为AF_UNSPEC。

connect版dg_cli函数:

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

    connect(sockfd,(struct sockaddr *)pservaddr,servlen); //调用connect创建已连接套接字

    while(fgets(sendline,MAXLINE,fp)!=NULL){              //读入数据到发送缓冲区
        write(sockfd,sendline,strlen(sendline));          //发送缓冲区中的数据送入套接字,write代替sendto

        n=read(sockfd,recvline,MAXLINE);                  //从套接字中读入数据到接收缓冲区,read代替recvfrom
        recvline[n]=0;
        fputs(recvline,stdout);
    }
}

名字与地址转换

域名系统主要用于主机名字与IP地址之间的映射。主机名既可以是一个简单名字(simple name)如example,也可以是一个全限定域名例如example.com。

资源记录
DNS中的条目称为资源记录(resource record,RR):

  1. A: A记录把一个主机名映射成一个32位的IPv4地址。
  2. AAAA: 称为“四A”记录的AAAA记录把一个主机名映射成一个128位的IPv6地址。
  3. PTR: 称为“指针记录” 的PTR记录把IP地址映射成主机名。对于IPv4,32位地址的4个字节先反转顺序,每个字节都转换成各自的十进制值后再添上in-addr.arpa,如A记录为12.106.32.254的PTR为254.32.106.12.in-addr.arpa。对于IPv6,位每个四元组转换成相应十六进制后添加.ip6.arpa。
  4. MX: MX记录把一个主机指定作为给定主机的“邮件交换器”。当存在多个MX记录时,它们按照优先级顺序使用,值越小优先级越高。
  5. CNAME:CNAME代表“canonical name”(规范名字),它的常见用法是为常用的服务(如ftp和www)指派CNAME记录。

gethostbyname函数
该函数用于把主机名映射成IPv4地址,如果调用成功,它就返回一个指向hostent结构的指针,该结构中含有所查找主机的所有IPv4地址。

#include<netdb.h>
struct hostent *gethostbyname(const char *hostname);

若成功,本函数返回的非空指针指向如下 的hostent结构:

struct hostent{
    char *h_name;            //正式主机名,即所查询主机的规范名字即example.com
    char **h_aliases;        //别名数组,以NULL结束
    int h_addrtype;          //IP地址类型:AF_INET
    int h_length;            //地址长度:4
    char **h_addr_list;      //IPv4地址数组,以NULL结束
};

gethostbyname执行的是对A记录的查询,它只能返回IPv4地址

若出错则为NULL,该函数不设置errno变量而是将全局整数变量h_errno设置为在头文件

#include<netdb.h>
struct hostent *gethostbyaddr(const char *addr,socklen_t len,int family);

若成功则返回非空指针,若出错则为NULL且设置h_errno。

addr参数实际上不是char*类型而是一个指向存放IPv4地址的某个in_addr结构的指针,len参数是这个结构的大小:对于IPv4为4,family参数为AF_INET。

gethostbyaddr函数在in_addr.arpa域中向一个名字服务器查询PTR记录。

getservbyname函数
该函数根据给定名字查找相应服务。

#include<netdb.h>
struct servent *getservbyname(const char *servname,const char *protoname);

若成功返回非空指针,若出错则为NULL。返回的非空指针指向如下的servent结构:

struct servent{
    char *s_name;           //规范服务器名即example.com
    char **s_aliases;       //别名数组
    int s_port;             //网络字节序的端口号,与sin_port的格式相同
};

服务名参数servname必须指定,如果同时指定了协议(即protoname参数非空指针,为”udp”或”tcp”),那么指定服务必须有匹配的协议。若protoname未指定而servname指定服务支持多个协议,那么返回哪个端口号取决于实现。

getservbyport函数
该函数根据给定端口号和可选协议查找相应服务。

#include<netdb.h>
struct servent *getservbyport(int port,const char *protoname);

若成功则返回非空指针,若出错则为NULL。

port参数的值必须为网络字节序,需htons将int的端口号转换成。

getaddrinfo函数
该函数支持IPv6,能够处理名字到地址以及服务到端口这两种转换,返回的是一个sockaddr结构而不是一个地址列表。

#include<netdb.h>
int getaddrinfo(const char *hostname,const char *service,const struct addrinfo *hint,struct addrinfo **result);

若成功则返回0,若出错则为非0。

本函数通过result指针参数返回一个指向addrinfo结构链表指针,其结构定义如下:

struct addrinfo{
    int ai_flags;              //标志值及其含义
    int ai_family;             //AF_INET或AF_INET6
    int ai_socktype;           //SOCK_STREAM或SOCK_DGRAM
    int ai_protocol;           //0或IPPROTO_TCP或IPPROTO_UDP
    socklen_t ai_addrlen;      //ad_addr的长度为4或16
    char *ai_canonname;        //规范名即example.com
    struct sockaddr *ai_addr;  //套接字地址结构
    struct addrinfo *ai_next;  //链表的下一个节点
};
  1. hostname参数是一个主机名或地址串(IPv4的点分十进制或IPv6的十六制)
  2. service参数是一个服务名或十进制端口号数串
  3. hints参数可以是一个空指针,也可以是一个指向某个addrinfo结构的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示,使得返回的仅仅是适用于数据报套接字的信息。

hint结构中调用者可以设置的成员有:

  • ai_flags:零个或多个或在一起的AI_XXX值
  • ai_family:地址族AF_XXX
  • ai_socktype:套接字类型SOCK_XXX
  • ai_protocol:协议类型IPPROTO_XXX

其中ai_flags成员可用的标志值及其含义如下:
《UNIX网络编程:套接字联网API》啃书笔记(第8UDP套接字编程、11章地址转换)

若hints参数是一个空指针,默认ai_flag、ai_socktype和ai_protocol的值为0,ai_family的值为AF_UNSPEC。

若本函数返回成功0,那么由result参数指向的变量已被填入一个指针,它指向的是由其中的ai_next成员串接起来的addrinfo结构链表。可导致返回多个addrinfo结构情形如下:

  1. 若与hostname参数关联的地址有多个,那么适用于所请求地址族的每个地址都返回一个对应的结构。
  2. 若service参数指定的服务支持多个套接字类型,那么每个套接字类型都可能返回一个对应的结构,具体取决于hints结构的ai_socktype成员。

例如在没有提供任何暗示信息的前提下,请求查找有两个IP地址的某个主机上的domain服务将返回4个addrinfo结构:

  • 第一个IP地址+SOCK_STREAM
  • 第一个IP地址+SOCK_DGRAM
  • 第二个IP地址+SOCK_STREAM
  • 第二个IP地址+SOCK_DGRAM

当有多个addrinfo结构返回时,这些结构的先后顺序没有保证。

在addrinfo结构中返回的信息可现成用于socket调用,随后现成用于适用客户的connect或sendto调用,或者是适合服务器的bind调用。若在hints结构中设置了AI_CANONNAME标志,那么本函数返回的第一个addrinfo结构的ai_canonname成员指向所查找主机的规范名字。

只有在未提供ai_socktype暗示信息时才可能为每个IP地址返回多个addrinfo结构,此时或者服务以名字标识并且同时支持TCP和UDP,或者服务以端口号标识。

freeaddrinfo函数
注意由getaddrinfo返回的所有存储空间都是动态获取的,包括addrinfo结构、ai_addr结构和ai_canonname字符串。这些存储空间通过调用freeaddrinfo返还给系统。

#include<netdb.h>
void freeaddrinfo(struct addrinfo *ai);

ai参数指向由getaddrinfo返回的第一个addrinfo结构。这个链表中的所有结构以及它们指向的任何动态存储空间都被释放掉。

gai_strerrot函数
该函数返回一个对应于getaddrinfo返回的非0错误值的出错信息串的指针。

#include<netdb.h>
const char *gai_strerror(int error);

下图为getaddrinfo返回的非0错误常值:
《UNIX网络编程:套接字联网API》啃书笔记(第8UDP套接字编程、11章地址转换)

getnameinfo函数
该函数以一个套接字地址为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串。

#include<netdb.h>
int getnameinfo(const struct sockaddr *sockaddr,socklen_t addrlen,
                char *host,socklen_t hostlen,
                char *serv,socklen_t servlen,int flags);

若成功则返回0,出错则返回非0(见上图)。

sockaddr指向一个套接字地址结构,其中包含待转换成直观可读的字符串的协议地址,addrlen是这个结构的长度。

待返回的2个直观可读字符串由调用者预先分配存储空间,host和hostlen指定主机字符串,serv和servlen指定服务字符串。如果调用者不想返回主机字符串,那就指定hostlen为0,同样把servlen指定为0就是不想返回服务字符串。

flags的值及其含义如下:
《UNIX网络编程:套接字联网API》啃书笔记(第8UDP套接字编程、11章地址转换)

  1. 注意在套接字地址结构中给出的仅仅是IP地址和端口号,getnameinfo无法就此确定所用协议(TCP或UDP),故而可以使用NI_DGRAM标志指定数据报套接字。
  2. NI_NOFQDN标志导致返回的主机名第一个点号之后的内容被截去。
  3. NI_NUMBERICHOST标志告知getnameinfo不要调用DNS(因为调用DNS可能耗时),而是以数值表达格式以字符串的形式返回IP地址。
  4. NI_NUMBERICSERV标志指定以十进制数格式作为字符串返回端口号,以代替查找服务名。由于客户一般是临时端口,故而服务器通常应该设置此标志。
  5. NI_NUMBERICSCOPE标志指定以数值格式作为字符串返回范围标识以代替其名字。