大O
技术面试题
至少需掌握以下基础知识
掌握它们的具体用法、实现方法、应用场景以及空间和时间复 杂度。其中,散列表是必不可少的一个题目。对这个数据结构,务必要胸有成竹。
赛事介绍:
阶段一(15):
Hadoop分布式、HadoopHA、hive、spark、kafka、flume、zookeeper、Sqoop
(HBase、Storm)
阶段二(20):scrapy、正则表达式、xpath、数据存储(csv、json、mysql)
阶段三(25):mapreduce、spark、hive
阶段四(20):flask、jinja2、echarts、mysql语法
阶段五(15):写报告
团队素质(5):
集群hostname,hosts配置
网络环境配置
关闭防火墙
systemctl stop firewalld
永久关闭(禁用)防火墙
systemctl disable firewalld
启动防火墙
systemctl start firewalld
关闭SELinux
修改/etc/selinux/config 文件
将SELINUX=enforcing改为SELINUX=disabled
重启机器即可
配置SSH免密登录
在master机器上输入:ssh-keygen–t rsa回车
ssh-copy-id –iroot@主机名回车输入主机对应的用户密码
ssh root@node01
JDK
Mysql
可选:
配置同步脚本
1 |
|
显示进程
1 |
|
1 | # core-site |
1 | # hdfs |
1 | # mapred-env.sh |
1 | # mapred |
1 | # Yarn |
1 | 1.配置slaves文件,填入datanode的节点 |
zookeeper(集群之间进行协调管理,同步,故障检测等作用)
1)解压zookeeper到/usr/local/src目录下,重命名
2)配置环境变量,并且生效
3)配置文件: zoo_sample.cfg拷贝一个
cp zoo_sample.cfg zoo.cfg
修改内容:
dataDir=/usr/local/src/zookeeper/zkdata/data (自己创建的数据存放目录)
dataLogDir=/usr/local/src/zookeeper/zkdata/log (自己创建的日志目录)
server.1=主机名1:2888:3888 (集群配置)
server.2=主机名2:2888:3888 (集群配置)
server.3=主机名3:2888:3888 (集群配置)
4)在刚才配置的dataDir目录下新建一个 名为myid的文件,vim myid
在里面写上数字编号:1
5)将zookeeper远程拷贝至其他机器上。
6)修改拷贝后其他机器上zookeeper中myid的编号
7)将环境变量也同步拷贝到其他机器上,并且在其他机器上source一下
8)分别在集群机器上启动zookeeper:
启动: zkServer.sh start
查看状态: zkServer.sh status
停止: zkServer.sh stop
a) Hadoop-env.sh 配置jdk路径
b) Yarn-env.sh配置jdk路径
配置core-site.xmla 配置zookeeper和hadoop通信地址
1 |
|
a) 配置hdfs-site.xml
1 | <!-- 指定副本的数量 --> |
1 | <!--mapreduce运行的平台 ,默认Local--> |
1 | <!-- 开启YARN高可用 --> |
1 | slaves 集群节点 |
配置文件修改完毕,同步发送到其他机器上
scp -r /opt/soft/hadoop/etc/hadoop hadoop2:/opt/soft/hadoop/etc/
scp -r /opt/soft/hadoop/etc/hadoop hadoop3:/opt/soft/hadoop/etc/
1、分别每个机器上启动zookeeper集群(jps看到QuorumPeerMain进程)
bin/zkServer.sh start
2、每个机器上启动 journalnode(最好是奇数台机器)
(hadoop-daemon.sh start journalnode)
启动完毕后jps能看到JournalNode进程
3、在一个namenode节点上进行格式化(配置了两个namenode机器,只需要一台电脑上格式化)
(格式化成功后在配置的data目录下,有个dfs,里面有name和data,因为没有启动集群,data是空的因为格式化后暂时没有内容)
(name中存储元数据,data存真数据)
(name下有current,下面有4个文件,就是元数据信息<fsimage…..,seen_txid,VERSION>)
4、注意,在格式化后namenode的机器上找到存放数据的data(core-site.xml里面配置的元数据存储目录)目录,然后拷贝到另外一个备份的data目录下,保持初始元数据一致。
下面是例子,具体目录自己决定(只需要拷贝到另外一个namenode节点机器上即可)。
scp -r data hadoop02:/opt/temp
5、在任何一个namenode上执行zkfc -formatZK操作(#格式化zookeeper),命令如下:
hdfs zkfc -formatZK
6、start-dfs.sh
7、start-yarn.sh(在自己配置的对应任何一台resourcemanager机器上执行)
8、测试
1)分别在每个机器上jps查看状态
2)分别访问两个namenode机器上的50070界面,查看状态是否一个是Active,另外一个是Standby
3)也可以测试访问一下yarn集群
4)高可用测试,测试主备切换
(将active状态的namenode进程干掉,kill -9 xxxxid)
(可以再使用单节点启动方式启动namenode:hadoop-daemon.sh start namenode)
在另外一台resourceManager机器上单独启动resourceManager进程
yarn高可用测试和hadoop一样,访问的端口是8088
yarn-daemon.sh start resourcemanager
hadoop-daemon.sh start namenode
启动历史服务器
mr-jobhistory-daemon.sh start historyserver
yarn-daemon.sh start nodemanager
yarn-daemon.sh start resourcemanager
然后两台机器分别启动resourcemanager即可
命令方式查看hadoop状态:hdfs haadmin -getServiceState nn2
命令方式查看yarn状态:yarn rmadmin -getServiceState rm1
在本地创建一个文本文件,随便放入一些单词,以空格分开
a》将本地文件上传至已经启动的hdfs中 hdfs dfs -put /本地目录/本地文件.txt /
b》运行mapreduce示例(/usr/local/src/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.0.jar)
hadoop jar hadoop-mapreduce-examples-2.6.0.jar wordcount /hello.txt /test1
注意:/hello.txt是hdfs中的文件路径和名称
/test1是mapreduce结果输出到hdfs的目录,该目录存在则直接报错,所以该目录不能存在
随机返回指定行数的样本数据
hdsf dfs -cat /test/gonganbu/scene_analysis_suggestion/* | shuf -n 5
返回前几行的样本数据
hdsf dfs -cat /test/gonganbu/scene_analysis_suggestion/* | head -100
返回最后几行的样本数据
hdsf dfs -cat /test/gonganbu/scene_analysis_suggestion/* | tail -5
……
1、保证有一台机器上安装好了MySql数据库
2、解压hive 改名 配置环境变量
3、hive中conf下:
复制hive-env.sh 修改
复制hive-site.xml 修改(数据库连接,数据库驱动名,账号,密码,3个地址配置改)
4、将mysql的驱动放入hive中lib目录下,保证hive能够正常连接mysql数据库
5、将hive/lib中jline2.12.xxx的包拷贝到 hadoop/share/hadoop/yarn/lib
原来的jlineXXX老版本要删除掉
6、初始化元数据
schematool -dbType mysql -initSchema
7、直接输入hive启动进入hive命令行:
show databases;
create database test1;
use test1;
create table employee(eid int ,name string ,salary float)
row format delimited
fields terminated by ‘hdfs中文件行中字段的分隔符’
lines terminated by ‘\n’
;
8、load data inpath ‘/hdfs中的文件’ overwrite into table employee;
本地上传:load data local inpath ‘/export/servers/hivedatas/student.csv’ into table student;
9、使用select * from 表做一些查询测试
如果涉及分组则会进行mapreduce操作,得到结果。
在HDFS上默认储存路径:/user/hive/warehouse
1 | export JAVA_HOME=/opt/soft/jdk |
1 | <configuration> |
通过hadoop的mapreduce进行数据传输
1 | export HADOOP_COMMON_HOME= /usr/app/hadoop-2.7.3 |
将MySQL相关驱动jar拷贝到 sqoop下的lib目录下
测试:
sqoop-list-tables –connect jdbc:mysql://localhost:3306/bigdata_db – username root –password 123456
1 | sqoop-list-tables --connect jdbc:mysql://localhost:3306/bigdata_db -- username root --password 12345 |
1 | mv slaves.template slaves |
spark-env.sh 添加 JAVA_HOME 环境变量和集群对应的 master 节点
1 | 指定 Java Home |
1 | spark-submit --class org.apache.spark.examples.SparkPi --master spark://master:7077 --executor-memory 1G --total-executor-cores 2 ../examples/jars/spark-examples_2.11-2.0.0.jar 100 |
配置 HistoryServer
默认情况下, Spark 程序运行完毕后, 就无法再查看运行记录的 Web UI 了, 通过 HistoryServer 可以提供一个服务, 通过读取日志文件, 使得我们可以在程序运行结束后, 依然能够查看运行过程
复制 spark-defaults.conf
, 以供修改
1 | cd /export/servers/spark/conf |
将以下内容复制到spark-defaults.conf
末尾处, 通过这段配置, 可以指定 Spark 将日志输入到 HDFS 中
1 | spark.eventLog.enabled true |
将以下内容复制到spark-env.sh
的末尾, 配置 HistoryServer 启动参数, 使得 HistoryServer 在启动的时候读取 HDFS 中写入的 Spark 日志
1 | 指定 Spark History 运行参数 |
为 Spark 创建 HDFS 中的日志目录
1 | hdfs dfs -mkdir -p /spark_log |
1、在linxu中搭建好spark环境(单机或集群都可以)并启动
如果是单机在jps后可以看到:Master和Worker节点进程
2、代码中修改:
val sparkConf = new SparkConf().setMaster(“spark://hadoop05:7077”).setAppName(“Task1”)
说明:hadoop05是linux主机名,7077是spark通信端口
//指明读取的文件是hdfs文件系统中的文件
val rdd = sc.textFile("hdfs://hadoop05:9000/myword.txt")
说明:hadoop05是hdfs所在机器的主机名,9000是hadoop通信端口
//保存的时候也指明保存结果目录是hdfs文件系统
rdd4.coalesce(1).saveAsTextFile("hdfs://hadoop05:9000/out1")
3、在linux服务器中运行jar包:
spark-submit –class com.xyzy.spark3.MyTask1 –executor-memory 1g /opt/test/spark3_jar/spark3.jar
说明:com.xyzy.spark3.MyTask1是代码中的包名+类名
1 | bin/spark-submit \ |
在提交应用中,一般会同时一些提交参数
参数 | 解释 | 可选值举例 |
---|---|---|
–class | Spark 程序中包含主函数的类 | |
–master | Spark 程序运行的模式(环境) | 模式:local[*]、spark://linux1:7077、 Yarn |
–executor-memory 1G | 指定每个 executor 可用内存为 1G | 符合集群内存配置即可,具体情况具体分析。 |
–total-executor-cores 2 | 指定所有executor 使用的cpu 核数 为 2 个 | 符合集群内存配置即可,具体情况具体分析。 |
–executor-cores | 指定每个executor 使用的cpu 核数 | 符合集群内存配置即可,具体情况具体分析。 |
application-jar | 打包好的应用 jar,包含依赖。这 个 URL 在集群中全局可见。 比如 hdfs:// 共享存储系统,如果是 |
我们在初始化SparkConf时,或者提交Spark任务时,都会有master参数需要设置,如下:
1 | conf = SparkConf().setAppName(appName).setMaster(master) |
但是这个master到底是何含义呢?文档说是设定master url,但是啥是master url呢?说到这就必须先要了解下Spark的部署方式了。
我们要部署Spark这套计算框架,有多种方式,可以部署到一台计算机,也可以是多台(cluster)。我们要去计算数据,就必须要有计算机帮我们计算,当然计算机越多(集群规模越大),我们的计算力就越强。但有时候我们只想在本机做个试验或者小型的计算,因此直接部署在单机上也是可以的。Spark部署方式可以用如下图形展示:
Spark部署方式
下面我们就来分别介绍下。
Local模式就是运行在一台计算机上的模式,通常就是用于在本机上练手和测试。它可以通过以下集中方式设置master。
使用示例:
1 | /bin/spark-submit \ |
总而言之这几种local模式都是运行在本地的单机版模式,通常用于练手和测试,而实际的大规模计算就需要下面要介绍的cluster模式。
cluster模式肯定就是运行很多机器上了,但是它又分为以下三种模式,区别在于谁去管理资源调度。(说白了,就好像后勤管家,哪里需要资源,后勤管家要负责调度这些资源)
这种模式下,Spark会自己负责资源的管理调度。它将cluster中的机器分为master机器和worker机器,master通常就一个,可以简单的理解为那个后勤管家,worker就是负责干计算任务活的苦劳力。具体怎么配置可以参考Spark Standalone Mode
使用standalone模式示例:
1 | /bin/spark-submit \ |
–master就是指定master那台机器的地址和端口,我想这也正是–master参数名称的由来吧。
这里就很好理解了,如果使用mesos来管理资源调度,自然就应该用mesos模式了,示例如下:
1 | /bin/spark-submit \ |
同样,如果采用yarn来管理资源调度,就应该用yarn模式,由于很多时候我们需要和mapreduce使用同一个集群,所以都采用Yarn来管理资源调度,这也是生产环境大多采用yarn模式的原因。yarn模式又分为yarn cluster模式和yarn client模式:
使用示例:
1 | /bin/spark-submit \ |
【武汉二手丰田】武汉丰田二手车报价_武汉二手丰田价格|多少钱-人人车 (renrenche.com)
1.创建一个scrapy项目
1 scrapy startproject mySpider #mySpider是项目名字2.生成一个爬虫
1 scrapy genspider itcast itcast.cn #itcast是爬虫名字,"itcast.cn"限制爬虫地址,防止爬到其他网站3.提取数据
1 完善spiders,使用xpath等方法3.保存数据
1 pipelines 中保存数据
启动爬虫
1 | scrapy crawl 爬虫名字 #crawl(抓取的意思) |
启动爬虫不打印日志
1 | scrapy crawl 爬虫名字 --nolog |
run.py启动爬虫
1 | from scrapy import cmdline |
启动爬虫并保存
1 | scrapy crawl 爬虫名字 --o data.csv |
1 | name = 'maoyan3' |
1 | post_data= dict( |
Scrapy shell是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath表达式
使用方法:
1 | 命令行输入: |
常用参数:
1 | response.url:当前响应的url地址 |
1 | #常见的设置 |
您可以使用scrapy crawl myspider命令从命令行运行您的scraper 。如果要创建输出文件,则必须设置要使用的文件名和扩展名:
1 | scrapy crawl myspider -o data.json |
Scrapy有自己的内置工具来生成json,csv,xml和其他序列化格式。如果要指定生成的文件的相对路径或绝对路径,或者从命令行设置其他属性,也可以执行此操作:
1 | scrapy crawl reddit -s FEED_URI='/home/user/folder/mydata.csv' -s FEED_FORMAT=csv |
1 |
|
1 | \d 一个数字 |
Python:re模块
1 | //title/text():选择title标签内的文字。 |
1) 右键项目 Open Module Settings
2)找到左侧 Artifacts,点击中间的 +号,选择JAR,然后选择“”“From Module With dependicies”
3)选择要执行的Main方法
4)点击idea上面的Builde菜单,选择Builder Artifacts。然后builder即可(如果改了代码,直接点击Rebuild)
1 | JobClass类继承 Configured 实现 Tool 接口 // 实现Tool接口免去一些麻烦 |
可能会用到的方法:
1 | setup(),此方法被MapReduce框架仅且执行一次,在执行Map任务前,进行相关变量或者资源的集中初始化工作。若是将资源初始化工作放在方法map()中,导致Mapper任务在解析每一行输入时都会进行资源初始化工作,导致重复,程序运行效率不高! |
排序: 如果想要全局排序,吧reduce数设为1,可在map阶段进行一次排序,reduce进行归并二次排序
数据去重: –分组在reduce只取一个value输出
非法过滤+去重+计数: –map过滤 Counter计数 去重
统计平均值案例: – reduce累加处理
TopN案例: – 如果是每一类的TopN则只有在reduce处理
MapReduce的Join操作: – 正常处理 –缓存优化
MapReduce数据清洗+排序:
找共同好友案例:
1 | compareTo 方法用于将当前对象与方法的参数进行比较。 |
1 | //第一种 组名+计数器名 |
1 | SimpleDateFormat sdf_1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
1 | jack,88 |
1 |
|
1 | protected void cleanup(Context context) throws IOException, InterruptedException { |
1 | create [external] table [if not exists] table_name ( |
create table
创建一个指定名字的表。如果相同名字的表已经存在,则抛出异常;用户可以用 IF NOT
EXISTS 选项来忽略这个异常。
external
可以让用户创建一个外部表,在建表的同时指定一个指向实际数据的路径
(LOCATION),Hive 创建内部表时,会将数据移动到数据仓库指向的路径;若创建外部
表,仅记录数据所在的路径,不对数据的位置做任何改变。在删除表的时候,内部表的
元数据和数据会被一起删除,而外部表只删除元数据,不删除数据。
comment
表示注释,默认不能使用中文
partitioned by
表示使用表分区,一个表可以拥有一个或者多个分区,每一个分区单独存在一个目录下 .
clustered by 对于每一个表分文件, Hive可以进一步组织成桶,也就是说桶是更为细粒
度的数据范围划分。Hive也是 针对某一列进行桶的组织。
sorted by
指定排序字段和排序规则
row format
指定表文件字段分隔符
storted as指定表文件的存储格式, 常用格式:SEQUENCEFILE, TEXTFILE, RCFILE,如果文件
数据是纯文本,可以使用 STORED AS TEXTFILE。如果数据需要压缩,使用 storted as
SEQUENCEFILE。
location
指定表文件的存储路径
1 | SELECT [ALL | DISTINCT] select_expr, select_expr, ... |
1 |
|
1 | hive-exec 添加依赖 |
1 | import org.apache.hadoop.hive.ql.exec.UDF; |
RDD不存储数据,数据流动处理.
spark中job,stage,task的关系
1、一个应用程序对应多个job,一个job会有多个stage阶段,一个stage会有多个task
2、一个应用程序中有多少个行动算子就会创建多少个job作业;一个job作业中一个宽依赖会划分一个stage阶段;同一个stage阶段中最后一个算子有多少个分区这个stage就有多少个task,因为窄依赖每个分区任务是并行执行的,没有必要每个算子的一个分区启动一个task任务。如图所示阶段2最后一个map算子是对应5个分区,reducebykey是3个分区,总共是8个task任务。
3、当一个rdd的数据需要打乱重组然后分配到下一个rdd时就产生shuffle阶段,宽依赖就是以shuffle进行划分的。
spark中job,stage,task的关系 - 简书 (jianshu.com)
1 | 1) 从集合(内存)中创建 RDD |
1 | val source: RDD[String] = sc.textFile( |
使用textFile方法,有两个参数,第一个是文件路径,第二个是numpartitions。
如果我们不传第二个参数的话,minpartitions数就是采取默认的(用local指定的并行数和2取最小值)
如果我们传入第二个参数后,numpartitions会和参数保持一致。
,最终一步一步的传入到hadoop的切片处。
Spark 的 Shuffle 发展大致有两个阶段: Hash base shuffle
和 Sort base shuffle
默认的分区数量是和 Cores 的数量有关的, 也可以通过如下几种方式修改或者重新指定分区数量
1 | parallelize 设置 |
让来自相同 Key 的所有数据都在 reduceByKey
的同一个 reduce
中处理, 需要执行一个 all-to-all
的操作, 需要在不同的节点(不同的分区)之间拷贝数据, 必须跨分区聚集相同 Key 的所有数据, 这个过程叫做 Shuffle
.
Master Daemon
负责管理 Master
节点, 协调资源的获取, 以及连接 Worker
节点来运行 Executor
, 是 Spark 集群中的协调节点
Worker Daemon
Workers
也称之为叫 Slaves
, 是 Spark 集群中的计算节点, 用于和 Master 交互并管理 Executor
.
当一个 Spark Job
提交后, 会创建 SparkContext
, 后 Worker
会启动对应的 Executor
.
Executor Backend
上面有提到 Worker
用于控制 Executor
的启停, 其实 Worker
是通过 Executor Backend
来进行控制的, Executor Backend
是一个进程(是一个 JVM
实例), 持有一个 Executor
对象
另外在启动程序的时候, 有三种程序需要运行在集群上:
Driver
Driver
是一个 JVM
实例, 是一个进程, 是 Spark Application
运行时候的领导者, 其中运行了 SparkContext
.
Driver
控制 Job
和 Task
, 并且提供 WebUI
.
Executor
Executor
对象中通过线程池来运行 Task
, 一个 Executor
中只会运行一个 Spark Application
的 Task
, 不同的 Spark Application
的 Task
会由不同的 Executor
来运行
Stage 的划分是由 Shuffle 操作来确定的, 有 Shuffle 的地方, Stage 断开
1 | {% for k in names %} |
1 | sql = ‘select xx,xxfrom tb_xxwhere . group by . having… order by … ’ |
1 | 1、常量和变量:尽量使用常量,可以避免很多问题 |
函数基本语法
高阶函数
1、作为值的函数
函数就像和数字、字符串一样,可以将函数传递给一个方法,如List的map方法,可以接收一个函数
1 | val func:Int => String = (num:Int) => "*" * num |
2、匿名函数
没有赋值给变量的函数就是匿名函数
1 | (1 to 10).map(num => "*" * num) |
3、柯里化
将原先接受多个参数的方法转换为多个只有一个参数的参数列表的过程。
1 | def add(x:Int)(y:Int) = { |
相当于
1 | def add(x:Int) = { |
4、闭包
闭包其实就是一个函数,返回值依赖于声明在函数外部的一个或多个变量。
1 | val y = 10 |
样例类和普通类的区别:
1、数组:定长数组(Array)、变长数据(ArrayBuffer)
2、列表:不可变列表(List)、可变列表(ListBuffer)
3、Set:
4、Map
Java - StringTokenizer类
Scala - Spark基本方法都支持 方便快速测试
1 | // 单词计数:将集合中出现的相同的单词,进行计数,取计数排名前三的结果 |
原: https://github.com/alex/what-happens-when
接下来的内容介绍了物理键盘和系统中断的工作原理,但是有一部分内容却没有涉及。当你按下“g”键,浏览器接收到这个消息之后,会触发自动完成机制。浏览器根据自己的算法,以及你是否处于隐私浏览模式,会在浏览器的地址框下方给出输入建议。大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。你打算输入 “google.com”,因此给出的建议并不匹配。但是输入过程中仍然有大量的代码在后台运行,你的每一次按键都会使得给出的建议更加准确。甚至有可能在你输入之前,浏览器就将 “google.com” 建议给你。
为了从零开始,我们选择键盘上的回车键被按到最低处作为起点。在这个时刻,一个专用于回车键的电流回路被直接地或者通过电容器间接地闭合了,使得少量的电流进入了键盘的逻辑电路系统。这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除(debounce),并将其转化为键盘码值。在这里,回车的码值是13。键盘控制器在得到码值之后,将其编码,用于之后的传输。现在这个传输过程几乎都是通过通用串行总线(USB)或者蓝牙(Bluetooth)来进行的,以前是通过PS/2或者ADB连接进行。
USB键盘:
虚拟键盘(触屏设备):
键盘在它的中断请求线(IRQ)上发送信号,信号会被中断控制器映射到一个中断向量,实际上就是一个整型数 。CPU使用中断描述符表(IDT)把中断向量映射到对应函数,这些函数被称为中断处理器,它们由操作系统内核提供。当一个中断到达时,CPU根据IDT和中断向量索引到对应的中断处理器,然后操作系统内核出场了。
WM_KEYDOWN
消息被发往应用程序HID把键盘按下的事件传送给 KBDHID.sys
驱动,把HID的信号转换成一个扫描码(Scancode),这里回车的扫描码是 VK_RETURN(0x0d)
。 KBDHID.sys
驱动和 KBDCLASS.sys
(键盘类驱动,keyboard class driver)进行交互,这个驱动负责安全地处理所有键盘和小键盘的输入事件。之后它又去调用 Win32K.sys
,在这之前有可能把消息传递给安装的第三方键盘过滤器。这些都是发生在内核模式。
Win32K.sys
通过 GetForegroundWindow()
API函数找到当前哪个窗口是活跃的。这个API函数提供了当前浏览器的地址栏的句柄。Windows系统的”message pump”机制调用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam)
函数, lParam
是一个用来指示这个按键的更多信息的掩码,这些信息包括按键重复次数(这里是0),实际扫描码(可能依赖于OEM厂商,不过通常不会是 VK_RETURN
),功能键(alt, shift, ctrl)是否被按下(在这里没有),以及一些其他状态。
Windows的 SendMessage
API直接将消息添加到特定窗口句柄 hWnd
的消息队列中,之后赋给 hWnd
的主要消息处理函数 WindowProc
将会被调用,用于处理队列中的消息。
当前活跃的句柄 hWnd
实际上是一个edit control控件,这种情况下,WindowProc
有一个用于处理 WM_KEYDOWN
消息的处理器,这段代码会查看 SendMessage
传入的第三个参数 wParam
,因为这个参数是 VK_RETURN
,于是它知道用户按下了回车键。
KeyDown
NSEvent被发往应用程序中断信号引发了I/O Kit Kext键盘驱动的中断处理事件,驱动把信号翻译成键码值,然后传给OS X的 WindowServer
进程。然后, WindowServer
将这个事件通过Mach端口分发给合适的(活跃的,或者正在监听的)应用程序,这个信号会被放到应用程序的消息队列里。队列中的消息可以被拥有足够高权限的线程使用 mach_ipc_dispatch
函数读取到。这个过程通常是由 NSApplication
主事件循环产生并且处理的,通过 NSEventType
为 KeyDown
的 NSEvent
。
当使用图形化的 X Server 时,X Server 会按照特定的规则把键码值再一次映射,映射成扫描码。当这个映射过程完成之后, X Server 把这个按键字符发送给窗口管理器(DWM,metacity, i3等等),窗口管理器再把字符发送给当前窗口。当前窗口使用有关图形API把文字打印在输入框内。
浏览器通过 URL 能够知道下面的信息:
Protocol
“http”使用HTTP协议
Resource
“/“请求的资源是主页(index)
当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。
a-z
, A-Z
,0-9
, -
或者 .
的字符google.com
,所以没有非ASCII的字符;如果有的话,浏览器会对主机名部分使用 Punycode 编码gethostbyname
库函数(操作系统不同函数也不同)进行查询。gethostbyname
函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系统有所不同gethostbyname
没有这个域名的缓存记录,也没有在 hosts
里找到,它将会向 DNS 服务器发送一条 DNS 查询请求。DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。要想发送 ARP(地址解析协议)广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。
如果缓存没有命中:
ARP Request
:
1 | Sender MAC: interface:mac:address:here |
根据连接主机和路由器的硬件类型不同,可以分为以下几种情况:
直连:
ARP Reply
(见下面)。集线器:
ARP Reply
。交换机:
ARP Reply
ARP Reply
:
1 | Sender MAC: target:mac:address:here |
现在我们有了 DNS 服务器或者默认网关的 IP 地址,我们可以继续 DNS 请求了:
当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket
,请求一个 TCP流套接字,对应的参数是 AF_INET/AF_INET6
和 SOCK_STREAM
。
到了现在,TCP 封包已经准备好了,可以使用下面的方式进行传输:
对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点 处理。节点的目标地址和源地址将在后面讨论。
大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理。
最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域(autonomous system, 缩写 AS)的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP数据报头部 time to live (TTL) 域的值每经过一个路由器就减1,如果封包的TTL变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。
上面的发送和接受过程在 TCP 连接期间会发生很多次:
客户端选择一个初始序列号(ISN),将设置了 SYN 位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号
服务器端接收到 SYN 包,如果它可以建立连接:
服务器端选择它自己的初始序列号服务器端设置 SYN 位,表明自己选择了一个初始序列号服务器端把 (客户端ISN + 1) 复制到 ACK 域,并且设置 ACK 位,表明自己接收到了客户端的第一个封包
客户端通过发送下面一个封包来确认这次连接:
自己的序列号+1接收端 ACK+1设置 ACK 位
数据通过下面的方式传输:
当一方发送了N个 Bytes 的数据之后,将自己的 SEQ 序列号也增加N另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个 ACK 包,ACK 的值设置为接收到的数据包的最后一个序列号
关闭连接时:
要关闭连接的一方发送一个 FIN 包另一方确认这个 FIN 包,并且发送自己的 FIN 包要关闭的一方使用 ACK 包来确认接收到了 FIN
ClientHello
消息到服务器端,消息中同时包含了它的 Transport Layer Security (TLS) 版本,可用的加密算法和压缩算法。ServerHello
消息,消息中包含了服务器端的TLS版本,服务器所选择的加密和压缩算法,以及数字证书认证机构(Certificate Authority,缩写 CA)签发的服务器公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥Finished
消息给服务器端,使用对称密钥加密这次通讯的一个散列值Finished
消息,也使用协商好的对称密钥加密如果浏览器是 Google 出品的,它不会使用 HTTP 协议来获取页面信息,而是会与服务器端发送请求,商讨使用 SPDY 协议。
如果浏览器使用 HTTP 协议而不支持 SPDY 协议,它会向服务器发送这样的一个请求:
1 | GET / HTTP/1.1 |
“其他头部”包含了一系列的由冒号分割开的键值对,它们的格式符合HTTP协议标准,它们之间由一个换行符分割开来。(这里我们假设浏览器没有违反HTTP协议标准的bug,同时假设浏览器使用 HTTP/1.1
协议,不然的话头部可能不包含 Host
字段,同时 GET
请求中的版本号会变成 HTTP/1.0
或者 HTTP/0.9
。)
HTTP/1.1 定义了“关闭连接”的选项 “close”,发送者使用这个选项指示这次连接在响应结束之后会断开。例如:
Connection:close
不支持持久连接的 HTTP/1.1 应用必须在每条消息中都包含 “close” 选项。
在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了。
服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的:
1 | 200 OK |
然后是一个换行,接下来有效载荷(payload),也就是 www.google.com
的HTML内容。服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供之后的请求重用。
如果浏览器发送的HTTP头部包含了足够多的信息(例如包含了 Etag 头部),以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应:
1 | 304 Not Modified |
这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。
在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到HTML页面引入的所有资源(图片,CSS,favicon.ico等等)全部都获取完毕,区别只是头部的 GET / HTTP/1.1
会变成 GET /$(相对www.google.com的URL) HTTP/1.1
。
如果HTML引入了 www.google.com
域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host
头部会变成另外的域名。
HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。
HTTPD 接收请求
服务器把请求拆分为以下几个参数:
HTTP 请求方法(GET
, POST
, HEAD
, PUT
, DELETE
, CONNECT
, OPTIONS
, 或者 TRACE
)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法域名:google.com请求路径/页面:/ (我们没有请求google.com下的指定的页面,因此 / 是默认的路径)
服务器验证其上已经配置了 google.com 的虚拟主机
服务器验证 google.com 接受 GET 方法
服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)
如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求
服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 “/“ ,会访问首页文件(你可以重写这个规则,但是这个是最常用的)。
服务器会使用指定的处理程序分析处理这个文件,假如 Google 使用 PHP,服务器会使用 PHP 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者
当服务器提供了资源之后(HTML,CSS,JS,图片等),浏览器会执行下面的操作:
浏览器的功能是从服务器上取回你想要的资源,然后展示在浏览器窗口当中。资源通常是 HTML 文件,也可能是 PDF,图片,或者其他类型的内容。资源的位置通过用户提供的 URI(Uniform Resource Identifier) 来确定。
浏览器解释和展示 HTML 文件的方法,在 HTML 和 CSS 的标准中有详细介绍。这些标准由 Web 标准组织 W3C(World Wide Web Consortium) 维护。
不同浏览器的用户界面大都十分接近,有很多共同的 UI 元素:
浏览器高层架构
组成浏览器的组件有:
浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成8kB大小的分块传输。
HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树。
解析树是以 DOM 元素以及属性为节点的树。DOM是文档对象模型(Document Object Model)的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如Javascript)的接口。树的根部是”Document”对象。整个 DOM 和 HTML 文档几乎是一对一的关系。
解析算法
HTML不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:
由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。
解析结束之后
浏览器开始加载网页的外部资源(CSS,图像,Javascript 文件等)。
此时浏览器把文档标记为可交互的(interactive),浏览器开始解析处于“推迟(deferred)”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成(complete)”,浏览器会触发“加载(load)”事件。
注意解析 HTML 网页时永远不会出现“无效语法(Invalid Syntax)”错误,浏览器会修复所有错误内容,然后继续解析。
<style>
标签包含的内容以及 style 属性的值StyleSheet object
),这个对象里包含了带有选择器的CSS规则,和对应CSS语法的对象floated
,位置有 absolutely
或 relatively
属性的时候,会有更多复杂的计算,详见http://dev.w3.org/csswg/css2/ 和 http://www.w3.org/Style/CSS/current-workCPU
,也可能使用图形处理器 GPU
GPU
用于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利用 GPU
强大的并行计算能力,用于在渲染过程中进行大量的浮点计算。渲染结束后,浏览器根据某些时间机制运行JavaScript代码(比如Google Doodle动画)或与用户交互(在搜索栏输入关键字获得搜索建议)。类似Flash和Java的插件也会运行,尽管Google主页里没有。这些脚本可以触发网络请求,也可能改变网页的内容和布局,产生又一轮渲染与绘制。
简单记录
1 | WebSettings webSettings = x5webView.getSettings(); |