UNP_Chapter03_Socket详解与实现_笔记总结

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter03: Sockets Introduction

3.1 简介

从这里开始我们将接入了API的使用与原理,帮助我们了解这些API有助于开发的扩展性发展. 我们会先以今后几乎每个案例都会用到的结构体:socket地址结构体开始, 我们还会讲到地址转换函数inet_addr, inet_ntoa, 但是这两个是只针对于IPv4的,之后出来的inet_pton和inet_ntop是兼容v4和v6的. 具体的情况我们下文分析,开始吧.

3.2 socket地址结构体

大多数socket函数(包括listen, bind, socket等)都需要一个指向socket地址结构体的指针, 针对不同的协议族它们所都有着不同的socket地址结构体,统一命名为sockaddr_然后加上些特有标记.
我们先来看看运用最广的IPv4的socket地址结构:
我们在<netinet/in.h>中能够看到定义:
这里写图片描述
这里写图片描述
有人会说到底是哪个呢?我们能够看到sockaddr_in结构体中有in_addr结构体,所以当然是sockaddr_in是IPv4的特有socket地址结构体了. 其中这个in_port_t类型是这样定义的typedef uint16_t in_port_t;这样是不是一目了然了. 来我们看看ip(7)中是怎么定义和说明的:
这里写图片描述
在UNP这本书上是这样的版本:
这里写图片描述
显然现在的内核中定义已经稍微改变了一下,那我们就追踪下去, 因为我们发现我们头文件中的结构体的内容和man中的不一样,那么man中的sa_family_t是什么玩意儿?并且在头文件中的结构体中怎么没有呢?然后头文件中的__SOCKADDR_COMMON是什么呢?一切从<bits/sockaddr.h>中能够看出:
这里写图片描述
看到没, 这样是不是和man中的一样了. 那这个到底是干什么的呢?可以看到这就是一个整形变量(2字节),主要的用处是指明地址类型:取值主要是AF_UNIX|AF_INET|AF_INET6|AF_PACKET等等. 由于所有的地址结构体必然有地址类型,因此封装成一个统一的宏. 这样一来就对这个结构体清晰多了,in_addr_t是32位的无符号整形,也就表示了32bit的IPv4的地址. __SOCKADDR_COMMON呢直接用sa_family_t sa_prefix来进行替换,然后sa_family_t表示的是无符号短整型(2字节).
这里写图片描述
这里写图片描述
这样看来我们要是用IPv4的话是不是传个2(尽量不要用magic number)或者是用AF_INET进去就没问题啦~~
我们也能发现在man手册中会将源码中好多包装的宏进行替换将类型更直观的呈现出来.
UNP给出的sockaddr_in结构体重第一行是sin_len但是这个并不是POSIX规定的,所以好多Unix和Linux发行商将这个去掉了, 因为我们不是处理routing socket(这主要是和内核交互读取删除路由表中的信息等,我们后面会涉及)的情况下我们是不用它的. 这里我们将POSIX给出的自定义类型进行了简单罗列:
这里写图片描述
我们回到正轨上,有四个方法就是将这个socket addressing 结构体从用户进程传入到了内核中: bind, connect, sendto, sendmsg. 还有五个函数能从内核将socket addressing结构体传回到用户进程: accept, recvfrom, recvmsg, getpeername.
我们也看到man中的sockaddr_in结构体就是POSIX的标准, 几乎在所有的实现结构体中都会加上sin_zero但是我们用的时候并不需要指定这个参数,这个参数加上就是为了保证所有的socket address结构体都是至少16字节的.
再说到类型时,你可能还见过Unix中定义的u_char, u_short, u_int等, 这些都是为了兼容老版本而定义的.
这个结构体中的IPv4地址和TCP或者UDP端口号必须以网络字节序的方式传入. 至于为什么分出两个结构体来时因为你4.2BSD的历史原因, 这里就不说了,可以自行查.
我们能够发现我们传入IPv4有两种方式,第一种是serv.sin_addr这样的话,传入的IPv4作为了一个in_addr结构体,我们也可以serv.sin_addr.s_addr这样的话作为了一个in_addr_t类型,也就是无符号32位整型. 这两种都有自己的方式,编译器会不同对待的,所以要选择正确. in_addr在早期是定义为了Union, 这样允许每个都是4字节的内存,这样的话有利于区分出A,B,C类地址,但是现在的划分class的方式很多种了,所以这种联合体也很少有用了.
sin_zero就设置成0就行通常来说,一般是不用的. 一个这样的socket结构体只能进行一个端口和IP之间的交流.

但是我们要知道协议族并不是只有IPv4结构体,但是我们的socket函数是通用的函数,也就是说其中的协议结构体需要通用. 我们一般都是用指针指向结构体进行传入,有了ANSI定义就很简单直接用void*, 但是socket地址结构出现在ANSI之前,所以一直用的都是sockaddr结构体作为通用结构体
这里写图片描述
我们从我们的socket函数中也能看出:
int bind(int, struct sockaddr *, socklen_t);
所以这就要求我们调用这些socket函数都要强转成sockaddr结构体指针指向通用结构体.

struct sockaddr_in serv; //IPv4 socket address structure
bind(sockfd, (struct sockaddr *)&serv, sizeof(serv));

有的人会说,这样做有什么意义呢,就这样用一下,为什么不直接用void*, 作为编程者确实就是为了转一下,意义不大,但是对于内核来说,需要通过统一的sa_family来判断这个是那种类型的结构体.

我们接下来说说IPv6套接字结构体.
这里写图片描述
有一下几点需要注意:
1. 如果系统支持套接字地址结构体中的长度字段,那么SIN6_LEN就必须设定.
2. IPv6的family是AF_INET6
3. 这里的sin6_flowinfo还有sin6_scope_id都是TCP/IP中比较值得深究的参数.
在这里一个新的通用套接字地址结构体诞生了,这个结构体足够大能容纳系统所支持的IPv6
这里写图片描述
讲到这里,是不是有点混乱,这么多结构体,那么我们做个比较吧:
这里写图片描述
所以说socket并不是只是IP的的结构,数据链路层也是有socket的哦.

3.3 值-结果参数

通过上面混乱的分析,最起码我们知道我们是以指向结构体的指针将结构体传入到socket函数中的,所以结构体的长度也得进行传递,不然的话,指针做函数参数只是四字节,这是基本的C,这里不啰嗦. 这里我们讨论的是不同的传递方式,也就是说从用户态传入内核态还是从内核态传入到用户态.
1. bind``connect``sendto这三个函数都将socket地址结构体从用户态到内核套:

struct sockaddr_in serv;
connect(sockfd, (struct sockaddr*)&serv, sizeof(serv));

这里写图片描述
其实socket address结构体的大小数据类型是socklen_t而不是int, 但是POSIX推荐socklen_t就是uint32_t.
2. accept``recvfrom``getsockname``getpeername这些函数都是将结构体从内核送到用户态的.
这里写图片描述
这里写图片描述
这里的程度为什么是值的地址了呢?这是因为这个len既可能是从用户态调用函数的时候传入到内核告诉内核结构体大小的值也是内核收到对方的信息后塞入结构体传回给用户层的程度. 这就是值-结果参数.
比如说IPv46套接字结构体就是固定的长度,4是16, 6是28. 这样的话从内核中返回也是固定的长度,但是像Uninx domain的sockaddr_un结构体的长度可能返回的和传入的就不一样. 这就是值-结果参数.

3.4 字节序函数

在引入字节序之前,我们会经常碰到一个问题,就是大端序和小端序. 那到底什么是大端小端呢? 其实我们的字节在内存中都是这两种方式存储在内存中的. 我们就以2字节的Integer来进行解释:
这里写图片描述
MSB表示的就是16bit的高位,LSB就是低位. 我们就能够看出,小端就是高位对应高内存地址. 同理,大端字节序就是高位对应低内存地址. 在我们的系统中这两种存储方式都是有可能存在的,但是并没有个很好的标准来规范化,所以出来了主机字节序和网络字节序的概念. 那么系统中使用的就是主机字节序,也就是可能是大端或小端.
这里给举个例子吧: 比如说一个short值是0x0102(short值一般就是2字节). 这样的话,用一个union存储short s; char c[sizeof(short)]. 为什么要用union呢,这是因为union中的成员是共享内存的. 细节请看Union通俗易懂的解释 ,我们知道数字存在栈中,栈中先进入的是高地址,所以c[0]是高地址, 我们就可以比较高地址的值是不是1或者2就能知道是小端还是大端. 以内存储的这字节01肯定是字节序高端.
通过不同的系统比较,发现: freeBSD4和Linux是小端对其的,像macOS, FreeBSD5, AIX, HPUX, Solaris都是大端存储的.
不管怎么样,在网络中必须要保证字节序的一致,也就是在我们的TCP segment中, 有16位的port还有32或128位的IP, 在发送协议栈中的序列和接收协议栈中的序列必须一致才能保证不会乱序. 所以我们规定在网络传输中统一使用大端字节序.
所以当我们进行网络间传输的时候就需要进行字节序的转换:
这里写图片描述
在32位机上,uint16_t处理16位的,uint32_t处理32位的,64位机上uint16_t处理32位的,uint32_t处理64位的.
这样的话我们就不需要知道系统到底是怎么样存储的了.直接转就行.
还有一点就是我们平常用一个字节表示8位数量,但是在网络属于中是octet而不是字节.

3.5 字节处理函数

我们要引入两组在socket结构体重常用到的函数,b开头的和mem开头的. 我们先说前者:
这里写图片描述
我们用的最多的就是bzero函数,我们也能够看出其作用就是将目的字穿的指定字节数清为0. 但是为什么要引入berzo呢?这个函数广泛应用在socket address structure, 因为比如说IP其中含有0是正常的,但是这并不是C字符串,因为C碰到0会结束,所以像IP这种字串并不属于ANSI的C字穿. 处理C的null结束的字穿的函数是str开头的函数. 这组函数是BSD4.2产生的.
我们再看看ANSI C定义的mem打头的函数:
这里写图片描述

3.6 inet_aton, inet_addr, inet_ntoa

我们之前也说到了要将主机字节序转换为网络字节序,但是这样就够了吗?别忘了我们的例如IP地址可并不是C字符串哦. 因为其中有点,这个则么处理. 于是这三个方法就产生了:
这里写图片描述
目的是直接转换成二进制网络字节序fit in结构体中. 是不是贼方便. 在之后有更加高级的方法inet_ptoninet_ntop可以通用于IPv4和IPv6. 我们先说inet_aton, 直接传入你的IP(带点的)然后它就直接转入到in_addr结构体中了. 返回1表示成功. 同理inet_ntoa, 但是它是直接传入结构体的,这种函数很少有,大多数还是指针指向结构体作为参数的. 最后说一下inet_addr, 这个函数是被摒弃了的,因为从返回值上看出, INADDR_NONE返回错误,但是INADDR_NONE表示的是32位的1, 这样的话就是说255.255.255.255这样的广播地址是不能转的. 并且取决于C编译器,有的man上说是返回-1是错误,这样的话in_addr_t是unsigned的值,但是现在出来负数, 这不操蛋么, 所以现在已经没什么系统用了. 注意这三个函数只能转换32bit的.

3.7 inet_pton, inet_ntop

这两个函数是用处最广的. 支持IPv4和IPv6的地址转换. 这样是不是也解决了之前用htons转换IPv6大小不够的问题. 我们这里的p表示presentation, 就是表示ASCII(我们常规看到的10.211.55.9),n表示numeric, 就是网络字节序的二进制值适应于socket address structure.
这里写图片描述
这里的family只支持AF_INETAF_INET6, 可见IP特有转换方法. 如果不是这俩family, 就会errno=EAFNOSUPPORT. inet_pton函数就是将strptr字符串转后存储二进制字串通过addrptr指针. inet_ntop函数呢中的presentation格式的strptr指针不能是空指针, 必须提前为它分配好内存空间,然后函数成功返回就是返回这个指针了. 这个函数还涉及到你要得到的转换后的大小,也就是说我们定义数组的时候的大小, 系统为我们定义了两个宏:
#define INET_ADDRSTRLEN 16 /*for IPv4 dotted-decimal*/
#define INET6_ADDRSTRLEN 46 /*for IPv6 hex string*/
所以我们在位strptr分配空间的时候就直接用这俩就行.
我们用个图来整体归纳一下这五个转换方法:
这里写图片描述
我们来看一个简单的inet_pton实现:
这里写图片描述
这个是简单的inet_ntop的实现:
这里写图片描述

3.8 sock_ntop

我们通过之前的介绍能够看出,对于IPv4:
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
对于IPv6:
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
这样的话是IP独立的,我们也可以自行写方法函数来让合在一起, Richard为我们提供了很好的模板,实现并不难:

未完待续


联系方式: reyren179@gmail.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值