前言
以<深入理解计算机系统>(以下称"本书")内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定
引入
本贴内容主要是套接字相关的api.在学习一项新技术过程中,api接口函数的学习是比较"痛苦"的.因为新技术的学习根本上是概念的理解,概念本来就是抽象的,api把抽象概念具体化成数据,因此一定要明白概念的核心是什么,才可以把api理解透彻.
每个接口函数(api)的学习大概有这些内容:
1.函数做了什么事?从函数名看得出来---函数的命名规范最重要的就是表达函数的作用.除了函数名称,得具体解读函数的说明文档.
2.函数形参及形式.形参相当于函数表达逻辑中的"因"
3.返回值.返回值相当于函数表达逻辑中的"果".
注意:因为函数返回值只有一个类型的数据.如果有多个数据该怎么办?有两个解决办法:
1>返回值类型设计成一个结构struct.把所有需要返回的数据放进去.
2>形参返回.这也是很常见的方式,特别是在套接字接口的函数设计中用得很多.
白话再讲一声,api的难点是只能把他的含义背下来.这个也是没办法的事,计算机体系里每层做每层的事,如果要一直分析到芯片层,都是一些三极管和门电路,也没有那个必要.
套接字接口
回顾
依前所述,套接字概念:套接字是对网络硬件的抽象,客户端和服务器之间的通信,是通过建立套接字对连接,并调用Unix I/O函数(read/write)传递数据的过程.
套接字接口api是围绕着这一概念来展开的.
套接字接口向导图

向导图导读:
通信分为准备阶段和连接阶段,传输数据阶段和结束.本书主要讨论准备和连接阶段
准备阶段 :客户端调用socket做好连接准备,服务器端调用socket,bind,listen函数做好连接准备;这阶段完成的标志:客户端生成++clientfd描述符++ ,服务器生成++listenfd监听描述符++
连接阶段 :客户端调用connect函数试图建立连接,服务器端调用accept函数接收,顺序不分先后 .这阶段的标志是服务器生成++connfd描述符++
传输数据阶段对应上图的connect和accept下面两行,rio_writen和rio_readlineb函数在两者间互传数据.
结束阶段由客户端发起--close,服务器接收到EOF后终止这次连接,并等待下一次连接.
套接字地址结构
从Linux内核的角度来看,一个套接字就是一个通信的一个端点.从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件.套接字地址结构有两种形式
//套接字地址结构1
struct sockaddr_in{
uint16_t sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
//套接字地址结构2
struct sockaddr{
uint16_t sa_family;
char sa_data[14]
};
两者是等价的.因为一些历史原因导致了有两种形式.
套接字函数
正式进入api学习环节了,这个过程是枯燥而抽象的,需要紧扣概念.但也是程序员工作内容所以比较重要.对照上面的向导图理解
socket函数
作用: 客户端和服务器使用socket函数来创建一个套接字描述符
**---**解读:不管是客户端还是服务器,建立连接的第一个步骤,见向导图
函数原型如下:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
//返回值:若成功非负文件描述符,若出错则为-1
具体使用,是一个硬编码参数调用
int clientfd=Socket(AF_INET,SOCK_STREAM,0); //因为是硬编码,所以固定写法
细节上这里的Socket函数是一个包装函数, 有包含其他内容**.**
不过最好的方法是用getaddrinfo函数来自动生成这些参数,这样代码就与协议无关了.---黑体字是原话
结论:socket函数将不会直接使用,而是配合getaddrinfo函数使用.
connect函数
作用:客户端通过调用connect函数来建立和服务器的连接
**---**解读:客户端发出连接请求
函数原型如下:
#include<sys/socket.h>
int connect(int clientfd,const struct sockaddr* addr,socklen_t addrlen);
//成功返回0,出错返回-1
函数解读及参数说明:connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)---常数?connect函数会阻塞(等待),一直到连接成功建立或是发生错误(返回-1)
如果成功,clientfd描述符现在就准备好可以读写,并且得到的连接产生的套接字对
(x:y,addr.sin_addr:addr.sin_port) //套接字对的含义见上一帖
x表示客户端的IP地址,而y表示临时端口(客户端内核自动分配),唯一确定客户端主机上的客户端进程.
**---**解读:填入想连接服务器的信息,试图建立连接,还是挺直接的.
小问题:众所周知C语言中想改变某个数据,需要传入地址,而这里并没有这样的要求(clientfd)
结论:对于socket,最好方法配合getaddrinfo来为connect提供参数.
bind函数
作用:告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来
---解读:本书的说明有点让人费解,这里的sockfd是产生监听描述符listenfd的条件,并不会再次出现.所以笔者认为理解成由服务器套接字地址,产生一个套接字描述符sockfd比较恰当.
函数原型如下:
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
//成功返回0,出错返回-1
和connect函数比较,只有函数名及第一个参数名有不同,其他相同.
结论:对于socket,最好方法配合getaddrinfo来为bind提供参数.
listen函数
作用:将sockfd从一个主动套接字转化成一个监听套接字,该套接字可以接受来自客户端的连接请求.
**---**解读:默认情况下,内核会认为socket函数创建的描述符对应于主动套接字,他存在于一个连接的客户端.服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用.---黑体字是本书原话.这个说明同样令人费解.因为服务器端调用的socket函数,他还不知道是服务器端生成的套接字?但作为被动使用api的程序员,也不要追究那么多,因为没有源码可看,估计是一些其他原因.
注意:调用listen函数后,sockfd成为监听套接字,在下面的accept函数中被称为"监听描述符"
函数原型如下:
#include<sys/socket.h>
int listen(int sockfd,int backlog);
//若成功则为0,若出错则为-1
backlog参数的确切含义要求对TCP/IP协议的理解,通常设为一个较大值,比如1024---本书原话
accept函数
作用:服务器通过调用accept函数来等待来自客户端的连接请求.
accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在++addr中填写客户端的套接字地址++ ,并返回一个++已连接描述符++,这个描述符被用来利用Unix I/O函数与客户端通信.---黑体字是原话
--- 解读: 这里的已连接描述符是服务器端的++connfd++ ,可被用来读写数据.对照向导图解读中的内容,当生成++connfd++,表示此次连接建立完毕,双方可以传送数据.
函数原型如下:
#include<sys/socket.h>
int accept(int listenfd,struct sockaddr* addr,int *addrlen);
//返回值:若成功返回非负连接描述符--connfd描述符,若出错则为-1
注意:本书没有和前面几个函数一样,明确提到配合getaddrinfo来为accept提供参数.但根据笔者的理解,他和前面几个服务器端函数(除了listen外),需要配合getaddrinfo函数.
监听描述符和已连接描述符
简单理解:++每当客户端和服务器建立连接++ ,服务器生成一个监听描述符listenfd ,直到客户端和服务器完全断开为止.而++每次客户端发出连接请求++ ,即每次需要传输数据时,服务器端生成已连接描述符connfd 来处理,直到该次连接完毕.下面是本书配图
本书解释了这样做的原因:建立并发服务器.客户端每次连接产生一个connfd,那么客户端同一时间里建立多个连接,服务器用多个connfd来应对,即可以在同一时间内传送更多数据.
getaddrinfo函数
本书原话:Linux提供了一些强大的函数(getaddrinfo和getnameinfo)来实现二进制套接字地址结构和主机名,主机地址,服务名和端口号的字符串表示之间的相互转化.
=========================内容分割线↓=======================================
这个函数看得人有些"头大".简单说说笔者的理解:
1>他设计了一个叫做addrinfo的struct,把前面函数中需要的struct sockaddr*包含了进去.
2>采用了一个"倒装"的形式,保证连接建立的前提下"计算"出struct sockaddr*,然后把他当成参数传入前面的api中,建立连接.
=========================内容分割线↓=======================================
函数理解的重点内容:
本书P657第一段:客户端和服务器两边都调用getaddrinfo函数---向导图也能看见.客户端调用getaddrinfo之后,会遍历这个列表,依次尝试每个套接字地址,直到调用socket和connect成功 ,建立起连接.类似地,服务器会尝试遍历列表中的每个套接字地址,直到调用socket和bind成功,描述符会被绑定到一个合法的套接字地址.
**---**笔者有个小疑问,既然这里socket,connect和bind都成功了,为什么后面(下面红体字部分)还要说把参数传递给socket,connect和bind?可以理解成这里是在做"计算",并没有实际上建立连接.
本书P657第二段:host和service参数的填写---域名(IP地址)和端口号(套接字)等
本书P658第三段:getaddrinfo是主动创建的const sockaddr*
本书P658第四段:getaddrinfo一个很好的方面是addrinfo结构中的字段是不透明的,即他们可以直接传递给套接字接口中的函数,应用程序代码无需再做任何处理.例如,ai_family,ai_socktype和ai_protocol可以直接传递给socket.类似地,ai_addr和ai_addrlen可以直接传递给connect和bind.这个强大地属性使得我们编写地客户端和服务器能够独立于某个特殊版本地IP协议.---粗体字是原话.
**---**解读:函数中的hint暂时不做解读,这段话可以印证笔者的思路.再看看函数原型,思考一下过程:填入IP地址和端口号,"求出"前面的socket函数,connect函数,bind函数中的实参.调用时将其直接传入.
此后,服务器调用listen函数不需要这里得出的实参,而服务器端调用accept需要++客户端套接字地址++.---这里又是一个"盲点",可以想象当客户端试图建立连接时,交出套接字地址给服务器,传给accept进行调用,服务器同意后,连接建立(发散思考一下:如果服务器不想被某些IP地址所访问,可以在这里进行禁止).
小结
套接字函数的部分解读,待续