Python06网络

Python网络

原始Python服务器

不使用框架,甚至不使用Python标准库中的高级包,只使用标准库中的socket接口,写一个Python服务器。

socket接口是实际上是操作系统提供的系统调用。socket的使用并不局限于Python语言,你可以用C或者JAVA来写出同样的socket服务器,而所有语言使用socket的方式都类似(Apache就是使用C实现的服务器)。

TCP/IP和socket

socket是进程间通信的一种方法,它是基于网络传输协议的上层接口。socket有许多种类型,比如基于TCP协议或者UDP协议(两种网络传输协议)。其中又以TCP socket最为常用。

TCP socket与双向管道(duplex PIPE)有些类似,一个进程向socket的一端写入或读取文本流,而另一个进程可以从socket的另一端读取或写入,比较特别是,这两个建立socket通信的进程可以分别属于两台不同的计算机

所谓的TCP协议,就是规定了一些通信的守则,以便在网络环境下能够有效实现上述进程间通信过程。双向管道(duplex PIPE)存活于同一台电脑中,所以不必区分两个进程的所在计算机的地址,而socket必须包含有地址信息,以便实现网络通信。

一个socket包含四个地址信息: 两台计算机的IP地址和两个进程所使用的端口(port)。IP地址用于定位计算机,而port用于定位进程 (一台计算机上可以有多个进程分别使用不同的端口)。

TCP socket

在互联网上,我们可以让某台计算机作为服务器。服务器开放自己的端口,被动等待其他计算机连接。当其他计算机作为客户主动使用socket连接到服务器的时候,服务器就开始为客户提供服务。

在Python中,我们使用标准库中的socket包来进行底层的socket编程。

首先是服务器端,我们使用**bind()方法来赋予socket以固定的地址和端口,并使用listen()方法来被动的监听该端口。当有客户尝试用connect()方法连接的时候,服务器使用accept()**接受连接,从而建立一个连接的socket

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#! /usr/bin/python
# coding=utf8
# 服务端
import socket
# 地址信息
HOST = ''
PORT = 8085
reply = 'Yes'
# socket使用的是IPv4(AF_INET,IP version 4)和TCP协议(SOCK_STREAM)。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST,PORT)) # 依据传入tuple绑定本机IP/端口
s.listen(3) # 最大链接数为3
while True: # 保持监听, 持续响应
conn,addr = s.accept() # 获取接收数据及来源IP
request = conn.recv(1024) # 接收数据
print('connected by: ', addr)
print('request is: ', request)
conn.sendall(reply) # 发送数据给请求方
conn.close()

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#! /usr/bin/python
# coding=utf8
# 客户端
import socket
HOST = '192.168.253.134'
PORT = 8085
request = 'Can you hear me?'
# socket使用的是IPv4(AF_INET,IP version 4)和TCP协议(SOCK_STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT)) # 搜索链接服务器端的IP地址和端口
s.sendall(request)
reply = s.recv(1024)
print('reply is: ',reply)
s.close()

我们对socket的两端都可以调用**recv()方法来接收信息,调用sendall()方法来发送信息。这样,我们就可以在分处于两台计算机的两个进程间进行通信了。当通信结束的时候,我们使用close()**方法来关闭socket连接。

image-20220611142632171

基于TCP socket的HTTP服务器

socket传输自由度太高,从而带来很多安全和兼容的问题。我们往往利用一些应用层的协议(比如HTTP协议)来规定socket 使用规则,以及所传输信息的格式

HTTP协议利用**请求-回应(request-response)**的方式来使用TCP socket。客户端向服务器发一段文本作为request,服务器端在接收到request之后,向客户端发送一段文本作为response。在完成了这样一次request-response交易之后,TCP socket被废弃。下次的request将建立新的socket。request和response本质上说是两个文本,只是HTTP协议对这两个文本都有一定的格式要求。

如下为一个HTTP服务器案例:

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
43
44
45
46
47
48
49
50
51
52
53
#! /usr/bin/python
# coding=utf8
import socket

HOST = ''
PORT = 8086

text_content = '''
HTTP/1.x 200 OK
Content-Type: text/html

<head>
<title>NI9NE</title>
</head>
<html>
<p> This is NI9NE's Python Http Server</p>
<img src='ni9ne.png'/>
</html>
'''

f = open('ni9ne.png','rb')
# 构建响应头信息
pic_content = '''
HTTP/1.x 200 OK
Content-Type: image/png

'''
pic_content = pic_content + f.read()
f.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST,PORT))

while True:
s.listen(3)
conn,addr = s.accept()
request = conn.recv(1024)
request_info = request.split(' ')
method = request_info[0]
src = request_info[1]

if method == 'GET':
if src == '/ni9ne.png':
content = pic_content
else:
content = text_content

print('Connected by: ', addr)
print('Request is: ', request)

conn.sendall(content)

conn.close()

image-20220612165645276

socket包是比较底层的包。Python标准库中还有高层的包,比如SocketServer,SimpleHTTPServer,CGIHTTPServer,cgi。

Python服务器进化

支持POST

即从POST请求中提取数据,再显示在屏幕上。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#! /usr/bin/python
# coding=utf8
import socket

HOST = ''
PORT = 8087

text_content = '''
HTTP/1.x 200 OK
Content-Type: text/html

<head>
<title>NI9NE</title>
</head>
<html>
<p> This is NI9NE's Python Http Server</p>
<img src='ni9ne.png'/>
<form name="input" action="/" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>
'''

f = open('ni9ne.png','rb')
pic_content = '''
HTTP/1.x 200 OK
Content-Type: image/jpg

'''
pic_content = pic_content + f.read()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))

while True:
s.listen(3)
conn, addr = s.accept()
request = conn.recv(1024) # 接收缓冲区大小

method = request.split(' ')[0]
src = request.split(' ')[1]

print 'Connected by', addr
print 'Request is:', request

if method == 'GET':

if src == '/ni9ne.png':
content = pic_content
else: content = text_content

conn.sendall(content)

if method == 'POST':
form = request.split('\r\n')
print form
idx = form.index('') # 空行索引
print idx
entry = form[idx:] # post包所有数据

value = entry[-1].split('=')[-1]
conn.sendall(text_content + '\n <p>post value is: ' + value + '</p>')

conn.close()

image-20220613231535023

post包 request.split(' ') 结果

1
2
['POST', '/', 'HTTP/1.1\r\nHost:', '192.168.253.134:8087\r\nConnection:', 'keep-alive\r\nContent-Length:', '14\r\nPragma:', 'no-cache\r\nCache-Control:', 'no-cache\r\nUpgrade-Insecure-Requests:', '1\r\nOrigin:', 'http://192.168.253.134:8087\r\nContent-Type:', 'application/x-www-form-urlencoded\r\nUser-Agent:', 'Mozilla/5.0', '(Windows', 'NT', '10.0;', 'Win64;', 'x64)', 'AppleWebKit/537.36', '(KHTML,', 'like', 'Gecko)', 'Chrome/98.0.4758.82', 'Safari/537.36\r\nAccept:', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nReferer:', 'http://192.168.253.134:8087/\r\nAccept-Encoding:', 'gzip,', 'deflate\r\nAccept-Language:', 'zh-CN,zh;q=0.9\r\n\r\nfirstname=1232']

使用SocketServer

基于TCP协议

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#! /usr/bin/python
# coding=utf8
import SocketServer as socketserver

HOST = ''
PORT = 8086

text_content = '''
HTTP/1.x 200 OK
Content-Type: text/html

<head>
<title>NI9NE</title>
</head>
<html>
<p> This is NI9NE's Python Http Server</p>
<img src='ni9ne.png'/>
<form name="input" action="/" method="post">
first name: <input type="text" name="firstname"><br/>
<input type="submit" value="Submit">
</form>
</html>
'''

f = open('ni9ne.png','rb')
pic_content = '''
HTTP/1.x 200 OK
Content-Type: image/png

'''
pic_content = pic_content + f.read()
f.close()

# 传递给TCPServer一个MyTCPHandler类。这个类定义了如何操作socket。MyTCPHandler继承自BaseRequestHandler。改写handler()方法,来具体规定不同情况下服务器的操作。
class MyTCPHandle(socketserver.BaseRequestHandler):
def handle(self):
# 在handler()中,通过self.request来查询通过socket进入服务器的请求 (正如在handler()中对socket进行recv()和sendall()操作),还使用self.address来引用socket的客户端地址。
request = self.request.recv(1024)

print('Connected by', self.client_address[0])
print('Request is:', request)

method = request.split(' ')[0]
src = request.split(' ')[1]

if method == 'GET':
if src == '/ni9ne.png':
content = pic_content
else:
content = text_content
self.request.sendall(content) # 使用self.request代替原有的conn

if method == 'POST':
form = request.split('\r\n')
idx = form.index('')
entry = form[idx:]

value = entry[-1].split('=')[-1]
self.request.sendall(text_content + '\n <p>post value is: ' + value + '</p>')

# 建立了一个TCPServer对象,即一个使用TCP socket的服务器。在建立TCPServe的同时,设置该服务器的IP地址和端口。
server = socketserver.TCPServer((HOST,PORT),MyTCPHandle)

# 使用server_forever()方法来让服务器不断工作(就像原始程序中的while循环一样)。
server.serve_forever()

image-20220613233010856

SimpleHTTPServer: 使用静态文件来回应请求

HTTP协议基于TCP协议,但增加了更多的规范。这些规范,虽然限制了TCP协议的功能,但大大提高了信息封装和提取的方便程度。

对于一个HTTP请求(request)来说,它包含有两个重要信息:请求方法和URL。

请求方法(request method) URL 操作
GET / 发送text_content
GET /ni9ne.png 发送pic_content
POST / 分析request主体中包含的value(实际上是我们填入表格的内容); 发送text_content和value

可以使用SimpleHTTPServer包和CGIHTTPServer包来规定针对不同请求的操作。

其中,SimpleHTTPServer可以用于处理GET方法和HEAD方法的请求。它读取request中的URL地址,找到对应的静态文件,分析文件类型,用HTTP协议将文件发送给客户。

image-20220614002623225

index.html

1
2
3
4
5
6
7
8
9
10
11
<head>
<title>NI9NE</title>
</head>
<html>
<p> This is NI9NE's Python Http Server</p>
<img src='ni9ne.png'/>
<form name="input" action="/" method="post">
first name: <input type="text" name="firstname"><br/>
<input type="submit" value="Submit">
</form>
</html>

socket_simplehttp.py

1
2
3
4
5
6
7
8
9
10
11
12
# coding=utf8

import SocketServer as socketserver
import SimpleHTTPServer

HOST = ''
PORT = 8086

# 创建服务, SimpleHTTPRequestHander是SimpleHTTPServer包中预设
server = socketserver.TCPServer((HOST,PORT), SimpleHTTPServer.SimpleHTTPRequestHandler)
# 开始服务
server.serve_forever()

image-20220614003048310

这里的程序不能处理POST请求。可以使用使用CGI来弥补这个缺陷。

image-20220614003031673

CGIHTTPServer:使用静态文件或者CGI来回应请求

1
2
3
文档: 
https://docs.python.org/zh-cn/3/library/http.server.html#http.server.CGIHTTPRequestHandler
https://docs.python.org/zh-cn/3/library/http.server.html#http.server.SimpleHTTPRequestHandler

CGIHTTPServer包中的CGIHTTPRequestHandler类继承自SimpleHTTPRequestHandler类,所以可以用来代替上面的例子,来提供静态文件的服务。此外,CGIHTTPRequestHandler类还可以用来运行CGI脚本

CGI (Common Gateway Interface)。CGI是服务器和应用脚本之间的一套接口标准。它的功能是让服务器程序运行脚本程序,将程序的输出作为response发送给客户。总体的效果,是允许服务器动态的生成回复内容,而不必局限于静态文件。

支持CGI的服务器程接收到客户的请求,根据请求中的URL,运行对应的脚本文件。服务器会将HTTP请求的信息和socket信息传递给脚本文件,并等待脚本的输出。脚本的输出封装成合法的HTTP回复,发送给客户。

服务器和CGI脚本之间的通信要符合CGI标准。CGI的实现方式有很多,比如说使用Apache服务器与Perl写的CGI脚本,或者Python服务器与shell写的CGI脚本。

为了使用CGI,需要使用BaseHTTPServer包中的HTTPServer类来构建服务器。

image-20220614010142853

index.html

CGIHTTPRequestHandler默认当前目录下的cgi-bin和ht-bin文件夹中的文件为CGI脚本,而存放于其他地方的文件被认为是静态文件。因此,我们需要修改一下index.html,将其中form元素指向的action改为cgi-bin/post.py。

1
2
3
4
5
6
7
8
9
10
11
<head>
<title>NI9NE</title>
</head>
<html>
<p> This is NI9NE's Python Http Server</p>
<img src='ni9ne.png'/>
<form name="input" action="cgi-bin/post.py" method="post">
first name: <input type="text" name="firstname"><br/>
<input type="submit" value="Submit">
</form>
</html>

CGIHTTPServer.py

1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/python
# coding=utf8

import BaseHTTPServer
import CGIHTTPServer

HOST = ''
PORT = 8086

server = BaseHTTPServer.HTTPServer((HOST,PORT), CGIHTTPServer.CGIHTTPRequestHandler)
server.serve_forever()

post.py

1
2
3
4
5
6
7
8
9
10
#! /usr/bin/python
# coding=utf8

import cgi
form = cgi.FieldStorage()

print("Content-Type: text/html")
print('')
print("<p>Hello world!</p>")
print("<p>" + repr(form['firstname']) + "</p>")