网络编程
前期的Python只是一个铺垫,网络安全中Python应用最广泛的就是网络编程。
- TCP/IP协议:
通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123
。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,途径多个路由,但不保证能到达,也不保证顺序到达。
TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
- TCP编程
Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。
所以,我们要创建一个基于TCP连接的Socket,可以这样做:
# 导入socket库: import socket # 创建一个socket: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 建立连接: s.connect(('www.sina.com.cn', 80))
创建Socket
时,AF_INET
指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6
。SOCK_STREAM
指定使用面向流的TCP协议,这样,一个Socket
对象就创建成功,但是还没有建立连接,客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。新浪网站的IP地址可以用域名www.sina.com.cn
自动转换到IP地址。
建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容
# 发送数据: s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
服务器
和客户端编程相比,服务器编程就要复杂一些。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
用两个程序模拟一下这个过程
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',9999)) print(s.recv(1024).decode('utf-8')) for data in [b'Michael',b'Tracy',b'Sarah']: s.send(data) print (s.recy(1024).decode('utf-8')) s.send(b'exit') s.close()
# -*- coding: UTF-8 -*- import socket s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('127.0.0.1', 9999)) s.listen(5) #等待连接的最大数量 print ('Waiting for connection...') while True: #服务器程序通过一个永久循环来接受来自客户端的连接 sock, addr = s.accept() #,accept()
会等待并返回一个客户端的连接: t = threading.Thread(target=tcplink, args=(sock, addr)) #连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上Hello
再发送给客户端。
如果客户端发送了exit
字符串,就直接关闭连接。 t.start() def tcplinck(sock, addr): print ('Accept new connection from %s:%s...' %addr) sock.send(b'Welcome!') while True: data = sock.recv(1024) time.sleep(1) if not data or data.decode('utf-8')=='exit': break sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8')) sock.close() print ('Connection from %s:%s closed.' %addr)
这两段代码还是极其的重要,花了好长好长时间才弄懂。
在运行时我们需要把这两段代码同时开启,
┌────────────────────────────────────────────────────────┐ │Command Prompt - □ x │ ├────────────────────────────────────────────────────────┤ │$ python echo_server.py │ │Waiting for connection... │ │Accept new connection from 127.0.0.1:64398... │ │Connection from 127.0.0.1:64398 closed. │ │ │ │ ┌────────────────────────────────────────────────┴───────┐ │ │Command Prompt - □ x │ │ ├────────────────────────────────────────────────────────┤ │ │$ python echo_client.py │ │ │Welcome! │ │ │Hello, Michael! │ └───────┤Hello, Tracy! │ │Hello, Sarah! │ │$ │ │ │ │ │ └────────────────────────────────────────────────────────┘
这两个程序就简单模拟了服务器与客户端之间的关系。
- UDP编程
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
我们来看看如何通过UDP协议传输数据。和TCP类似,使用UDP的通信双方也分为客户端和服务器。服务器首先需要绑定端口:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定端口: s.bind(('127.0.0.1', 9999))
创建Socket时,SOCK_DGRAM
指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用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)
recvfrom()
方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()
就可以把数据用UDP发给客户端。
客户端使用UDP时,首先仍然创建基于UDP的Socket,然后,不需要调用connect()
,直接通过sendto()
给服务器发数据:
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()
┌────────────────────────────────────────────────────────┐ │Command Prompt - □ x │ ├────────────────────────────────────────────────────────┤ │$ python udp_server.py │ │Bind UDP on 9999... │ │Received from 127.0.0.1:63823... │ │Received from 127.0.0.1:63823... │ │Received from 127.0.0.1:63823... │ │ ┌────────────────────────────────────────────────┴───────┐ │ │Command Prompt - □ x │ │ ├────────────────────────────────────────────────────────┤ │ │$ python udp_client.py │ │ │Welcome! │ │ │Hello, Michael! │ └───────┤Hello, Tracy! │ │Hello, Sarah! │ │$ │ │ │ │ │ └────────────────────────────────────────────────────────┘
可以注意到这个程序和上一个不同的地方在于UDP不需要和服务器连接,也就是相当于验证的这一个过程。但是这个过程也是不安全的,因为设想一下,现在有100万个
未知来源的ip同时访问这个服务器,而这个服务器可能最大承载的访问量是10万 远远超过了服务器可以承载的限度。服务器过载最直接的隐患就是服务器停止工作了,
这是非常危险的情况,因为服务器不工作了,黑客就可以轻而易举的拿到最高权限。因此例如双十一,阿里的服务器在一瞬间需要承载超大规模的访问量,他们在双十一
之前都需要做一个很重要的工作,链路压测,来测试服务器可以承载的最大限度。