前言
以<深入理解计算机系统>(以下称“本书”)内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定
引入
在上一篇帖子中,感觉学习方法有所失误.api的学习有两个好方法
1.做一个简单的例子---这个例子要比较典型,最好能包含知识点所有内容(最简),然后把他背下来---这个观点来自笔者学习的<C++ Prime Plus> 6th Edition这本书中的原话,过去某篇帖子中有提及.
2.读别人写的代码---这个观点本书P662也提到了.本质上和第1条相同,可能效果上会弱一点,综合起来可以手抄一遍或多遍(没错,手抄).这样学起来比较快,理解深刻.
还有一种办法:读api的源码.
反过来看背诵api的学习方法,一是不够深刻,二是容易出错,事倍功半.本书没有getaddrinfo的源码,查了权威书籍<Unix 网络编程 卷1:套接字联网API>(简介中号称"不朽的经典")也没找到源码,所以不在getaddrinfo上继续了(包括getnameinfo),如果以后有时间再细说.
学习方法回顾
磨刀不误砍柴工,对学习本身也要有个清晰的认识.学习=理解(记忆)+应用(练习)
1>学习概念. 编程是一种逻辑的表达.有趣的是,学习本身也是在建立一种"未知"和"已知"的逻辑,也就是理解.此外有的知识没有逻辑可言,是一种"公理",这就需要记忆了.所以,对概念的学习是"理解优先,记忆在后"
2>练习.贯彻"学以致用"的思路,做例子巩固学习效果.例子做多少视情况而定,难度上一般循序渐进.如果想简单出效果,就用"做一个简单的例子并背诵"这个办法.
注意:学习概念和练习最好一起进行,不要分开.在学会的基础上,可以增加"熟练"的要求,只要一提到某个知识点马上就浮现出相关的文字,图,思路并解决问题,这就是从新手进阶到高手了.
虽然此处有点班门弄斧,但笔者也不怕被人嘲笑(*^_^*)
套接字辅助函数
前面的套接字函数看起来很费劲,那么让封装后的套接字辅助函数来帮忙.回忆一下,编程不也是在把机器指令层层封装?
辅助函数介绍了两个open_clientfd和open_listenfd,字面意思打开客户端描述符,和打开监听描述符.
open_clientfd
函数解读
含义:客户端调用open_clientfd建立与服务器的连接.
函数原型如下:
#include"csapp.h"
int open_clientfd(char *hostname,char *port);
//返回值:若成功返回描述符clientfd,若出错返回-1
参数说明:
返回值clientfd,看前面向导图,客户端连接准备阶段的完成标志,生成一个clientfd的文件描述符,然后可以用Unix I/O函数做输入输出.---也就是一步到位,中间过程被封装.
hostname:服务器主机名,也就是域名.
---主机名的解读:互联网设备有IP地址作为身份证明,域名通常和IP地址一一对应,也有一对多,多对一.笔者理解服务器的主机名是其域名,客户端有没有域名也无所谓---只用IP地址做标识,某度上举了个例子,某一台电脑是客户端,右键"我的电脑"上可以修改的那个电脑名,可以看作是客户端的域名.
port:端口号,作监听连接请求.
---解读:这里有一点迷惑,端口号照理说是个short或者int整型,这里是char*类型,例如前面提到了著名的端口号80,难道是char a=80;然后参数传入&a?
源码解读
这一步很重要,看getaddrinfo函数如何使用,各个部分的功能(注释).实在读不懂还可以照着抄.源码在本书P661.
第5-10行,注释:获得一个潜在服务器地址的链表.
前面不想读的话,第10行调用了Getaddrinfo---包装函数,得到一个struct addrinfo**类型的数据&listp.这个双重指针表示一个链表结点的指针,即链表结点的集合.而每个链表结点中封装了可能的地址参数.
======================内容分割线↓==========================================
这里有个语法方面的新内容,以前没有遇见过
给双重指针传入指向链表的指针,是想表达什么意思?
复习指针的用法:
指针基本内容:指向单个元素,指向数组,指向链表
当函数形参用指针作参数,可以访问和改变传入实参指针指向的数据值.
要点:某类型的指针→某类型的值.如int*修改int值,而int**修改int*的值,即指针指向不同的地址.
举例:下面给int*形参传入int数组a,可以访问和修改数组a里的数值
//单层指针,已测试
#include<stdio.h>
int a[] = { 0,1,2 };
void fun1(int* p,int size) { //指针指向数组
for (int i = 0; i < size; i++) {
*p += 1;
printf("数组传入的值加1等于%d\n", *p);
}
}
int main(void) {
int size = sizeof(a) / sizeof(int);
fun1(a,size); //a数组内元素加1;
}
双重指针作形参的用法,通常有两种:
1需要修改某个指针变量指向的地址,传入这个指针变量的地址;
//双重指针,已测试,部分用不了
#include<stdio.h>
int a[] = { 0,1,2 };
int b[] = { 3,4,5 };
//如果传入大于0数字,双重指针指向第一个数组
void fun2(int** pd, int* p1, int* p2, int judge) { //双重指针表示返回的数组
if (judge >= 0)
*pd = p1;
else
*pd = p2;
}
//如果传入大于0数字,双重指针指向第1个数字
void fun3(int* pd, int a, int b, int judge) { //指针表示返回值
if (judge >= 0)
*pd = a;
else
*pd = b;
}
//用不了:打印数组
void show(int* pd, int size) {
for (int i = 0; i < size; i++) {
printf("数组里的第%d值是:%d\n", i, pd[i]);
}
}
int main(void) {
int* p=NULL;
int** pd = &p;
fun2(pd, a, b, 0); //传入0,*pd指向a;
// int size = sizeof(*pd) / sizeof(int); //不支持*pd表示数组
// show(*pd, size);
printf("选择后数组中第1个数字是:%d\n", **pd); //求得第1个值,即a[0]
printf("选择后数组中第2个数字是:%d\n", *(*pd+1)); //求得第2个值,即a[1]
printf("选择后数组中第3个数字是:%d\n", *(*pd+2)); //求得第3个值,即a[2]
fun3(p, 3, 4, 0);
printf("选择后的数字是:%d\n", *p);
}
这段代码有点勉强,有个问题没解决,就是不能用*pd表示数组,以后再说.
看fun3这个函数来推导,本来可以把需要的数据放在返回值,但放在了形参里用指针返回.
2双重指针指向二维数组,访问和修改二维数组中的数值.---示例略
注意:链表的数据被包裹在指向链表的指针中.当给指针传入指向链表首元素的指针时,他可以遍历整个链表并且修改链表中的数据.但是链表的机制和数组有所不同,一般不会说传入双重指针来修改其指向(笔者想到一种可能,就是函数里有多个链表,这种情况下是可能的,不过那也太复杂了,可不考虑)
//指向链表的指针,已测试
#include<stdio.h>
typedef struct node { //链表定义
int n;
struct node* next;
}Node;
//很简单的链表两个元素,n1在前,n2在后
Node n2 = { 2,NULL };
Node n1 = { 1,&n2 };
void fun2(Node* nd) { //指针指向链表
while (nd) {
nd->n += 2;
printf("链表传入的值加2等于%d\n", nd->n);
nd = nd->next;
}
}
int main(void) {
fun2(&n1); //传入链表
}
所以:结合上面fun3的推导,双重指针传入链表指针的意思是返回函数中做好的链表.
//伪代码
void fun4(Node** listp){
//构建链表
NOde* p=....
//返回这个链表
*listp=p;
}
Node* p=NULL; //初始化一个指空的指针,一般会报警告
fun4(&p); //传入双重指针
//执行后p指向了fun4里做好的链表p
小结:当使用指针作形参时,可以访问和修改指针指向的数据,或者以指针形式返回函数中的数据.
======================内容分割线↑==========================================
第12-16行,注释:遍历链表,找到能成功连接的结点.
第13行遍历链表结点,第15行调用socket函数(原型在上一帖找),传入结点数据,如果返回值小于0,需要继续遍历.当大于或等于0表示连接准备工作完成.
第18行,注释:连接服务器
第19行调用connect函数(原型在上一帖找),传入客户端描述符clientfd,判断返回值,如果≠-1,则连接成功并跳出循环.至此客户端连接完毕,可以用clientfd描述符+Unix I/O读写传送数据.如果不成功则关闭clientfd.
第24行,注释:释放链表 代码在第25行调用包装函数Freeaddrinfo
后面是判断,如果前面连接不成功,p遍历到了NULL跳出循环,所以写的是if(!p)此时返回-1,表示此次连接不成功,后面可以继续这个main函数来重复.
与此对应的是连接成功,把描述符clientfd返回,用于后面的操作.
函数小结
读完源码,因为是选择性解读,所以显得难度不大.此外还有之前函数的一些用法.有时候函数的应用是这样的:即使有一些模糊的部分,你读不太懂,那就依样画葫芦--抄.但抄的时候要特别留意注释,以免出错.
这个函数的核心和前一贴联系起来,他的目的是在客户端返回一个可进行读写的描述符clientfd,其中封装了socket函数和connect函数,大大简化了使用.
open_listenfd
函数解读
含义:服务器端调用open_listenfd创建一个监听描述符,准备好连接请求.
函数原型如下:
#include"csapp.h"
int open_listenfd(char *port);
//若成功则为描述符,若出错则为-1.
open_listenfd函数打开(?)和返回一个监听描述符,这个描述符准备好在端口port上接收连接请求.---黑体字是本书原话,估计是翻译笔误,是打开某个端口port,返回一个监听描述符listenfd.这里同样和前一贴对应上,得到一个监听描述符.接下来仍需要调用accept函数建立连接.在后面的例子中将看到这一点.
参数就一个port,同样有前面的迷惑,返回值是一个监听描述符,为得到描述符connfd作准备.
源码解读
大体上和open_clientfd的思路差不多:封装了bind和listen函数,得到了一个监听描述符listenfd.
注意:P661倒数第2行,使用setsockopt函数来配置服务器,默认30秒拒绝客户端的连接请求,严重阻碍了测试(本书原话).---又引出了新的内容,这种情况就适合"抄",这也是用框架或者别人封装函数的缺点:有的东西被固定了.如果想修改的话,看SOL_SOCKET,SO_REUSEADDR这两个枚举值是否可以改变.
小结
本贴分析了两个函数的优缺点,大大简化了建立连接的过程.同时回顾了指针的用法,并分析了getaddrinfo函数中出现的双重指针的由来,算是基础知识的巩固