前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >套接口编程简介

套接口编程简介

原创
作者头像
jackieluo
发布2018-09-03 13:44:38
1.1K0
发布2018-09-03 13:44:38
举报
文章被收录于专栏:Jackie技术随笔

套接口编程简介

套接口地址结构

每个协议族都定义了自己的套接口地址结构,名字均以sockaddr_开头,对应协议族的标志结束。大部分套接口函数需要指向套接口地址结构的指针作为参数。

IPv4套接口地址结构

代码语言:txt
复制
/*
 * Internet address (a structure for historical reasons)
 */
struct in_addr {
   in_addr_t s_addr;              /* 32位的IPv4地址(网络字节序) */
};

/*
 * Socket address, internet style.
 */
struct sockaddr_in {
    uint8_t         sin_len;       /* 长度(固定16字节) */
    sa_family_t     sin_family;    /* AF_INET */
    in_port_t       sin_port;      /* 16位的TCP或者UDP端口号(网络字节序) */
    struct in_addr  sin_addr;      /* 32位的IPv4地址(网络字节序) */
    char            sin_zero[8];   /* 未用 */
};
计算IPv4套接口地址结构长度

注:对于结构体类型的,计算其内层数据类型

字段名

数据类型

长度

sin_len

uint8_t

8位

sin_family

uint8_t

8位

sin_port

uint16_t

16位

sin_addr

uint32_t

32位

sin_zero

char[8]

8字节

sin_len=(8+8+16+32)/8+8=16 byte

IPv4套接口地址结构
IPv4套接口地址结构
验证

intro/daytimetcpclo.c中添加一行打印套接口地址结构大小:

代码语言:txt
复制
printf("size of servaddr is %zu\n", sizeof(servaddr));

得到结果:

代码语言:txt
复制
size of servaddr is 16
3个成员

使用的时候基本只需要这个结构中的3个成员:sin_familysin_addrsin_port

代码语言:txt
复制
// intro/daytimetcpcli.c
servaddr.sin_family = AF_INET;
servaddr.sin_port   = htons(13);    /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    err_quit("inet_pton error for %s", argv[1]);

// intro/daytimetcpsrv.c
servaddr.sin_family      = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port        = htons(13);    /* daytime server */
网络字节序存储

IPv4地址和TCP或UDP端口号在套接口地址结构中总是以网络字节序来存储。

IPv6套接口地址结构

代码语言:txt
复制
/*
 * IPv6 address
 */
struct in6_addr {
    uint8_t s6_addr8[16];                /* 128位的IPv6地址 */
};

/*
 * Socket address for IPv6
 */
#if condition
#define	SIN6_LEN
#endif /* condition */

struct sockaddr_in6 {
    uint8_t            sin6_len;         /* 长度(固定24字节) */
    sa_family_t        sin6_family;      /* AF_INET6 */
    in_port_t          sin6_port;        /* 16位的TCP或者UDP端口号(网络字节序) */
    uint32_t           sin6_flowinfo;    /* 32位的IPv6流标 */
                                         /* 低24位是流量标号 */
                                         /* 下4位是优先级 */
                                         /* 再下4位保留 */
    struct in6_addr    sin6_addr;        /* 128位的IPv6地址(网络字节序) */
};
  • 如果系统支持套接口地址结构中的长度成员,则SIN6_LEN常值必须定义,例如macOS中:
代码语言:txt
复制
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define	SIN6_LEN
#endif /* (_POSIX_C_SOURCE && !_DARWIN_C_SOURCE) */
  • 结构中的成员是有序排列的,因此,如果sockaddr_in6是64位对齐的,则128位的成员sin6_addr也是64位对齐的。在一些64位处理机上,如果数据存储在64位便捷的位置,则对64位数据的访问将优化处理。
IPv6套接口地址结构
IPv6套接口地址结构

通用套接口地址结构

套接口函数,应当是协议无关的,可以处理任何支持的协议族的套接口地址结构。套接口函数是在ANSI C之前定义的,因此它没有使用通用的指针类型void *,而是定义了一个通用套接口地址结构:

代码语言:txt
复制
/*
 * [XSI] Structure used by kernel to store most addresses.
 */
struct sockaddr {
    uint8_t        sa_len;        /* total length */
    sa_family_t    sa_family;     /* [XSI] address family */
    char           sa_data[14];   /* [XSI] addr value (actually larger) */
};

于是,套接口函数使用的参数,为指向通用套接口地址结构sockaddr的指针,例如bind函数:

代码语言:txt
复制
int bind(int, const struct sockaddr *, socklen_t);

因此,在调用这些函数时,我们需要将指向特定协议的套接口地址结构的指针类型转换成指向通用套接口地址结构的指针struct sockaddr *

代码语言:txt
复制
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

4种套接口格式对比

下图是四种套接口地址结构的对比。为了处理类似Unix域结构和数据链路结构这种可变长度的结构体,我们把指向套接口地址结构的指针以及它的长度作为参数传递给套接口函数。

不同套接口地址结构的比较
不同套接口地址结构的比较

值-结果参数(value-result)

Call by value-result的情形下,在调用函数的时候,参数的值被传入函数,函数返回时,可以修改这个值,使其成为一个返回值。

上面说到,套接口函数中的两个参数,一个是指向套接口地址结构的指针,一个是结构的长度。其中。结构的长度的传递方式,又根据其传递的方向有所不同。

从进程到内核

如下面三个函数,最后一个参数都是结构的整数大小(socklen_t),由于指针和指针所指结构的大小都传递给内核,所以从进程到内核要确切拷贝多少数据是已知的。

代码语言:txt
复制
int bind(int, const struct sockaddr *, socklen_t);
int connect(int, const struct sockaddr *, socklen_t);
ssize_t sendto(int, const void *, size_t,int, const struct sockaddr *, socklen_t)

从内核到进程

下面四个函数,长度的参数则是指向结构的整数的指针(socklen_t *)。当函数被调用时,告诉内核,它的结构是多大,使内核写这个结构时不会越界。当函数返回时,它的值则被修改为结果——告诉进程内核在此结构中确切存储了多少信息。

代码语言:txt
复制
int accept(int, struct sockaddr * __restrict, socklen_t * __restrict);
ssize_t recvfrom(int, void *, size_t, int, struct sockaddr * __restrict,socklen_t * __restrict);
int getpeername(int, struct sockaddr * __restrict, socklen_t * __restrict);
int getsockname(int, struct sockaddr * __restrict, socklen_t * __restrict);

不过,尽管如此,对于IPv4和IPv6这两种定长套接口地址结构,那么从内核到进程返回的值也是定长的(分别是16字节和24字节),如果是可变的情况,那么从内核返回的值可能比结构的最大长度小。

字节排序函数

考虑一个16位整数0x0102,它由2个字节组成。内存中存储这两个字节有两种方式:

  1. 将低序字节(02)存储在起始地址,这称为小端(little-endian)字节序。
  2. 将高序字节(01)存储在起始地址,这称为大端(big-endian)字节序。

上面说的低序和高序,以我们熟悉的十进制来看,从右到左一次是个位,十位,百位,依次增大。二进制和十六进制也是一样,最右侧是最低有效位(LSB),左侧是最高有效位(MSB)。

代码语言:txt
复制
-----------------------------------
| MSB | 0000 0001 0000 0010 | LSB |
-----------------------------------

术语“小端”和“大端”表示多字节值的哪一端存储在内存的起始地址。

代码语言:txt
复制
-----------------------------------
| <-----------内存地址增大方向 | 起始 | 小端字节序
-----------------------------------
      |                     |
-----------------------------------
| MSB | 0000 0001 0000 0010 | LSB |
-----------------------------------
      |                     |
-----------------------------------
| 起始 | 内存地址增大方向-----------> | 大端字节序
-----------------------------------

打印当前机器的字节序

把两字节数0x0102存储为一个短整数,然后查看两个连续的内存地址的值c[0]c[1],以确定字节序。

代码语言:txt
复制
/**
 * intro/byteorder.c
 */
#include	"unp.h"

int
main(int argc, char **argv)
{
	union {
	  short  s;
      char   c[sizeof(short)];
    } un;

	un.s = 0x0102;
	printf("%s: ", CPU_VENDOR_OS);
	if (sizeof(short) == 2) {
		if (un.c[0] == 1 && un.c[1] == 2)
			printf("big-endian\n");
		else if (un.c[0] == 2 && un.c[1] == 1)
			printf("little-endian\n");
		else
			printf("unknown\n");
	} else
		printf("sizeof(short) = %d\n", sizeof(short));

	exit(0);
}

编译运行,可以看到本机是小端字节序:

代码语言:txt
复制
JACKIELUO-MC0:intro jackieluo$ make byteorder
JACKIELUO-MC0:intro jackieluo$ ./byteorder
i386-apple-darwin17.3.0: little-endian

网络协议使用大端字节序

网际协议在处理多字节整数时,使用大端字节序。因此需要考虑主机字节序和网络字节序间的互相转换,下面是我本机上的相关函数:

代码语言:txt
复制
#define ntohs(x)	__DARWIN_OSSwapInt16(x) // network to host
#define htons(x)	__DARWIN_OSSwapInt16(x) // host to network

#define ntohl(x)	__DARWIN_OSSwapInt32(x) // network to host
#define htonl(x)	__DARWIN_OSSwapInt32(x) // host to network

初始化套接口

书中示例程序用bzero来把套接口地址结构初始化为0:

代码语言:txt
复制
bzero(&servaddr, sizeof(servaddr));

bzero函数

bzero函数只有两个参数,便于记忆。

代码语言:txt
复制
void bzero(void *s, size_t n)

memset函数

memset将目标中指定数目的字节置为指定值。

代码语言:txt
复制
void* memset( void* dest, int ch, std::size_t count );
  • dest - 指向修改目标的指针
  • ch - 指定值
  • count - 要set的字节数

考虑到bzero是BSD中的过时函数,可以考虑使用memset来初始化套接口地址结构:

代码语言:txt
复制
memset(&servaddr, 0, sizeof(servaddr));

地址转换

在套接口编程中,我们需要在可读的ASCII字符串的地址,及网络字节序的二进制值间进行转换。书中使用协议无关的inet_ptoninet_ntop两个函数进行转换,字母p和n分别代表“presentation”和“numeric”。例如:

  • presentation:127.0.0.1
  • numeric:0000 0001 0000 0000 0000 0000 0111 1111
代码语言:txt
复制
/* int
 * inet_pton(af, src, dst)
 *	convert from presentation format (which usually means ASCII printable)
 *	to network format (which is usually some kind of binary format).
 * return:
 *	1 if the address was valid for the specified address family
 *	0 if the address wasn't valid (`dst' is untouched in this case)
 *	-1 if some other error occurred (`dst' is untouched in this case, too)
 * author:
 *	Paul Vixie, 1996.
 */
int
inet_pton(af, src, dst)
	int af;
	const char *src;
	void *dst;
{
	switch (af) {
	case AF_INET:
		return (inet_pton4(src, dst));
	case AF_INET6:
		return (inet_pton6(src, dst));
	default:
		errno = EAFNOSUPPORT;
		return (-1);
	}
	/* NOTREACHED */
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 套接口编程简介
    • 套接口地址结构
      • IPv4套接口地址结构
      • IPv6套接口地址结构
      • 通用套接口地址结构
      • 4种套接口格式对比
    • 值-结果参数(value-result)
      • 从进程到内核
      • 从内核到进程
    • 字节排序函数
      • 打印当前机器的字节序
      • 网络协议使用大端字节序
    • 初始化套接口
      • bzero函数
      • memset函数
    • 地址转换
    相关产品与服务
    数据保险箱
    数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档