0.前言
代码目录: https://github.com/brandon-rhodes/fopnp/tree/m/py3
0.1.网络实验环境:理解客户端与服务器是如何通过网络进行通信的
每台机器通过一个Docker容器实现
0.1.1.调制解调器A和B下面的客户机(h1~h4)表示典型客户端场景,家庭或咖啡店(内部网络,不能访问互联网,如果要连互联网,都通过调制解调器IP进行连接)
0.1.2.调制解调器通过ISP网关连接广域网(主干路由器,负责将数据包发送至与之相连的网络)
0.1.3.example.com及相连机器表示机房配置。没有网络地址转换或伪装,互联网上的各个客户端可随意访问example.com后的三个服务器提供的服务端口
0.1.4.ftp、mail、www服务器运行正确配置的守护进程,Python脚本可以运行在其他机器,并成功连接到上述服务
0.1.5.所有服务器成功安装TLS证书,所有客户机有example.com的签名及安装受信证书,及要求TLS认证的Python脚本可以成功获取认证
0.1.6.可以在网络环境的任意一台机器上连接并运行命令,可对网络中的任意一个点进行数据包追踪,查看客户端和服务端之间的网络数据传输情况
1.客户端、服务器网络编程
1.1.协议栈与库
1.1.1.协议栈:复杂的网络服务建立在简单网络服务的基础上
1.1.2.使用Python库(内置标准库+三方库),解析要使用的网络通信协议
网络编程就是选择并使用一个已经支持所需网络操作的库的过程。通过了解底层网络服务知识,除了可以理解网络库的运行原理,还能够在底层部分出现错误时,知道具体发生了什么。
标准库:http://docs.python.org/3/library/
三方库:https://pypi.python.org/
谷歌地理编码服务pygeocoder包
邮箱地址:207 N. Defiance St Archbold, OH
获取该物理地址的纬度和经度:
安装virtualenv来避免,在几个月的开发后,Python安装环境包含无用包及新安装包与已安装包不兼容的问题。
win中,虚拟环境中包含Python二进制文件的目录为Scripts,不是bin
virtualenv -p python3 geo_env
cd geo_env
ls
O]bin/ include/ lib/
.bin/activate
python -c 'import pygeocoder'
O]error
pygcocoder包未安装,在虚拟环境中使用pip安装pygeocoder包
pip install pygeocoder
python -c 'import pygeocoder'
#获取经度与纬度 search1.py
#!/usr/bin/env python3
from pygeocoder import Geocoder
if __name__ == "__main__":
address = '207 N. Definace St, Archbold, OH'
print(Geocoder.geocode(address)[o].coordinates)
执行python3 search1.py
O](41.521954, -84.306691)
pygeocoder接口背后的原理是怎样的?
后面将详细学习如何在一个包含至少6层的网络协议栈的顶层构建这个复杂的服务
1.2.应用层:
如果需要自己为谷歌地图API编写客户端:没有使用直接提供地理编功能的三方库,而是使用更底层的Requests库。
#!/usr/bin/env python3
#/chapter01/search2.py
import requests
def geocode(address):
parameters = {'address': address, 'sensor': 'false'}
base = 'http://maps.googleapis.com/maps/api/geocode/json'
response = requests.get(base, params=parameters)
answer = response.json()
print(answer['results'][0]['geometry']['location']
if __name__ == '__main__':
gecode('207 N. Definace St, Archbold, OH')
执行python3 search2.py
O]{'lat': 41.521954, 'lng': -84.306691}
结果并不完全相同,JSON数据将结果封装为对象,Requests库以Python字典形式提供该对象
谷歌文档:http://code.google.com/apis/maps/documentation/geocodeing/
search2.py没有通过地址和纬度直接解决问题,而是通过构造URL,获取查询响应,然后将结果转化为JSON,一步步解决问题
高层的代码描述了查询的意义,而底层的代码展示了查询的构造细节
1.3.协议的使用:
search2.py脚本构建了一个URL,并获取了该URL查询的响应文档,为了使URL查询看起来像一个基础操作,Web浏览器做了很多事情。
URL可以获取某个文档的原因:描述了网络上该特定文档的位置及获取方法。提供了更底层协议查询该文档所需的指令,search2.py就能够解析URL并获取响应文档了。
URL包含了协议的名称,后面跟着保存文档的主机名,最后是该主机上特定文档的路径。
Requests库从谷歌获取结果的具体原理,其实就是由HTTP提供的,如果不想使用Requests库提供的功能,而是想直接使用HTTP来获取结果,使用/chapter01/search3.py
#!/usr/bin/env python3
import http.client
import json
from urllib.parse import quote_plus
base = '/maps/api/geocode/json'
def geocode(address):
path = '{}?address={}&sensor=false'.format(base, quote_polus(address))
connection = http.client.HTTPConnection('maps.google.com')
connection.request('GET', path)
rawreply = connection.getresponse().read()
reply = json.loads(rawreply.decode('utf-8'))
print(reply['results'][o]['geometry']['location'])
if __name__ == '__main__':
geocode('207 N. Definace St, Archbold, OH')
该程序直接使用HTTP协议:请求连接特定主机->手动构造带path参数的GET查询->直接从HTTP连接获取响应结果。
此方法没有使用字典将查询参数方便的表示为独立的键值对,而是手动嵌入到查询地址中。
要通过该方法完成查询,需要在?后跟上&分隔的参数,这些参数通过name=value的形式表示
1.4.原始的网络会话
HTTP协议利用,现代操作系统提供的使用TCP协议在IP网络的不同程序间进行纯文本网络会话的功能,在两台机器间传输数据。
HTTP协议精确描述了两台主机间通过TCP传输的信息格式,并以此提供HTTP的各项功能。
通过Python可以方便操作的网络协议栈的最底层:/chapter01/search4.py,像网络发送了一个原始文本信息作为请求,并收到了很多原始文本作为响应。
#!/usr/bin/env python3
import socket
from urllib.parse import quote_plus
request_text = """\
GET /maps/api/geocode/json?address={}&sensor=false HTTP/.1\r/n/
Host: maps.google.com:80\r\n\
User-Agent:search4.py (Foundations of Python Network Programming)\r\n\
Connection: close\r\n\
\r\n\
"""
def geocode(address):
sock = socket.socket()
sock.connect(('maps.google.com', 80))
request = request_text.format(quote_plus(address))
sock.sendall(request.encode('ascii'))
raw_reply = b''
while True:
more = sock.recv(4096)
if not more:
break
raw_reply += more
print(raw_reply.decode('utf-8'))
if __name__ == '__main__':
geocode('207 N. Defiance St, Archbold, OH')
search4.py本质的不同:深入到最底层:使用主机操作系统提供的原始socket()函数来支持IP网络上的网络通信。相当于C写底层系统一样
原始网络通信的过程就是发送与接收字符串的过程。发送的查询是一个字符串,接收到的响应同样也是一个字符串。
通过sendall()函数传入的参数了解到该HTTP查询的具体内容。查询中包含了关键字GET,GET后跟着待获取文档的路径以及支持的HTTP版本。
GET /maps/api/geocode/json?address=207+N.+Defiance+St%2C+Archbold%2C+OH&sensor=false HTTP/1.1
GET信息后跟着一些请求头,每个请求头包含了名称、冒号、值。最后是请求结束的回车符和换行符
python search4.py
O]
HTTP/1.1 200 OK
Content-Type:
Date: Sat, 23 Nov 2013 18:34:30 GMT
Expires: Sun, 24 Nov 2013 18:34:30 GMT
Cache-Control:public, max-age=86500
Vary: Accept-Language
Access-Control_Allow-Origin: *
Server: mafe
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Alternate-Protocol: 80:quic
Connection: close
{
"results": [
{
...
"formatted_address": "207 North Definace Street, Archbold, OH 43502, USA",
"geometry": {
"location" : {
"lat" : 41.521954,
"lng" : -84.306691
},
...
},
"types" : [ "street_address" ]
}
],
"status": "OK"
}
HTTP响应的结构和HTTP请求类似。首先是状态行,跟着一些响应头,响应头后有一个空行,接着就是响应体(JSON格式~JavaScripts数据结构),此JSON就是之前查询的响应结果,描述了查询谷歌地理编码API返回的地理位置。
这里所有状态和响应头都是使用httplib库处理的底层细节。如果没有没有分层,网络通信的具体细节即为此。
1.5.层层深入
1.5.1.协议栈:先构建利用网络硬件在两台PC间传送文本字符串的原始会话功能,然后在此基础上创建更复杂、更高层语义更丰富的对话(邮寄地址对应的地理位置)。
例子中分析的协议栈包含4层
谷歌地理编码API,封装了1)如何用URL表示地理信息查询;2)如何获取包含坐标信息的JSON数据
URL,标识了科通过HTTP获取的文档
HTTP层,支持面向文档的命令(GET),操作使用了原始TCP/IP套接字
TCP/IP套接字,只处理字节串的发送和接收
1.5.1.1.协议栈的每一层都使用了其底层协议提供的功能,并同时向上层协议提供服务。
1.5.1.2.Python对涉及的各网络层都提供了非常全面的支持。除非使用应用提供商定制的协议并定制请求的格式(pygeocoder连接谷歌服务),否则无需使用三方库
1.5.1.3.使用的通信协议越底层,程序质量也明显随之下降。故应该尽可能使用标准库或三方库。
1.5.1.4.高层的网络协议(如解析街道地址的谷歌地理编码API)通常会将底层网络细节隐藏,如果值使用过pygeocoder库,可能永远不会知道URL和HTTP是pygeocoder用来解决问题的底层机制。
1.5.1.5.socket()接口其实并不是查询谷歌涉及的最底层的协议,套接字这一抽象其实也基于更底层的协议,只不过这些协议由操作系统管理,非Python
socket()API层以下的几层:
1.5.2.1.传输控制协议(TCP),通过发送、接收以及重排数据包(packet)的小型网络星系,支持由字节流组成的双向网络会话。
1.5.2.2.网际协议(IP),该层处理不同计算机间数据包的发送
1.5.2.3.最底层的"链路层",负责在直接相连的计算机之间发送物理星系,由网络硬件设备组成,如以太网端口和无线网卡
1.6.编码与解码
1.6.1.python3对字符串和底层字节序列做了区分。
1.6.1.1.字节(byte)是网络通信中实际传输的二进制数。每个字节8位二进制,范围从00000000到11111111,转为十进制就是0到255.
1.6.1.2.字符(character)串包含了Unicode字符,如a、{,∅(空集)。每个Unicode字符均有一个编码点(code point)的数字标识符与之对应。除非主动请求Python对字符和外部可见的实际字节进行相互转化,否则对使用者可见的只有字符。
1.6.2.字节和字符串两者间的相互操作为解码(decoding)和编码(encoding)
1.6.2.1.解码:应用程序使用字节时发生的。当应用程序从文件或网络接收到字节时,程序就像一个一流间谍一样,对通信信道间传输的原始字节进行解密。
1.6.2.2.编码:程序将字符串对外输出时所实施的过程。此时应用程序使用一种编码方法将字符串转化为字节。当计算机需要传输或存储符号时,字节才是真正使用的格式。
使用python3操作这两个过程相当简单。
1.6.3.1.使用decode()方法将读入的字节串转化为字符串,
1.6.3.2.使用encode()方法对要输出的字符串进行编码。
if __name__ == '__main__':
# Translating from the outside world of bytes to Unicode characters.
input_bytes = b'\xff\xfe4\x001\x003\x00 \x00i\x00s\x00 \x00i\x00.\x00'
input_characters = input_bytes.decode('utf-16')
print(repr(input_characters))
# Translating chsracters back into bytes before sending them.
output_characters = 'We copy you down, Eagle.\n'
output_bytes = output_characters.encode('utf-8')
with open ('eagle.txt', 'wb') as f:
f.write(output_bytes)
注意在调用两者的repr()方法时的区别:
1.6.4.1.字节串由字母b开始,如b'Hello';
1.6.4.2.字符串则没有起始字母,如'world'。
为了消除字节串与字符串带来的混淆,Py3只对字符串类型提供了大量的字符串方法。
1.7.网际协议(IP):为全世界通过互联网连接的计算机赋予统一地址系统的一种机制,使得数据包能够从互联网的一段发送至另一段。理想情况下。网络浏览器无需了解具体使用哪种网络设备来传输数据包,就能够连接上任意一台主机。
1.7.1.网络互联是通过物理链路将多台计算机连接,使之可以互相通信。
1.7.2.网际互联是将相邻的物理网络相连,使之形成更大的网络系统,如互联网。
但两者本质都是允许资源共享的精心设计的机制。
1.7.3.计算机中的各种各样的资源都需要被共享,网络设备间进行共享的基本单元是数据包(packet),只要有需要,就可以交换。一个数据表是一串长度在几字节到几千字节间的字节串,是网络设备间进行数据传输的基本单元。
1.7.4.数据包在物理层通常只有两个属性:包含的字节串数据+目标传输地址。
1.7.5.物理数据包的地址一般是一个唯一的标识符,表示了再传输数据包的过程中,插入同一以太网段的其他网卡或无线信道。
1.7.6.网卡负责发送并接收这样的数据包,使得计算机操作系统不用关心网络是如何处理网线、电压即信号这些细节的。
Python程序很少直接操作IP这么底层的协议。
1.8.IP地址:最初版本为连接到万维网的每台计算机分配了一个4字节的地址,通常写为由句点分隔的十进制数。每个十进制数表示地址的1字节,因此,每个数的范围是0到255。
由于纯数字表示的地址不便记忆,人们使用主机名(hostname)来代替IP地址,只要键入google.com就可访问谷歌,其实,它是将主机名解析到了类似74.125.67.103的地址,实际上通过互联网将数据包传输到了该地址。
# 1-7 chapter01/getname.py 主机名转换为IP地址
import socket
if __name__ == '__main__':
hostname = 'www.python.org'
addr = socket.gethostbyname(hostname)
print('The IP address of {} is {}'.format(hostname, addr))
O]The IP address of www.python.org is 151.101.40.223
1.8.1.无论一个互联网应用程序看起来多么新奇,实际上IP协议总是使用数字表示的IP地址来作为数据包传输的目标地址。
1.8.2.将主机名解析为IP地址这一复杂的细节是由操作系统来处理的,操作系统倾向于自己处理IP的多数操作细节,对于用户及Python代码不可见。
4字节IP已经不够用,又部署了IPv6的拓展地址机制,允许使用16字节的地址:fe80::fcfd:4aff:fecf:ea4e,只要代码从用户处接收IP地址或主机名,将它们传递给网络库来处理,那么就永远不需要考虑IPv4和v6的区别,运行代码的操作系统会知道使用的IP版本,并作出相应的解析。
1.8.2.IP地址从左往右,前两个直接表示某个机构,第3个字节表示目标机器所在的特定子网,最后一个字节将地址细化至该特定的机器或服务。
1.8.3.特殊IP地址段:
1.8.3.1.127...*:由机器上运行的本地应用程序使用。当程序连接到这一地址段中的地址时,其实是在与同一机器上的一些其他服务或程序交互。大多数只使用127.0.0.1,表示该程序的机器本身,通常可以通过主机名localhost来访问
1.8.3.2.10...、172.16-31..、192.168..*:为私有子网(private subnet)预留的。
运营互联网的机构保证,绝不会把这三个地址段中的任何地址分发给运行服务器或服务的实体公司。故连接互联网时,这些地址是没有意义的,并不对应可连接的任一主机。
构建组织内部网络,可以随意使用这些地址来*分配内部的IP地址,不需让外网访问这些主机。
1.9.路由:根据目的IP地址,选择将IP数据包发往何处
一旦程序请求操作系统向某一特定IP地址发送数据,操作系统就需要决定如何使用该机器连接的某一物理网络来传输数据。这一决定就叫路由(routing)
1.9.1. 如果IP地址形如127...*,那么操作系统会知道数据包的目的地址,是本机运行的另一个程序,该数据包不会传送给物理网络设备,直接通过操作系统内部数据复制转交给另一应用程序。
1.9.2. 如果目的IP地址与本机处于同一子网,可以通过简单检查本地以太网段、无线信道,或是其他任何网络信息来找到目标主机,将数据包发送给本地连接的机器。
1.9.3. 否则将数据包转发给一台网关机器(gateway machine),该网关将本地子网连接至互联网,再决定将该数据包发往何处。
路由只是在网络边缘时才这么容易,Py应用程序很少运行在互联网骨干路由器上,所以实际情况几乎全是简单路由情形。
1.9.4. 同一子网中所有主机有着相同的IP地址前缀,信号表示地址可变部分,但ASCII信号并没有插入到路由表中,而是通过结合IP地址和掩码来表示子网。
1.9.5. 掩码指出了某主机属于某子网所需的高位比特匹配数。
1.9.5.1. 127.0.0.0/8:该模式指出地址的前8位(1字节必须与127匹配,余下的24位(3字节)则可以是任意值。
1.9.5.2. 192.168.0.0/16:该模式匹配了属于192.168私有地址段的任何IP地址,指出前16位必须完全匹配,后16位可以是任意值
1.9.5.3. 192.168.5.0/24:明确指定一个特定的独立子网,最常见的子网掩码。属于该子网的机器只有最后1字节不同,允许有256个不同的地址。通常, .0地址用来表示子网名, .255地址用作'广播数据包'的目标地址,会被发送到子网内的所有主机。这样,就有254个地址随机分配给计算机。 .1地址通常用于连接外网的网关,但有些公司/学校也会选择其他地址。
py代码直接使用主机操作系统体统的功能,去正确选择数据包路由,和之前依靠操作系统来将主机名解析至IP地址是一样的。
1.10.数据包分组
IP支持的数据包极大,最大可至64KB,但是构建于IP网络之上的实际网络设备,通常并不支持这么大的数据包,所以分组是必要的。以太网只支持1500B的数据包。
网络数据包中包含一个表示"不分组"(DF,Don't Fragment)的标记,在源计算机与目的计算机之间的某条物理网络无法容纳太大的数据包时,发送者可以通过这个标记选择是否进行分组。
1.10.1.如果没有设置DF标记,允许分组。当数据包大小超过网络能够容纳的上限时,网关能够将其分为多个小数据包,并进行标记,表示接受方在接受之后需要将这些小数据包重组为原始大数据包。-->将大的拆分为小的,接收后再拼接成大的。
1.10.2.如果设置了DF标记,不允许分组。如果网络无法容纳数据包,将会丢弃该数据包,并发回一条错误信息。错误信息由特殊信号数据包表示,叫做Internet控制报文协议(ICMP)数据包。发送方在收到错误后,会尝试将信息分割为较小的数据包重发
DF标记无法由Py程序控制,由操作系统来设置。-->一开始就一直发小的,接收小的
系统通常使用的逻辑:
1.10.3.如果正在进行一个,由网络键传输的独立数据报组成的UDP会话,那么操作系统不会设置DF标记,故无论需要传输多少数据,所有数据包都能到达接收方。
1.10.4.如果是TCP会话,TCP可能是由多达上千个数据包组成的长数据流,那么系统将设置DF标记,选择正确的数据包大小,使得TCP会话顺畅进行,如果不这样做,数据包会在途中不断分组,从而使得会话较为低效。
一个互联网子网能够接收的最大数据包叫做最大传输单元(MTU),90年代,互联网服务商(DSL链路电话公司)使用PPPoE,PPPoE对IP数据包进行封装,封装后大小只有1492B,不是以太网允许的1500B,使得很多默认1500B的网站措手不及,还使用错误的安全措施,阻塞了所有的ICMP数据包,收不到错误信息,就不知道1500B的数据包到达客户的DSL链路时无法兼容,导致小文件和网页的访问没有问题,Telnet和SSH等交互式协议也都正常,这两种操作发送的数据包都小于1492B。一旦用户尝试下载一个大文件,或者Telnet/SSH一次性大量输出好几个屏幕的信息,那么连接就会被冻结并无法响应。
1.11.进一步学习IP
余下的章节,学习IP层之上的协议,描述IP的官方资源是IETF发布的RFC文档,可以了解到网际协议工作的每个细节,网址:http://tools.ietf.org/html/rfc791
或《TCP/IP详解:协议》