套接口编程简介

套接口编程简介

套接口地址结构

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

IPv4套接口地址结构

/*
 * 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套接口地址结构
验证

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

printf("size of servaddr is %zu\n", sizeof(servaddr));

得到结果:

size of servaddr is 16

3个成员

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

// 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套接口地址结构

/*
 * 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中:
#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套接口地址结构

通用套接口地址结构

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

/*
 * [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函数:

int bind(int, const struct sockaddr *, socklen_t);

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

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

4种套接口格式对比

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

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

值-结果参数(value-result)

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

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

从进程到内核

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

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 *)。当函数被调用时,告诉内核,它的结构是多大,使内核写这个结构时不会越界。当函数返回时,它的值则被修改为结果——告诉进程内核在此结构中确切存储了多少信息。

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)。

-----------------------------------
| MSB | 0000 0001 0000 0010 | LSB |
-----------------------------------

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

-----------------------------------
| <-----------内存地址增大方向 | 起始 | 小端字节序
-----------------------------------
      |                     |
-----------------------------------
| MSB | 0000 0001 0000 0010 | LSB |
-----------------------------------
      |                     |
-----------------------------------
| 起始 | 内存地址增大方向-----------> | 大端字节序
-----------------------------------

打印当前机器的字节序

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

/**
 * 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);
}

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

JACKIELUO-MC0:intro jackieluo$ make byteorder
JACKIELUO-MC0:intro jackieluo$ ./byteorder
i386-apple-darwin17.3.0: little-endian

网络协议使用大端字节序

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

#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:

bzero(&servaddr, sizeof(servaddr));

bzero函数

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

void bzero(void *s, size_t n)

memset函数

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

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

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

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
/* 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 */
}

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小狼的世界

Linux下不同文件编码的转换

字符编码(Character Encoding)可以说就是让某一字符序列匹配一个指定集合中的某一东西,常见的例子包括长短电键组合起来表示的摩斯电码(Morse ...

1102
来自专栏达摩兵的技术空间

灵活的js

如果你觉得写基本的赋值语句,或定义几个方法,或者使用下对象的内置方法就算会了js,那其实还差的远。 还差什么呢?还差一些编程的思维,以及优化的编程思想。

952
来自专栏IMWeb前端团队

ES6解构嵌套对象

本文作者:IMWeb zzbozheng 原文出处:IMWeb社区 未经同意,禁止转载 让我们先回忆一下ES6的对象解构,本文介绍各种ES6的对象解构...

2805
来自专栏大内老A

ASP.NET MVC的Model元数据与Model模板:模板的获取与执行策略

当我们调用HtmlHelper或者HtmlHelper<TModel>的模板方法对整个Model或者Model的某个数据成员以某种模式(显示模式或者编辑模式)进...

2096
来自专栏不会写文章的程序员不是好厨师

[翻译]Java 6,7,8中的String.intern

最近一直在关注“故障排查”的相关知识,首先着手的是OOM的异常。OOM异常通常会有Perm区的OOM(java7及以前)和HeapSpace的OOM,这两种各有...

1622
来自专栏熊二哥

快速入门系列--CLR--03泛型集合

.NET中的泛型集合 在这里主要介绍常见的泛型集合,很多时候其并发时的线程安全性常常令我们担忧。因而简述下.NET并发时线程安全特性,其详情请见MSDN。 ...

1797
来自专栏极客生活

Python查看对象或者方法使用帮助的三板斧

python中每一个对象或者对象的方法都有可以使用三种方式查看相关的使用方法和帮助文档。

681
来自专栏程序你好

在c#中,如何序列化/反序列化一个字典对象?

.Net提供的各种序列化的类,通过使用这些类,. Net对象的序列化和反序列化变得很容易。但是字典对象的序列化并不是那么容易。为此,您必须创建一个能够序列化自身...

1051
来自专栏Java后端技术栈

Redis常见的5种不同的数据类型详解

Redis除了可以存储键还可以存储常见的5种数据类型,分别是:String、List、Set、Hash、ZSet。对于Redis的命令有一部分是可以公用的,但是...

1071
来自专栏程序员的知识天地

Python程序员必备的30个编程技巧

直接交换2个数字的位置 Python 提供了一种直观的方式在一行代码中赋值和交换(变量值)。如下所示:

1562

扫码关注云+社区

领取腾讯云代金券