Linux 下网络编程之协议无关方法

协议无关方法

Linux 提供了很多强大的函数,实现了二进制套接字地址结构主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。若和套接字一起使用,就可以开发出独立于协议的网络程序。

0、数据结构

在一切开始之前,我们需要简单回顾表示二进制套接字地址结构的数据结构 addrinfo。因为接下来介绍的函数都与它密切相关。

struct addrinfo {
    int         ai_flags;	 	/* Input flags.  */
    int         ai_family;	 	/* Protocol family for socket.  */
    int         ai_socktype;	/* Socket type.  */
    int         ai_protocol;	/* Protocol for socket.  */
    char        *ai_cannonname;	/* Canonical name for service location.  */
    size_t      ai_addrlen;		/* Length of socket address.  */
    struct sockaddr *ai_addr;	/* Socket address for socket.  */
    struct addrinfo *ai_next;	/* Length of socket address.  */
}

addrinfo 中的变量可以直接传递到套接字接口中。其中

  • ai_family 表示选定的协议族
  • ai_socktype 表示套接字类型
  • ai_protocol 表示套接字协议类型,以上三类可直接传递给 socket() 函数做参数 。
  • ai_addr 指向套接字结构体
  • ai_addrlen 表示套接字地址的字节长度,以上两个可直接做参数传递给 connect()bind()
  • ai_next 指向列表中下一个 addrinfo 结构,该字段方便函数遍历查找。
  • ai_flags 是一个位掩码,通过如下标志定义:
标志描述
AI_ADDRCONFIG查询配置的地址类型(IPv4 或 IPv6)。如果使用连接,推荐使用该标志
AI_ALL查找 IPv4 和 IPv6 地址(仅用于AI_V4MAPPED)
AI_CANONNAME需要一个规范的名字,默认为 NULL
AI_NUMERICHOST以数字地址方式指定主机地址
AI_NUMERICSERV以数字地址方式指定数字端口号
AI_PASSIVE套接字地址用于绑定监听
AI_V4MPPED若未找到 IPv6 的地址,返回映射到IPv6 格式的 IPv4 地址

将这些二进制套接字地址结构封装完成后,编写客户端、服务端程序就无需关注于某一个 IP 协议,只要流程一致即可。

接下来介绍一下在编写协议无关程序时常用的几个 API 函数。

1、getaddrinfo 函数

简介

getaddrinfo 函数将主机名、主机地址、服务名和端口号的字符串转化为二进制套接字结构。作为 gethostbynamegetservbyname 函数的替代品,它是可重入的,适用于任何协议。

#include <sys/types.h>
#include <sys/socket.h>

int getaddrinfo(const char *host, /* host 或者IP地址 */
    const char *service, /* 十进制端口号 或者常用服务名称如"ftp"、"http"等 */
    const struct addrinfo *hints, /* 获取信息要求设置 */
    struct addrinfo **res); /* 获取信息结果 */

getaddrinfo() 函数一共有四个参数:

  • host 表示套接字地址中的 IP 地址,可以是域名,也可以是数字地址,如 IPv4 的点分十进制串。但若 hints 参数中的 ai_flags 中设置了 AI_NUMERICHOST 标志,那么该参数只能是数字地址,不能是域名。
  • service 表示端口号,但也可以用来表示服务名称,如 http 等。如果不想把主机名转换成地址,可以把 host 设置为 NULL ,对 service 来说也是一样。但是 hostservice 同时只能有一个为 NULL
  • hints 参数指向前文提到的 addrinfo 结构体,是为方便用户更好地控制返回的套接字列表。即对返回的套接字列表提出“要求”,因而用户只能设定其中 ai_familyai_socktypeai_protocolai_flags 四个域,其他域必须设置为0 或者 NULL,因此通常是先使用 memset() 初始化后再设定指定的四个域。
  • result 参数是 getaddrinfo 的返回值,它是一个指向 addrinfo 结构的链表的指针。其中每个结构指向一个对应于 hostservice 的套接字地址结构,如下图。

在这里插入图片描述
客户端调用后,会遍历该列表,依次尝试每个套接字地址,直到调用 socketconnect 成功。而服务器调用时,也会依次尝试套接字地址,直到 socketbind 成功。

此外,freeaddrinfo() 用以释放该链表。若出错,调用 gai_strerror 函数可将错误代码转为字符串。

#include <sys/types.h>
#include <sys/socket.h>

void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);

示例用法

使用 CSAPP 中例子能很好地说明该函数的用法。CSAPP 中实现了 open_listenfd 的辅助函数,用于打开和返回一个监听描述符,这个描述符准备好在端口 port 上接受连接请求。该函数是可重入的,可协议独立的。

int open_listenfd(char *port) {
  struct addrinfo hints, *listp, *p;
  int listenfd, rc, optval = 1;

  /* 通过 getaddrinfo 函数获得可能的服务地址 */
  /* 与前文说到那样,使用 hints 参数前先全部置为 0 */
  memset(&hints, 0, sizeof(struct addrinfo));
  /* Internet连接 监听端口 IPv4 使用数字地址端口 */
  hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
  hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
  hints.ai_flags |= AI_NUMERICSERV;            /* ... using port number */
  // 若执行成功 则返回 0
  if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
    fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port,
            gai_strerror(rc));
    return -2;
  }

  /* 遍历列表,找到可以bind的套接字接口 */
  for (p = listp; p; p = p->ai_next) {
    /* 使用 addrinfo 中的变量创建套接字 */
    /* 如果失败就遍历下一个 */
    if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
      continue;

    /* Eliminates "Address already in use" error from bind */
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
               (const void *)&optval, sizeof(int));

    /* 绑定对应的套接字 */
    if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
      break;
    /* 若失败就下一个 */
    if (close(listenfd) < 0) {
      fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
      return -1;
    }
  }

  /* 最后使用 freeaddrinfo函数清除列表 */
  freeaddrinfo(listp);
  if (!p)
    return -1;

  /* 监听端口开始工作,并返回 */
  if (listen(listenfd, LISTENQ) < 0) {
    close(listenfd);
    return -1;
  }
  return listenfd;
}

2、getnameinfo 函数

简介

getnameinfo 函数与 getaddrinfo 函数相反,它将一个套接字地址结构转换为相应的主机和服务名字符串。它是已弃用的 gethostbyaddrgetservbyport 的替代函数,是可重入,也是协议无关的。

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *sa, socklen_t salen, 
		char *host, size_t hostlen,
		char *service, size_t servlen, int flags); 

参数 sa 指向大小为 salen 的套接字地址结构,host 指向大小为 hostlen 的对应主机的字符串,service 指向大小为 servlen 的对应服务名字符串。而参数 flags 是一个位掩码,能够修改默认行为。同样地,错误代码处理由 gai_strerror 负责。

对于参数 host ,如果不想要主机名,可以设置为 NULLhostlen 设置为 0,同理可用在服务名,但两者不能全为 NULL
对于参数 flags,它是一个位掩码,其中下面两个值会经常使用到:

  • NI_NUMERICHOST 函数默认视图返回一个数字地址的 host 域名
  • NI_NUMERICSERV 函数会简单地返回服务的端口号

示例程序

int main(int argc, char **argv) {
  struct addrinfo hints, *listp, *p;
  int flags, rc;
  char buf[MAXLINE];

  if (argc != 2) {
  	fprintf(stderr, "usage: %s ", argv[0]);
  	exit(0);
  }
  /* 通过 getaddrinfo 函数获得可能的服务地址 */
  /* 与前文说到那样,使用 hints 参数前先全部置为 0 */
  memset(&hints, 0, sizeof(struct addrinfo));
  /* Internet连接 监听端口 IPv4 使用数字地址端口 */
  hints.ai_family = AF_INET;				   /* Use IPv4 only */
  hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
  // 若执行成功 则返回 0
  if ((rc = getaddrinfo(argv[1], port, &hints, &listp)) != 0) {
    fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port,
            gai_strerror(rc));
    exit(1);
  }

  /* 遍历列表套接字接口 */
  for (p = listp; p; p = p->ai_next) {
  	/* 使用 getnameinfo 函数来将套接字地址转换为点分十进制串 */
    getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
    printf("%s\n", buf);
  }

  /* 最后使用 freeaddrinfo 函数清除列表 */
  freeaddrinfo(listp);
  return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值