协议栈:是一组分层的网络协议,用于管理网络通信中的各种功能,例如OSI模型和TCP/IP模型。
OSI模型:OSI模型(Open Systems Interconnection Model)是一个抽象的网络通信模型,由七层组成:
TCP/IP模型:TCP/IP模型是互联网的基础协议栈,由四层组成:
库:是一组预先编写的代码,提供对网络协议的实现和抽象,使开发人员可以更方便地进行网络编程。
TCP/IP协议仅仅支持在客户端和服务器之间传输字节串。
HTTP协议描述了客户端如何通过TCP/IP建立的连接来请求特定的文档。以及服务器如何响应并提供相应的结果。
万维网将获取由HTTP托管的文档所需的指令编码为一个特殊的地址,这个地址称为URL。
在服务器需要向客户端返回结构化数据时,标准JSON数据格式是最流行的表示返回文档的格式。
每当需要在网络上传输文本信息,或将文本信息以字节的方式存储到磁盘等存储设备上时,都要将字符编码为字节。现代互联网最常用的方法是简单而又有限的ASCII编码以及强大而通用的Unicode系统。其中,UTF-8是尤为常见的Unicode编码方法可以使用Python的decode()将字节串转换为实际字符。encode()方法则可以用于反向的转换。Python3做了一项尝试,永远不会自动将字节转换为字符串,原因在于要正确完成这一转换操作,就必须事先知道所使用的编码方法,否则只能靠猜。因此,比起Python2,使用Python3编写代码时,需要更多地调用decode()和encode()方法
由于IP网络帮助应用程序传输数据包,网络管理员、设备供应商和操作系统程序员一起协力为单独的机器分配了IP地址,在机器以及路由器上建立了路由表,并配置了域名系统以将IP地址和用户可见的域名关联起来。
Python程序员应该知道,每个IP数据包在发往目的地址时,都有自己的传输路径。另外,如果一个数据包超过了传输路径上路由器间一跳的大小限制,那么就可能会对这个数据包进行分组。
在大多数应用程序中,有两种使用IP的基本方法。第一种是,将每个数据包视为独立的信息来使用:另一种则是,请求一个被自动分为多个数据包的数据流。这两种协议分别叫作UDP和TCP。
数据包分组:Ip支持的数据包最大可达64kb,但实际的网络设备一般不会支持这么大,例如以太网设备的1500B。DF标记表示不分组,如果设置了DF标记那么当网络无法容纳数据包就会丢弃并返回错误信息。MTU最大传输单元,表示能够接受的最大的数据包。
用户数据报协议使得用户级程序能够在IP网络中发送独立的数据包。通常情况下,客户端程序向服务器发送一个数据包,而服务器通过每个UDP数据包中包含的返回地址发送响应数据包。
POSIX网络栈让我们能够通过“套接字”的概念来操作UDP。套接字是一个通信端点,给出了IP地址和UDP端口号。IP地址和UDP端口的二元组叫作套接字的名字(name)或地址(address),可以用来发送与接收数据报。Python通过内置的socket模块提供了这些网络操作原语。服务器在接收数据包时需要使用bind()绑定一个IP地址和端口。由于操作系统会自动为客户端的UDP程序选择一个端口号,客户端的UDP程序可以直接发送数据包。
UDP建立在网络数据包的基础上,因此它是不可靠的。丢包现象发生的原因可能是网络传输媒介的故障,也可能是某个网段过于繁忙。因此,客户端需要弥补UDP的不可靠性,不断重发请求直至收到响应为止。为了不使繁忙的网络情况变得更糟,客户端应该在重复传输失败时使用指数退避。
指数退避:如果请求往返于服务器和客户端之间的时间超过了最初设置的等待时间,那么应该延长该等待时间。
请求ID是解决重复响应问题的重要利器。重复响应问题指的是,我们收到所有数据包后,又收到了一个被认为已经丢失的响应。此时可能会把该响应误认为是当前请求的响应。如果随机选择请求ID的话,就可以预防最简单的电子欺诈攻击。
使用套接字时有一点至关重要,那就是区分绑定(binding)和客户端的连接(connecting)这两个行为。绑定指定了要使用的特定UDP端口,而连接限制了客户端可以接收的响应,表示只接收从正在连接的特定服务器发来的数据包。
在可用于UDP套接字的套接字选项中,功能最强大的就是广播。使用广播可以一次向子网内的所有主机发送数据包,而无需向每台主机单独发送。这在编写本地LAN游戏或其他协作计算程序时是很有用的。这也是在编写新应用程序时选用UDP的原因之一。
UDP是比较原始的协议,需要我们自己来处理有关于报错的情况,使用TCP协议则会自动帮我们处理这些问题。
TCP的工作原理如下:每个TCP数据包都有一个序列号,接收方通过该序列号将响应数据包正确排序。也可通过该序列号发现传输序列中丢失的数据包,并请求进行重传。TCP并不使用顺序的整数(1、2、3……)作为数据包的序列号,而是通过一个计数器来记录发送的字节数。例如,如果一个包含1024字节的数据包的序列号为7200,那么下一个数据包的序列号就是8224。这意味着,繁忙的网络栈无需记录其是如何将数据流分割为数据包的。当需要进行重传时,可以使用另一种分割方式将数据流分为多个新数据包(如果需要传输更多字节的话,可以将更多数据包装入一个数据包),而接收方仍然能够正确接收数据包流。在一个优秀的TCP实现中,初始序列号是随机选择的。这样一来,不法之徒就无法假设每个连接的序列号都从零开始。如果TCP的序列号易于猜测,那么伪造数据包就容易多了。可以将数据包伪造成一个会话的合法数据,这样就有可能攻击这个会话了。这对于我们来说可不是件幸运的事儿。TCP并不通过锁步的方式进行通信,因为如果使用这种方式,就必须等待每个数据包都被确认接收后才能发送下一个数据包,速度非常慢。相反,TCP无须等待响应就能一口气发送多个数据包。在某一时刻发送方希望同时传输的数据量叫作TCP窗口(window)的大小。接收方的TCP实现可以通过控制发送方的窗口大小来减缓或暂停连接。这叫作流量控制(fowcontrol)。这使得接收方在输人缓冲区已满时可以禁止更多数据包的传输。此时如果还有数据到达的话,那么这些数据也会被丢弃,最后,如果TCP认为数据包被丢弃了,它会假定网络正在变得拥挤,然后减少每秒发送的数据量。这对于无线网络和其他会因为简单的噪声而导致丢包的媒体来说可是个灾难。它会破坏本来运行良好的连接,导致通信双方在一定时间内(比如20秒)无法通信,直到路由器重启通信才能恢复正常。网络重新连接时,TCP通信双方会认为网络负载已经过重,因此一开始就会拒绝向对方发送大型数据。
基于TCP的“流”套接字提供了所有必需的功能,包括重传丢失数据包、重新排列接收到的顺序错误的数据包,以及将大型数据流分割为针对特定网络的具有最优大小的数据包。这些功能提供了对在网络上两个套接字之间传输并接收数据流的支持。
跟UDP一样的是,TCP也使用端口号来区分同一台机器上可能存在的多个流端点。想要接收TCP连接请求的程序需要通过bind()绑定到一个端口,在套接字上运行1isten(),然后进入一个循环,不断运行accept(),为每个连接请求新建一个套接字(该套接字用于与特定客户端进行通信)。如果程序想要连接到已经存在的服务器端口,那么只需要新建一个套接字,然后调用connect()连接到一个地址即可。
服务器通常都要为绑定的套接字设置SOREUSEADDR选项,以防同一端口上最近运行的正在关闭中的连接阻止操作系统进行绑定。
实际上,数据是通过send()和recv()来发送和接收的。一些基于TCP的协议会对数据进行标记这样客户端和服务器就能够自动得知通信何时完成。其他协议把TCP套接字看作真正的流,会不断发送和接收数据,直到文件传输结束。套接字方法shutdown()可以用来为套接字生成一个方向上的文件
结束符(所有套接字本质上都是双向的),同时保持另一方向的连接处于打开状态。如果通信双方都写数据,套接字缓冲区被越来越多的数据填满,而这些数据却从未被读取,那么就可能会发生死锁。最终,在某个方向上会再也无法通过send()来发送数据,然后可能会永远等待缓冲区清空,从而导致阻塞。如果想要把一个套接字传递给一个支持读取或写人普通文件对象的Python模块,可以使用makefile()方法。该方法返回一个Python对象。调用方需要读取及写人数据时,该对象会在底层调用recv()和send()。
Python程序通常需要将主机名转换为可以实际连接的套接字地址
多数主机名查询都应该通过socket模块的getsockaddr()函数完成。这是因为,该函数的智能性通常是由操作系统提供的。它不仅知道如何使用所有可用的机制来查询域名,还知道本地IP栈配置支持的地址类型(IPv4或IPv6)。
传统的IPv4地址仍然是互联网上最流行的,但IPv6正在变得越来越常见。通过使用getsockaddr()进行主机名和端口号的查询,Python程序能够将地址看成单一的字符串,而无需担心如何解析与解释地址。
DNS是多数名称解析方法背后的原理。它是一个分布在世界各地的数据库,用于将域名查询直接指向拥有相应域名的机构的服务器,将域名转化为对应的ip地址(服务器)。尽管在Python中直接使用原始DNS查询的频率不高,但是它在基于电子邮件地址中@符号后的域名直接发送电子邮件时还是很有帮助的.
要把机器信息存放到网络上,就必须先进行相应的转换。无论我们的机器使用的是哪种私有的特定存储机制,转换后的数据都要使用公共且可重现的表示方式。这样的话,其他系统和程序,甚至其他编程语言才能够读取这些数据。
要把机器信息存放到网络上,就必须先进行相应的转换。无论我们的机器使用的是哪种私有的特定存储机制,转换后的数据都要使用公共且可重现的表示方式。这样的话,其他系统和程序,甚至其他编程语言才能够读取这些数据。
对于文本来说,最重要的问题就是选择一种编码方式,将想要传输的字符转换为字节。这是因为,包含8个二进制位的字节是IP网络上的通用传输单元。我们需要格外小心地处理二进制数据,以确保字节顺序能够兼容不同的机器。Python的struct模块就是用来帮助解决这个问题的。有时候,最好使用JSON或XML来发送数据结构和文档。这两种格式提供了在不同机器之间共享结构化数据的通用方法。
使用TCP/IP流时,我们会面临的一个重要问题,那就是封帧,即在长数据流中,如何判定一个特定消息的开始与结束。为了解决这个问题,有许多技术可供选用。由于recv()每次可能只返回传输的部分信息,因此无论使用哪种技术,都需要小心处理。为了识别不同的数据块,可以使用特殊的定界符或模式、定长消息以及分块编码机制来设计数据块
Python的pickle除了能把数据结构转换为能用于网络传输的字符串外,还能够识别接收到的pickle的结束符。这使得我们不仅可以使用pickle来为数据编码,也可以使用pickle来为单独的流消息封帧。压缩模块zlib通常会和HTTP一起使用。它也可以识别压缩的数据段何时结束,也因此提供了一种花销不高的封帧方法。与我们的代码使用的网络协议一样,套接字也可以抛出各种异常。
何时使用try...except从句取决于代码的用户--我们是为其他开发者编写库还是为终端用户编写工具?除此之外,这一选择也取决于代码的语义。如果从调用者或终端用户的角度来看,某个代码段进行的是同一个较为宏观的操作那么就可以将整个代码段放在一个try...except从句中。
最后,如果某个操作引发的错误只是暂时的,而调用晚些时候可能会成功,并且我们希望该操作能自动重试的话,就应将其单独包含在一个try...except从句中。
在一个典型的TLS交换场景中,客户端向服务器索取证书--表示身份的电子文件。客户端与服务器共同信任的某个机构应该对证书进行签名。证书中必须包含一个公钥。之后服务器需要证明其确实拥有与该公钥对应的私钥。客户端要对证书中声明的身份进行验证,确定该身份是否与想连接的主机名匹配。最后,客户端与服务器就加密算法、压缩以及密钥这些设定进行协商,然后使用协商通过的方案对套接字上双向传输的数据进行保护
许多管理员甚至都没有尝试在他们的应用程序中支持TLS。反之,他们把应用程序隐藏在了工业强度的前端工具之后,比如Apache、nginx或是HAProxy这些可以自己提供TLS功能的工具。在前端使用了内容分发网络的服务也必须把支持TLS功能的责任留给第三方工具,而不是将其嵌入自己的应用程序中。
尽管网络搜索的结果会提供一些使用第三方库在Python中提供TLS支持的建议,不过Python标准库的ss1模块实际上已经内置了对OpenSSL的支持。如果我们的操作系统以及Python版本上支持ss1模块,而且它能正常工作,那么只需要一个服务器的证书,就可以建立基本的加密连接。由Python 3.4或更新版本Python(如果应用程序要自己提供TLS支持,强烈建议至少使用3.4版本编写的应用程序通常会遵循如下模式:先创建一个“上下文”对象,然后打开连接,调用上下文对象的wrap socket()方法,表示使用TLS协议来负责后续的连接。尽管可以在旧式风格的代码中看到ss]模块提供的一个或两个简短形式的函数,但是上下文-连接-包装这一模式才是最通用,也是最灵活的许多Python客户端和服务器都能够直接接受ss1.create default context()返回的默认“上下文对象作为参数,并使用该对象提供的设置。服务器使用默认设置时设置更为严格,而客户端使用默认设置时则较为宽松一些,这样客户端就能够成功连接到一些只支持旧版本TLS的服务器了。其他Python应用程序为了根据它们的特定需求定制协议及加密算法,会自己实例化SSLContext对象。
SSL工作原理如下:握手协议:在建立连接之前,客户端和服务器之间会进行一个握手协议。这包括以下步骤:客户端Hello:客户端向服务器发送一个Hello消息,其中包含支持的SSL/TLS版本、加密算法、压缩方法等信息。服务器Hello:服务器回复一个Hello消息,确认使用的协议版本和加密套件。密钥交换:服务器向客户端发送一个公钥,用于加密通信。客户端生成一个随机的对称密钥,使用服务器的公钥进行加密,并将其发送回服务器。会话密钥生成:服务器使用私钥解密客户端发送的密文,获取对称密钥。现在,客户端和服务器都有了相同的会话密钥,用于加密和解密数据。
数据加密和认证:一旦握手完成,客户端和服务器之间的通信就可以开始了。SSL使用对称密钥加密算法(如AES)来加密数据,确保数据在传输过程中不被窃取。此外,SSL还使用数字证书来验证服务器的身份,防止中间人攻击。
数字证书:服务器使用数字证书来证明其身份。数字证书由受信任的证书颁发机构(CA)签发,其中包含服务器的公钥和其他信息。客户端可以验证证书的有效性,确保连接到的是合法的服务器。
使用多线程时通常可以不加修改地使用单线程服务器程序,操作系统会负责隐式地完成切换,使得等待中的客户端能够快速得到响应而空闲的客户端则不会消耗服务器的CPU。这一技术不仅允许同时进行多个客户端会话,而且很好地利用了服务器的CPU。而对于原始的单线程服务器,由于其大多数时间都在等待客户端的操作,因此CPU在很多时候都是空闲的。
更复杂但是更强大的方法是使用异步编程的风格在单个控制线程中完成对大量客户端的服务切换。这种方法向操作系统提供了当前正在进行会话的完整套接字列表。复杂之处在于需要将读取客户端请求然后构造响应的过程分割为小型的非阻塞代码块,这样就能在等待客户端操作时将控制权交还给异步框架。
异步:发送一个消息给一个客户端,不必等待回应,直接给下一个准备好的客户端发消息。
尽管可以通过select()或po11()这样的机制手动编写异步服务器,不过多数程序员还是会使用一个框架来提供异步功能,比如Python3.4或更新版本Python标准库中内置的asyncio框架。将编写的服务安装到服务器上,并且在系统启动时运行服务器的过程叫作部署(deployment)。可以使用许多现代机制进行自动化部署,比如使用supervisord这样的工具或是将控制权交给一个“平台即服务”容器。在一台基本的Linux服务器上可以使用的最简单的部署方法可能就是古老的inetd守护进程了。inetd提供了一种极其简单的方法,能够在客户端需要连接时保证服务处于启动状态。
消息队列是另一个为应用程序的不同部分提供协作与集成功能的机制。在协作与集成过程中,可能需要不同的硬件、负载均衡技术、平台,甚至是编程语言。普通的TCP套接字只能提供点对点连接的功能,但是消息队列能够将消息发送到多个处于等待状态的用户或服务器。
消息队列同样也可以使用数据库或其他持久化存储机制来保证消息在服务器未正常启动时不会丢失。除此之外,由于系统的一部分暂时成为性能的瓶颈时,消息队列允许将消息存储在队列中等待服务,因此消息队列也提供了可恢复性和灵活性。消息队列隐藏了为特定类型的请求提供服务的服务器或进程,因此在断开服务器连接、升级服务器、重启服务器以及重连服务器时无需通知系统的其余部分。
许多程序员会通过友好的API来使用消息队列,比如Django社区中非常流行的Celery项目。Celery也可以使用Redis来作为后端。
Redis与消息队列的相似之处在于它们都支持FIFO(先进先出队列).
消息队列的基本原理如下:
HTTP协议用于根据保存资源的主机名和路径来获取资源。标准库的urllib客户端提供了在简单情况下获取资源所需的基本功能。但是,比起Requests,urlib的功能就弱了很多。Requests提供了许多urllib没有的特性,是互联网上最热门的Python库。程序员如果想要从网上获取资源的话,Requests是最佳选择。
HTTP运行于80端口,通过明文发送。而通过TLS保护的HTTP(HTTPS)则在443端口运行。客户端的请求和服务器的响应在传输过程中都使用相同的基本结构:首行信息,然后是若干行由名字和值组成的HTTP头信息,最后是一个空行,然后是可选的消息体。消息体可以使用多种不同的方式进行编码和分割。客户端总是先发送请求,然后等待服务器返回响应。最常用的HTTP方法是用于获取资源的GET和用于更新服务器信息的POST。除了GET和POST之外,还有其他方法,不过本质上都与GET或POST类似。服务器在每个响应中都会返回一个状态码。表示请求成功、失败或需要客户端重定向以载入另一个资源。
HTTP的设计采用了像同心圆一样的分层结构。可以对头信息进行缓存,将资源存储在客户端的缓存中,这样可以重复使用资源,避免不必要的重复获取。这些缓存的头信息也可以避免服务器重复
发送没有修改过的资源。这两种优化方法对于繁忙站点的性能都至关重要。内容协商可以保证根据客户端和人类用户的真实偏好来决定返回的数据格式和语言。不过在实际应用中,内容协商会带来一些问题,这使得它没有得到广泛应用。内置的HTTP认证在交互设计上很糟糕,已经被自定义的登录页面和cookie替代。不过,在使用TLS保护的API时,有时还是会使用基本认证。
HTTP/1.1版的连接在默认情况下是保持打开并且可以复用的,而Requests库也在需要的时候精心提供了这一功能。
import queue
import threading
import time
# 创建一个队列对象
message_queue = queue.Queue()
def producer():
for i in range(5):
item = f"Message {i}"
print(f"Producing {item}")
message_queue.put(item) # 将消息放入队列
time.sleep(1) # 模拟生产消息的延迟
def consumer():
while True:
item = message_queue.get() # 从队列中获取消息
if item is None: # 如果获取到None,表示生产者结束生产
break
print(f"Consuming {item}")
time.sleep(2) # 模拟处理消息的延迟
# 创建生产者和消费者线程
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
# 启动线程
producer_thread.start()
consumer_thread.start()
# 等待生产者线程完成
producer_thread.join()
# 向消费者线程发送结束信号
message_queue.put(None)
# 等待消费者线程完成
consumer_thread.join()
import queue
import threading
import time
# 创建一个队列对象
message_queue = queue.Queue()
def producer(thread_id):
for i in range(5):
item = f"Message {i} from producer {thread_id}"
print(f"Producer {thread_id} producing {item}")
message_queue.put(item)
time.sleep(1)
def consumer(thread_id):
while True:
item = message_queue.get()
if item is None:
break
print(f"Consumer {thread_id} consuming {item}")
time.sleep(2)
# 创建多个生产者和消费者线程
producer_threads = [threading.Thread(target=producer, args=(i,)) for i in range(2)]
consumer_threads = [threading.Thread(target=consumer, args=(i,)) for i in range(2)]
# 启动所有生产者线程
for thread in producer_threads:
thread.start()
# 启动所有消费者线程
for thread in consumer_threads:
thread.start()
# 等待所有生产者线程完成
for thread in producer_threads:
thread.join()
# 向消费者线程发送结束信号
for _ in range(len(consumer_threads)):
message_queue.put(None)
# 等待所有消费者线程完成
for thread in consumer_threads:
thread.join()
import asyncio
async def producer(queue, id):
for i in range(5):
item = f"Message {i} from producer {id}"
print(f"Producer {id} producing {item}")
await queue.put(item) # 异步放入队列
await asyncio.sleep(1) # 模拟生产消息的延迟
async def consumer(queue, id):
while True:
item = await queue.get() # 异步从队列中获取消息
if item is None:
break
print(f"Consumer {id} consuming {item}")
await asyncio.sleep(2) # 模拟处理消息的延迟
async def main():
queue = asyncio.Queue()
producers = [asyncio.create_task(producer(queue, i)) for i in range(2)]
consumers = [asyncio.create_task(consumer(queue, i)) for i in range(2)]
await asyncio.gather(*producers)
# 向消费者发送结束信号
for _ in range(len(consumers)):
await queue.put(None)
await asyncio.gather(*consumers)
# 运行异步任务
asyncio.run(main())