Python3 - 网络编程

1 简介

对于TCP,服务端发送数据用send,接收数据用recv;客户端发送数据用send,接收数据用recv。

对于UDP,服务端发送数据用sendto,接收数据用recvfrom;客户端发送数据用sendto,接收数据用recv。

2 TCP

2.1 Server

这里只介绍用多线程实现server端,还可以用多进程,进程池,线程池实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import socket, threading, time

# 线程函数。sock为用于此连接新建的socekt,addr为客户端ip和客户端进程的端口号组成的tuple。
def tcplink(sock, addr):
print("Accept new conection from %s:%s..." % addr)
sock.send(b"Welcome!")

while True:
# 一次最多只能接受1024字节的数据。
data = sock.recv(1024)
time.sleep(1)

if not data or data.decode("utf-8") == "exit":
break

# 发送数据。
sock.send(("Hello, %s!" % data).encode("utf-8"))

# 关闭用于这个连接的新建的socket。
sock.close()
print("Connection from %s:%s closed." % addr)



# 建立socket。第一个参数表示使用的是ipv4,第二个参数表示使用的是tcp。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将服务器的ip和服务端进程的端口号绑定到建立的socket中。
s.bind(("127.0.0.1", 9999))

# 监听。5表示最多只允许5个未建立连接的客户端请求排队等待。
s.listen(5)
print("Waiting for connection...")

while True:
# 建立一个连接,并返回一个用于此次连接新建的socket,以及客户端的ip地址和客户端进程的端口号组成的tuple。
sock, addr = s.accept()

# 针对此连接开一个线程去处理。
t = threading.Thread(target = tcplink, args = (sock, addr))
t.start()

s.close()

2.2 Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import socket

# 建立socket。第一个表示使用的是ipv4,第二个参数表示使用的是tcp。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务端。参数为服务端ip地址和服务端进程的端口号组成的tuple。
s.connect(("127.0.0.1", 9999))

print(s.recv(1024).decode("utf-8"))

for data in [b"Python", b"C++", b"Java"]:
# 发送数据。
s.send(data)
# 接收数据。
print(s.recv(1024).decode("utf-8"))

# 发送结束标记。
s.send(b"exit")
# 关闭此次连接。
s.close()

3 UDP

3.1 Server

简单的UDP服务端可以只用单线程实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket

# 建立socket。第一个表示使用的是ipv4,第二个参数表示使用的是udp。
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 将服务器的ip和服务端进程的端口号绑定到建立的socket中。
s.bind(('127.0.0.1', 9999))
# 不需要调用listen()方法,而是直接接收来自任何客户端的数据:

print('Bind UDP on 9999...')
while True:
# 接收数据。
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
# 发送数据。
s.sendto(b'Hello, %s!' % data, addr)

3.2 Client

1
2
3
4
5
6
7
8
9
10
11
12
import socket

# 建立socket。第一个表示使用的是ipv4,第二个参数表示使用的是udp。
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据:
print(s.recv(1024).decode('utf-8'))

s.close()

4 Web开发

在Web应用中,浏览器和服务器之间的传输协议是HTTP。浏览器发送一个HTTP请求。服务器收到请求后,生成一个HTML文档。然后服务器把HTML文档作为HTTP响应的Body发送给浏览器。浏览器收到HTTP响应后,从HTTP响应报文中的Body中将HTML文档取出并显示。让浏览器显示出来。

  • HTML是一种用来定义网页的文本,会HTML,就可以编写网页;
  • HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。

4.1 HTTP

HTTP报文由header和body组成。

4.1.1 HTTP请求报文

header: 第一行由三部分组成。

  • GET(或者POST等其他请求)
  • URL路径(URL总是以/开头,/表示首页)
  • HTTP/1.1(目前HTTP协议的版本就是1.1,但是大部分服务器也支持1.0版本,主要区别在于1.1版本允许多个HTTP请求复用一个TCP连接,以加快传输速度。)

第二行为主机名。比如新浪网: Host: www.sina.com.cn

之后的各行为其他相关的header。

注意:每个Header一行一个,换行符是。

body: 如果是POST, PUT等,则body中会包含相关数据。

注意:当遇到连续两个,Header部分结束,后面的数据全部是Body。

4.1.2 HTTP响应报文

header:

  • 响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误。
  • 响应类型:由Content-Type指定。
  • 其他header。

body:

  • Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
  • 当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。

注意:同请求报文一样,每个header之后的换行符为。报文与最后一个header前由。

4.2 WSGI接口

全称:Web Server Gateway Interface

它实现的是接受HTTP请求、解析HTTP请求、发送HTTP响应这些底层细节。

有了WSGI,web开发人员只需要专注于如何针对请求生成相应的HTML即可。

4.3 Web框架

虽然WSGI实现了底层细节,但是在请求过多且不同的情况下时,调用WSGI会显得非常繁琐。因此需要在WSGI的基础上再进行抽象,这就是Web框架。

有了Web框架,开发人员就可以专注于用一个函数处理一个URL,至于URL到函数的映射,就交给Web框架来做。

4.4 MVC

也即,Model-View-Controller,中文名“模型-视图-控制器”。

  • Controller:接收URL请求,然后处理一些业务逻辑,并比如检查用户名是否存在。然后将相关信息替换到模板相应的变量之中,最后发送响应回复。
  • View:就是用html,css,javascript等语言编写的模板。
  • Model:比如相关信息与模板中相应变量之间的映射等等。

通过MVC,我们在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样,我们就成功地把Python代码和模板代码最大限度地分离了。

使用模板的另一大好处是,模板改起来很方便,而且,改完保存后,刷新浏览器就能看到最新的效果,这对于调试HTML、CSS和JavaScript的前端工程师来说实在是太重要了。

5 异步IO

在现代计算机结构中,CPU高速执行能力和IO设备的龟速严重不匹配是一直以来的一个问题,其中的多线程和多进程是解决这一问题的一种方法。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

5.1 协程

英文名为Coroutine。又称微线程,纤程。

协程不是多进程或多线程,有点像子程序(可以将协程近似地看作子程序)。但是它的执行与子程序不同。

子程序调用总是给定一个入口,然后返回子程序执行的结果,然后调用子程序的程序继续执行。要想再调用此子程序,则必须在给定一个入口后从子程序的起始语句开始执行。

而协程的调用和子程序不同。一个协程A在执行的过程中可以发生中断,然后去执行其他协程B,或者继续执行调用该协程的程序。当其他协程B或程序执行到某一处的时候,又可以返回至协程A中断的地方继续执行。显然,这跟子程序调用不同。

协程的特点在于它是在一个线程里执行。协程最大的优势就是它极高的执行效率。因为协程切换不是线程切换,而是由程序自身控制(就像子程序切换一样),因此,没有线程切换的开销。和多线程相比,线程数量越多,协程的性能优势就越明显。

它还有一个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注意:因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

5.2 Python中协程的实现方式

在Python中,协程是用generator实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def consumer():
r = ""

while True:
n = (yield r)

if not n:
return

print("[CONSUMER] Consuming %s..." % n)
r = "200 ok"

def producer(c):
res = c.send(None)
n = 0

while n < 5:
n = n + 1
print("[PRODUCER] Producing %s..." % n)
r = c.send(n)
print("[PRODUCER] Consumer return: %s" % r)

c.close()



# 建立一个generator。
c = consumer()
# 将这个generator传入到producer函数中。
producer(c)

在这个例子中,整个流程无锁,由一个线程执行。produce和consumer协作完成任务,而producer是调用consumer这个generator的,所以将consumer称为“协程”。

另外,在producer中,通过send方法来恢复协程consumer的执行。当遇见下一个yield语句时,又暂停执行,然后又通过send方法来恢复......假设有多个consumer,那么我们完全可以用send方法来选择性地恢复不同协程的继续运行。

凭借协程的这个特性,可以实现异步io。假设一个有一个消息队列,每个队列元素对应一个协程。当某一个协程由于一个io操作需要等待时,转而去执行其他协程(从它们上次停留出开始执行)。如果之前进行io操作的协程完成了io操作后,则可以暂停当前协程的运行,转而去继续执行io操作的协程。

5.3 asyncio

asyncio是python标准库中的一个异步io模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading
import asyncio

# 将hello这个generator标记为一个协程。
@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
# asyncio.sleep()也是一个协程,并且是一个等待操作,因此转而去执行tasks中的其他协程。
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

当执行到asyncio.sleep()时,由于它需要挂起线程,因此暂停执行当前协程,转而去执行tasks中的其他协程。当asyncio.sleep()返回时,又重新执行之前暂停的协程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import asyncio

@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
# connect是一个协程,由于建立好连接需要一定的时间,因此在建立过程中,转而去执行其他协程。
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
# writer.drain()为一个生成器,由于写完所有的字符串需要一定的时间,因此在此过程中,转而去执行其他协程。
yield from writer.drain()
while True:
# readr.readline()为一个生成器,由于读取所有的字符串需要一定的时间,因此在此过程中,转而去执行其他协程。
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))

writer.close()

loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Reference