1. socketserver
我们之前写的tcp协议的socket是不是一次只能和一个客户端通信,如果用socketserver可以实现和多个客户端通信。它是在socket的基础上进行了一层封装,也就是说底层还是调用的socket,在py2.7里面叫做SocketServer也就是大写了两个S,在py3里面就小写了。后面我们要写的FTP作业,需要用它来实现并发,也就是同时可以和多个客户端进行通信,多个人可以同时进行上传下载等。
我们举一个使用socketserver的例子
import socketserver #1、引入模块
class MyServer(socketserver.BaseRequestHandler): #2、自己写一个类,类名自己随便定义,然后继承socketserver这个模块里面的BaseRequestHandler这个类 def handle(self): #3、写一个handle方法,必须叫这个名字
#self.request #6、self.request 相当于一个conn self.request.recv(1024) #7、收消息
msg = '这是发送的消息'
self.request.send(bytes(msg,encoding='utf-8')) #8、发消息 self.request.close() #9、关闭连接 # 拿到了我们对每个客户端的管道,那么我们自己在这个方法里面的就写我们接收消息发送消息的逻辑就可以了
pass
if __name__ == '__mian__':
#thread 线程,现在只需要简单理解线程,别着急,后面很快就会讲到啦,看下面的图
server = socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)#4、使用socketserver的ThreadingTCPServer这个类,将IP和端口的元祖传进去,还需要将上面咱们自己定义的类传进去,得到一个对象,相当于我们通过它进行了bind、listen
server.serve_forever() #5、使用我们上面这个类的对象来执行serve_forever()方法,他的作用就是说,我的服务一直开启着,就像京东一样,不能关闭网站,对吧,并且serve_forever()帮我们进行了accept #注意:
#有socketserver 那么有socketclient的吗?
#当然不会有,我要作为客户去访问京东的时候,京东帮我也客户端了吗,客户端是不是在我们自己的电脑啊,并且socketserver对客户端没有太高的要求,只需要自己写一些socket就行了。
来看下完整的客户端与服务端代码
服务端代码:
import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self):
while 1:
from_client_msg = self.request.recv(1024) # self.request = conn
print(from_client_msg.decode('utf-8'))
msg = input('服务端说:')
self.request.send(msg.encode('utf-8')) if __name__ == '__main__': ip_port = ('127.0.0.1',8001) # 设置allow_reuse_address允许服务器重用地址
socketserver.TCPServer.allow_reuse_address = True #server = socketserver.TCPServer((HOST, PORT),Myserver)
server = socketserver.ThreadingTCPServer(ip_port,Myserver) # 让server永远运行下去,除非强制停止程序
server.serve_forever()
客户端:
import socket
client = socket.socket()
client.connect(('127.0.0.1',8001)) while 1:
msg = input('客户端说>>>')
client.send(msg.encode('utf-8'))
from_server_msg = client.recv(1024)
print(from_server_msg.decode('utf-8'))
2.验证客户端的链接合法性(加密)
首先,我们来探讨一下,什么叫验证合法性, 举个例子:有一天,我开了一个socket服务端,只想让咱们这个班的同学使用,但是有一天,隔壁班的同学过来问了一下我开的这个服务端的ip和端口,然后他是不是就可以去连接我了啊,那怎么办,我是不是不想让他连接我啊,我需要验证一下你的身份,这就是验证连接的合法性,再举个例子,就像我们上面说的你的windows系统是不是连接微软的时间服务器来获取时间的啊,你的mac能到人家微软去获取时间吗,你愿意,人家微软还不愿意呢,对吧,那这时候,你每次连接我来获取时间的时候,我是不是就要验证你的身份啊,也就是你要带着你的系统信息,我要判断你是不是我微软的windows,对吧,如果是mac,我是不是不让你连啊,这就是连接合法性。如果验证你的连接是合法的,那么如果我还要对你的身份进行验证的需求,也就是要验证用户名和密码,那么我们还需要进行身份认证。连接认证>>身份认证>>ok你可以玩了。
就用到两个方法
1. os.urandom(n)
其中os.urandom(n) 是一种bytes类型的随机生成n个字节字符串的方法,而且每次生成的值都不相同。再加上md5等加密的处理,就能够成内容不同长度相同的字符串了。
官方解释为:
os.urandom(n)函数在python官方文档中做出了这样的解释函数定位:
Return a string of n random bytes suitable for cryptographic use. 意思就是,返回一个有n个byte那么长的一个string,然后很适合用于加密。然后这个函数,在文档中,被归结于os这个库的Miscellaneous Functions,意思是不同种类的函数(也可以说是混种函数)
原因是: This function returns random bytes from an OS-specific randomness source. (函数返回的随机字节是根据不同的操作系统特定的随机函数资源。即,这个函数是调用OS内部自带的随机函数的。有特异性)
2. hamc
Python自带的hmac模块实现了标准的Hmac算法,我们首先需要准备待计算的原始消息message,随机key,哈希算法,这里采用MD5,使用hmac的代码如下:
import hmac
message = b'Hello world'
key = b'secret'
h = hmac.new(key,message,digestmod='MD5')
print(h.hexdigest())
比较两个密文是否相同,可以用hmac.compare_digest(密文、密文),然会True或者False。
可见使用hmac和普通hash算法非常类似。hmac输出的长度和原始哈希算法的长度一致。需要注意传入的key和message都是bytes
类型,str
类型需要首先编码为bytes
。
def hmac_md5(key, s):
return hmac.new(key.encode('utf-8'), s.encode('utf-8'), 'MD5').hexdigest() class User(object):
def __init__(self, username, password):
self.username = username
self.key = ''.join([chr(random.randint(48, 122)) for i in range(20)])
self.password = hmac_md5(self.key, password)
如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现,看代码
sendall()与send()没有什么区别 ,可视为send()
server端
from socket import *
import hmac,os secret_key=b'Jedan has a big key!'
def conn_auth(conn):
'''
认证客户端链接
:param conn:
:return:
'''
print('开始验证新链接的合法性')
msg=os.urandom(32)#生成一个32字节的随机字符串
conn.sendall(msg)
h=hmac.new(secret_key,msg)
digest=h.digest()
respone=conn.recv(len(digest))
return hmac.compare_digest(respone,digest) def data_handler(conn,bufsize=1024):
if not conn_auth(conn):
print('该链接不合法,关闭')
conn.close()
return
print('链接合法,开始通信')
while True:
data=conn.recv(bufsize)
if not data:break
conn.sendall(data.upper()) def server_handler(ip_port,bufsize,backlog=5):
'''
只处理链接
:param ip_port:
:return:
'''
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(backlog)
while True:
conn,addr=tcp_socket_server.accept()
print('新连接[%s:%s]' %(addr[0],addr[1]))
data_handler(conn,bufsize) if __name__ == '__main__':
ip_port=('127.0.0.1',9999)
bufsize=1024
server_handler(ip_port,bufsize)
client端
from socket import *
import hmac,os secret_key=b'Jedan has a big key!'
def conn_auth(conn):
'''
验证客户端到服务器的链接
:param conn:
:return:
'''
msg=conn.recv(32)
h=hmac.new(secret_key,msg)
digest=h.digest()
conn.sendall(digest) def client_handler(ip_port,bufsize=1024):
tcp_socket_client=socket(AF_INET,SOCK_STREAM)
tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True:
data=input('>>: ').strip()
if not data:continue
if data == 'quit':break tcp_socket_client.sendall(data.encode('utf-8'))
respone=tcp_socket_client.recv(bufsize)
print(respone.decode('utf-8'))
tcp_socket_client.close() if __name__ == '__main__':
ip_port=('127.0.0.1',9999)
bufsize=1024
client_handler(ip_port,bufsize)
3.实现ftp简单上传的功能
流程和思路:
我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
发送时:
1.先发报头长度
2.再编码报头内容然后发送,使用json,不要用eval,因为eval容易造成内存溢出
3.最后发真实内容
接收时:
1.先手报头长度,用struct取出来
2.根据取出的长度收取报头内容,然后解码,反序列化
3.从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
服务端server
import json
import socket
import struct server = socket.socket()
server.bind(('127.0.0.1',8001))
server.listen()
conn,addr = server.accept() #首先接收文件的描述信息的长度
struct_data_len = conn.recv(4)
data_len = struct.unpack('i',struct_data_len)[0] # 通过文件信息的长度将文件的描述信息全部接收
print('data_len>>>',data_len)
file_info_bytes = conn.recv(data_len)
#将文件描述信息转换为字典类型,以便操作
file_info_json = file_info_bytes.decode('utf-8')
file_info_dict = json.loads(file_info_json) #{'file_name': 'aaa.mp4', 'file_size': 24409470} print(file_info_dict) #统计每次接收的累计长度
recv_sum = 0 #根据文件描述信息,指定文件路径和文件名称
file_path = 'D:\s18\jj' + '\\' + file_info_dict['file_name'] #接收文件的真实数据
with open(file_path,'wb') as f:
#循环接收,循环结束的依据是文件描述信息中文件的大小,也是通过一个初始值为0的变量来统计
while recv_sum < file_info_dict['file_size']:
every_recv_data = conn.recv(1024)
recv_sum += len(every_recv_data)
f.write(every_recv_data)
客户端client:
import os
import socket
import json
import struct
client = socket.socket()
client.connect(('127.0.0.1',8001)) #统计文件大小
file_size = os.path.getsize(r'D:\python_workspace_s18\day029\aaa.mp4') #统计文件描述信息,给服务端,服务端按照我的文件描述信息来保存文件,命名文件等等,现在放到一个字典里面了
file_info = {
'file_name':'aaa.mp4',
'file_size':file_size,
} #由于字典无法直接转换成bytes类型的数据,所以需要json来将字典转换为json字符串.在把字符串转换为字节类型的数据进行发送
#json.dumps是将字典转换为json字符串的方法
file_info_json = json.dumps(file_info) #将字符串转换成bytes类型的数据
file_info_byte = file_info_json.encode('utf-8') #为了防止黏包现象,将文件描述信息的长度打包后和文件的描述信息的数据一起发送过去
data_len = len(file_info_byte)
data_len_struct = struct.pack('i',data_len) #发送文件描述信息
client.send(data_len_struct + file_info_byte) #定义一个变量,=0,作为每次读取文件的长度的累计值
sum = 0
#打开的aaa.mp4文件,rb的形式,
with open('aaa.mp4','rb') as f:
#循环读取文件内容
while sum < file_size:
#每次读取的文件内容,每次读取1024B大小的数据
every_read_data = f.read(1024)
#将sum累加,统计长度
sum += len(every_read_data)
#将每次读取的文件的真实数据返送给服务端
client.send(every_read_data)