每日分享
The secret to your success is found in your daily routine.
成功的秘诀在于您的日常生活。
小闫语录:
认真对待生活中的每一件事,让自己的生活自律起来,不抱怨,不丧。每天早起看一下初升的太阳,喝一杯牛奶,开始元气满满的一天。坚持数月,你会发现生活的质变。
1.5网络编程
上篇文章传送门『我是个链接』
上篇文章对 Linux 的一些命令和操作系统的一些机制做了归纳概括,学习上一部分内容最简单的办法就是系统换成 Linux ,经常使用,孰能生巧。Mac 买不起,可以给自己电脑安装 CentOS 或者 Ubuntu,Deepin 也不错。
本篇文章将开始网络编程的相关内容,开始咯~
1.5.1网络协议 TCP/UDP/HTTP
1.5.1.1浏览器输入一个 url 中间经历的过程
由于本人对 ARP 协议以及一些更深层的原理过程不是很熟悉,故此处只涉及到传输层和应用层。
TCP 在传输层,IP 在网络层,HTTP 在应用层。OSI
模型可以查看文章『python技术面试题(二)』
1.浏览器发起请求的时候,一开始其实做的不是 DNS 查询,而是先看一下 DNS 缓存。
2.如果不在缓存里面,那么去看看本地的 hosts 文件。
3.如果还是没找到,那么就会向本地的 DNS 服务器发起一次查询,如果本地的没有查到,那么它就向上层服务器进行查询,也就是域名服务器和根服务器。
4.查询完之后,DNS 服务器返回给我们一个对应的 IP 地址。
5.获取到 IP 地址,浏览器就可以调用 socket 函数发起 TCP 的请求了。也就是非常著名的三次握手。通过三次握手与服务器进行连接。
6.建立好连接之后,就可以发起应用层的 HTTP 请求了。
7.注意请求不是直接到我们的 web 应用了,而是先经过反向代理,一般为 Nginx(负载均衡的作用)。
8.然后会到达 uwsgi/gunicorn 这一层。它们主要就是兼容我们的 web 应用。uwsgi 是实现了 WSGI 协议的一个 web 服务器,它可以将客户端请求转发到 web 应用进行处理。
9.web 应用层的 Django 或者 Flask 等等框架接收到请求,就会进行一些业务逻辑的处理。处理完之后发送给 web 服务器,服务器通过 TCP 发送给了浏览器,浏览器接收到数据之后通过浏览器自己的渲染功能来显示这个网页。
10.如果没有什么数据要传输了,最后就关闭 TCP 的连接,完成四次挥手,结束整个过程。
1.5.1.2 TCP 三次握手/四次挥手
前面已经讲的足够详细了,大家可以参考一下这篇文章『python技术面试题(五)』。此处我们简单的进行回顾即可。
首先是三次握手,三次握手的过程说简单也简单说难也难。为了确保连接的可靠性,我们需要确保对方都在状态。首先客户端发起连接请求,将标志位 SYN 置为 1,然后生成一个随机的序列号 seq,假设为 J。发送给服务端之后,客户端进入 SYNSENT 状态。服务器收到数据包:哦,原来客户端要和我连接。然后做出应答,将 ACK 置为 1,ack 设置为客户端的随机序列号加 1 ,并没有结束,它同样需要将 SYN 置为 1,生成一个一个随机序列号 seq,然后将此 TCP 包发送给客户端,至此服务器进入 SYNRCVD 状态。过程仍然没有结束,因为服务器需要确保客户端收到这个数据包。客户端收到并检查数据包的可连接状态之后,返回一个数据包,将 ACK 置为 1,然后 ack 为服务器的随机序列号加 1。服务端检查也没什么问题了,它们就都进入了 ESTABLISHED 状态,完成三次握手,建立可靠连接。
我们可以看到有三次数据包的传输过程,这个过程形象的被称为三次握手。
SYN: 表示连接请求
ACK: 表示确认
FIN: 表示关闭连接
seq:表示报文序号
ack: 表示确认序号
然后说明一下四次挥手:
1.第一次挥手:Client 发送一个 FIN,用来关闭 Client 到 Server 的数据传送。
2.第二次挥手:Server 收到 FIN 后,发送一个 ACK 给 Client,确认序号为收到序号+1。
3.第三次挥手:Server 发送一个 FIN,用来关闭 Server 到 Client 的数据传送。
4.第四次挥手:Client 收到 FIN 后,接着发送一个 ACK 给 Server,确认序号为收到序号+1。
注意:四次挥手中有一个需要注意的地方,就是 TCP 的 2MSL。也就是主动发送 FIN 关闭的一方,在 4 次挥手最后一次要等待一段时间,这一段时间就是 2MSL。至于为什么需要有,详细请看『python技术面试题(五)』
1.5.1.3 TCP/UDP 的区别
TCP 是面向连接的,需要三次握手建立连接,四次挥手断开连接。它也是可靠的,有一些机制比如应答机制、超时重传机制、错误校验机制、流量控制和阻塞管理机制,确保传输的数据无误。同时它还是基于字节流的,我们都晓得 TCP 有一个缓冲区,应用层使用 TCP 的 socket 发送请求之后,TCP 会把在缓冲区应用层发送的数据进行分段的发送。
UDP 就是面向无连接的,也就是收不收是你的事,发不发是我的事,我发送了,就没有我的事了。正是基于此特点, UDP 适合做广播。它是不可靠的,有可能出现丢包的情况。他还是面向报文的,有多少数据给你一次性发送多少数据。
1.5.2 HTTP 协议
1.5.2.1 HTTP 请求的组成
1.状态行:请求方法、资源路径、HTTP 版本
2.请求头:格式为『头名称:对应的值』,常见的有 Host、Connection、User-Agent等。
3.消息主体。提交的一些表单数据。
在命令行中如何查看接收到的消息呢?有两个常用的方法:
第一个使用 curl 命令,我们以访问百度为例:
curl www.baidu.com
它会返回请求数据。
第二个使用 http 命令,同样是访问百度:
# 首先先安装 pip install httpie # 然后使用 http baidu.com # 打印详细的请求过程 http -v baidu.com
1.5.2.2 HTTP 响应的组成
1.状态行:HTTP 版本,状态码,状态
2.响应头:格式为『头名称:对应的值』
3.响应正文
1.5.2.3 HTTP 常见状态码
1xx 信息。服务器收到请求,需要请求者继续执行操作
2xx 成功。操作被成功接受并处理
3xx 重定向。需要进一步操作完成请求。301 永久重定向,302 临时重定向,304 请求被允许,而且文档没有改变。
4xx 客户端错误。请求语法错误或者无法完成请求。400 Bad request。403 Forbidden。404 Not Found。405 Method not allowed。
5xx 服务器错误。服务器在处理器请求的过程中发生错误。500 Internal server error。502 Bad gateway(网关错误)
1.5.2.4 HTTP 的 GET/POST 区别
在工作中常用的方法其实只有下面的几个:GET 获取;POST 创建;PUT 更新;DELETE 删除。和 Restful 语义相对应的。下面我们详细说一下 GET/POST。
1.Restful 语义上一个是获取,一个是创建。
2.GET 是幂等的,POST 是非幂等。当我们每次发送 GET 请求时对服务器没什么副作用,而 POST 创建的时候都会改变数据库的数据。
3.GET 请求参数放到 url(明文传输),它有长度限制;POST 放在请求体中,更安全。
1.5.2.5什么是幂等方法
幂等方法就是无论调用多少次都得到相同结果的 HTTP 方法。例如 a = 4 就是幂等的,而 a += 4 就是非幂等的。幂等的方法客户端可以安全的重发请求。
GET 、PUT、DELETE 是幂等的。因为获取数据,还有修改数据(类似于上面的 a = 4 赋值操作),删除数据,是么每次执行相同操作,得到的结果是一样的。
但是 POST 就不是幂等的,比如你发送一条数据,我创建了一条,再发再创建。它也是不安全的操作。那么怎么实现安全操作呢?我们需要在服务端做校验,比如针对同一个邮箱只能创建一个账户,提交之后看邮箱是否被创建过用户,创建过不再创建,否则就创建。
有可能你会听到一个词,就是这个方法是否是 Safe 的。幂等是 Idempotent,安全是 Safe。安全指的是是否修改服务端的数据。
我们可以通过下图进行查看:
PATCH 是局部更新资源,比 PUT 更节省带宽。
1.5.2.6什么是 HTTP 长连接
HTTP persistent connection,HTTP 1.1 中实现了长连接。
短连接:建立链接 -> 数据传输 -> 关闭连接
连接的建立和关闭开销比较大
长连接:Connection:Keep-alive。保持 TCP 连接不断开。
长连接就是连接之后不断开,等待着下一个 HTTP 请求的发送。大家仔细思考一下,就会发现有一个问题:既然多个 HTTP 请求通过同一个 TCP 连接发送过去,那么我们如何区分不同的 HTTP 请求呢?答案其实也简单,就是客户端告诉服务端你发送的 HTTP 请求有多长不就好了吗?这就是一种方案。
我们可以使用 Content-Length 告诉服务端多长。但是如果请求是动态生成的,我也不晓得它有多长怎么办呢?那就使用 HTTP 提供的另外一种分段发送的方式 Transfer-Encoding:chunked。每次发送一个 chunked size 和chunked body,告知服务器每次发送多长,直到 size 为 0,就代表发送结束了。
1.5.2.7 cookie 和 session 的区别
众所周知, HTTP 是无状态的,每一次发送完 HTTP 请求之后,下一次发送的和上一次没什么关系。那么如何识别用户呢?我们需要在服务端给用户生成一个标识,然后每次让客户端带过去给后端。 这就是我们通常识别用户会话的一个思路。
1.Session 一般是服务器生成之后给客户端(通过 url 参数或者 cookie)
2.Cookie 是实现 session 的一种机制,通过 HTTP cookie 字段实现
3.Session 通过在服务器保存 sessionid 识别用户,cookie 存储在客户端。
1.5.3网络编程
1.5.3.1 TCP socket 编程原理
我们需要从下列问题入手:
1.如何使用 socket 模块
2.如何建立 TCP socket 客户端和服务端
3.客户端和服务端之间的通信
上面的图片就是 TCP 的 socket 编程流程图。
下面我们通过编写客户端和服务端的小案例来理解一下 TCP socket 编程原理:
tcp_client.py 1. import socket 2. 3. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4. s.connect(('127.0.0.1', 8888)) 5. s.sendall(b'Hello world') 6. data = s.recv(1024) 7. print(data) 8. s.close() tcp_server.py 1. import socket 2. import time 3. 4. s = socket.socket() 5. s.bind(('', 8888)) 6. s.listen() 7. 8. while True: 9. client, addr = s.accept() 10. print(client) 11. timstr = time.ctime(time.time()) + '\r\n' 12. # send 参数 encode('utf-8') 13. client.send(timestr.encode()) 14. client.close()
1.5.3.2如何使用 socket 发送 HTTP 请求
1.使用 socket 接口发送 HTTP 请求
2.HTTP 建立在 TCP 基础之上
3.HTTP 是基于文本的协议
下面我们建立一个文件 socket_send_http.py
,然后编写代码发送请求:
import socket s = socket.socket() s.connect(('www.baidu.con', 80)) http = b"GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n" s.sendall(http) buf = s.recv(1024) print(buf) s.close()
1.5.4 IO 多路复用
一旦涉及到并发编程的时候,IO 多路复用是一个绕不开的话题。就比如我们知道的 Tornado 框架是如何实现高并发的,它底层其实是使用了操作系统提供的 IO 多路复用机制。
1.5.4.1五种 IO 模型
Unix 网络编程中提到了5种网络模型
1.Blocking IO(阻塞的 IO)
2.Nonblocking IO(非阻塞的 IO)
3.IO multiplexing(IO 多路复用 )
4.Signal Driven IO(信号驱动的 IO)
5.Asynchronous IO(异步 IO)
一般使用 IO 多路复用比较多
1.5.4.2如何提升服务器的并发能力呢?
1.多线程模型,创建新的线程处理请求
2.多进程模型,创建新的进程处理请求
线程/进程创建开销比较大,可以用线程池方式解决
线程和进程比较占用资源,难以同时创建太多
上面提到的问题也就限制了我们难以通过多进程和多线程在服务器上处理成千上万的请求。
3.IO 多路复用,实现单进程同时处理多个 socket 请求。(现在的服务器都用此方法解决并发的问题)
1.5.4.3什么是 IO 多路复用
为了实现高并发需要一种机制并发处理多个 socket 。操作系统提供了同时监听多个 socket 的机制。比如 Linux 常见的 select、poll 和 epoll,他它就实现了使用单线程单进程处理多个 socket。
我们先来看一下阻塞式 IO 的请求的流程图:
当调用 recvfrom 的时候,其实经历了两个流程。一个是操作系统内核等待数据的过程,另一个是将数据从内核拷贝到用户进程的过程,两个过程完成之后,应用程序才能拿到数据。因为一次只能处理一个请求,所以效率好低~
然后我们的主角登场:通过上面的阻塞式 IO 衬托 IO 多路复用。
1.5.4.4 IO 多路复用
我们 select 调用的时候,第一步还是内核等待数据,但是仅仅阻塞在这一步。一旦 socket 返回了可读之后,我们调用 recvfrom 直接就能拿到数据。你有可能看到在等待数据的时候还是阻塞的,疑惑为什么 select 就能实现高并发了呢?原因就是 select 可以同时处理多个 socket,有一个就绪应用程序代码就可以处理它。一般它底层的代码形式是这样的:
while True: events = sel.select() # 一旦有一个 socket 就绪之后,我们就遍历事件,处理 socket for key, mask in events: callback = key.data callback(key.fileobj, mask)
1.5.4.5 select/poll/epoll 区别
1.5.4.5.1 select
事件集合:用户通过 3 个参数分别传入感兴趣的可读、可写以及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用 select 都要重置这 3 个参数。
应用程序索引就绪文件描述符的时间复杂度:O(n)
最大支持文件描述符数:一般有最大限制
工作模式:LT
内核实现和工作效率:采用轮询方式来检测就绪事件,算法时间复杂度为O(n)
1.5.4.5.2 poll
事件集合:统一处理所有事件类型,因此只需一个事件集参数。用户通过 pollfd.events 传入感兴趣的时间,内核通过修改 pollfd.revents 反馈其中就绪的事件。
应用程序索引就绪文件描述符的时间复杂度:O(n)
最大支持文件描述符数:65535
工作模式:LT
内核实现和工作效率:采用轮询方式来检测就绪事件,算法时间复杂度为O(n)
1.5.4.5.3 epoll
事件集合:内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用 epollwait 时,无序反复传入用户感兴趣的事件。epollwait 系统调用的参数 events 仅用来反馈就绪的事件。
应用程序索引就绪文件描述符的时间复杂度:O(1)
最大支持文件描述符数:65535
工作模式:支持 ET 高效模式
内核实现和工作效率:采用回调方式来检测就绪事件,算法时间复杂度为O(1)
1.5.4.6 Python 如何实现 IO 多路复用
1.Python 的 IO 多路复用基于操作系统实现(select/poll/epoll)
2.Python2 select 模块
3.Python3 selectors 模块
selectors 模块
事件类型:EVENTREAD, EVENTWRITE
DefaultSelector类:自动根据平台选取和是的 IO 模型
1.register(fileobj, events, data=None)
注册方法,fileobj 是文件描述符;event 是 socket 可读还是可写的事件。
2.unregister(fileobj)
取消监听 socket
3.modify(fileobj, events, data=None)
先执行 unregister,再执行 register。简单的说就是上俩个方法的简化
4.select(timeout=None): returns[(key, events)]
会阻塞,直到有文件描述符就绪,可读或者可写了,我们返回这些文件描述符以及注册回调。
5.close()
下面是 Python 文档中 selectors 模块的一个官方示例,实现异步 TCP 回显服务器:
他可以并发的处理客户端的并发请求
import selectors import socket sel = selectors.DefaultSelector() def accept(sock, mask): conn, addr = sock.accept() # Should be ready print('accepted', conn, 'from', addr) conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, read) def read(conn, mask): data = conn.recv(1000) # Should be ready if data: print('echoing', repr(data), 'to', conn) conn.send(data) # Hope it won't block else: print('closing', conn) sel.unregister(conn) conn.close() sock = socket.socket() sock.bind(('localhost', 1234)) sock.listen(100) sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) while True: events = sel.select() for key, mask in events: callback = key.data callback(key.fileobj, mask)
1.5.5 Python 并发网络库
其实 Python 的各种并发网络库底层也是使用了上面提到的 IO 多路复用。
1.5.5.1并发网络库
1.Tornado 并发网络库,同时也是一个 web 微框架。
2.Gevent 绿色线程(greenlet)实现并发,猴子补丁修改内置 socket。
3.Asyncio Python3 内置的并发网络库,基于原生协程。
第三个还是不太稳定,目前没有大型的项目去使用,还没有经过检验,所以不推荐使用
1.5.5.2 Tornado 框架
Tornado 适用于微服务,实现 Restful 接口。它底层基于 Linux 多路复用,可以通过协程或者回调实现异步编程,不过生态不完善,响应的异步框架比如 ORM 不完善。下面举一个简单的小栗子:
import tornado.ioloop import tornado.web from tornado.httpclient import AsyncHTTPClient class APIHandler(tornado.web.RequestHandler): async def get(self): url = 'http://httpbin.org/get' http_client = AsyncHTTPClient() resp = await http_client.fetch(url) print(resp.body) return resp.body def make_app(): return tornado.web.Application([ (r"/api", APIHandler), ]) if __name__ == "__main__": app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
1.5.5.3 Gevent
高性能的并发网络库
1.基于轻量级绿色线程(greenlet)实现并发
2.需要注意 monkey patch,gevent 修改了内置的 socket 改为非阻塞
3.配合 gunicorn 和 gevent 部署作为 wsgi server
《Gevent 程序员指南》这本书不错,大家可以学习一下。
下面我们实现一个小案例:
import gevent.monkey # 修改内置的一些库为非阻塞 gevent.monkey.patch_all() import gevent import requests def fetch(i): url = 'http://httpbin.org/get' resp = requests.get(url) print(len(resp.text), i) def asynchronous(): threads = [] for i in range(1, 10): threads.append(gevent.spawn(fetch, i)) gevent.joinall(threads) print('Asynchronous:') asynchronous()
1.5.5.4 Asyncio
它是基于协程实现的内置并发网络库,是 Python3 的时候引入到内置库中(协程 + 时间循环)。但是它的生态不够完善,没有大规模生产环境检验,因此应用也不够广泛,基于 Aiohttp 可以实现一些小的服务。
此处再引入一个网上的小案例,不做过多解释,因为不太重要。大家可以通过查阅官方文档去了解:
import asyncio from aiohttp import ClientSession # pip install aiohttp asyncc def fetchurl,,esession:: async with session.get(url) as response: return await response.read() async def run(r=10): url = "http://httpbin.org/get" tasks = [] async with ClientSession() as session: for i in range(r): task = asyncio.ensure_future(fetch(url, session)) task.append(task) responses = await asyncio.gather(*tasks) for resp_body in responses: print(len(resp_body)) loop = asyncio.get_event_loop() future = asyncio.ensure_future(run()) loop.run_until_complete(future)