验证客户端的链接合法性和socketserver模块实现并发

时间:2021-06-12 23:58:01

本节内容:

1.验证客户端的链接合法性

2.socketserver模块实现并发

 

一、验证客户端的链接合法性

  首先,我们来探讨一下,什么叫验证合法性, 举个例子:有一天,我开了一个socket服务端,只想让咱们这个班的同学使用,但是有一天,隔壁班的同学过来问了一下我开的这个服务端的ip和端口,然后他是不是就可以去连接我了啊,那怎么办,我是不是不想让他连接我啊,我需要验证一下你的身份,这就是验证连接的合法性,再举个例子,就像我们上面说的你的windows系统是不是连接微软的时间服务器来获取时间的啊,你的mac能到人家微软去获取时间吗,你愿意,人家微软还不愿意呢,对吧,那这时候,你每次连接我来获取时间的时候,我是不是就要验证你的身份啊,也就是你要带着你的系统信息,我要判断你是不是我微软的windows,对吧,如果是mac,我是不是不让你连啊,这就是连接合法性。如果验证你的连接是合法的,那么如果我还要对你的身份进行验证的需求,也就是要验证用户名和密码,那么我们还需要进行身份认证。连接认证>>身份认证>>ok你可以玩了。

  
  好大致描述相信大家基本理解了,如果这还没有理解,那么同学,我要哭晕在厕所了。
    
 
  如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现,直接看代码!
验证客户端的链接合法性和socketserver模块实现并发验证客户端的链接合法性和socketserver模块实现并发
 1 from socket import *
 2 import hmac,os
 3 
 4 secret_key=b'Jedan has a big key!'
 5 def conn_auth(conn):
 6     '''
 7     认证客户端链接
 8     :param conn:
 9     :return:
10     '''
11     print('开始验证新链接的合法性')
12     msg=os.urandom(32)#生成一个32字节的随机字符串
13     conn.sendall(msg)
14     h=hmac.new(secret_key,msg) 
15     digest=h.digest()
16     respone=conn.recv(len(digest))
17     return hmac.compare_digest(respone,digest)
18 
19 def data_handler(conn,bufsize=1024):
20     if not conn_auth(conn):
21         print('该链接不合法,关闭')
22         conn.close()
23         return
24     print('链接合法,开始通信')
25     while True:
26         data=conn.recv(bufsize)
27         if not data:break
28         conn.sendall(data.upper())
29 
30 def server_handler(ip_port,bufsize,backlog=5):
31     '''
32     只处理链接
33     :param ip_port:
34     :return:
35     '''
36     tcp_socket_server=socket(AF_INET,SOCK_STREAM)
37     tcp_socket_server.bind(ip_port)
38     tcp_socket_server.listen(backlog)
39     while True:
40         conn,addr=tcp_socket_server.accept()
41         print('新连接[%s:%s]' %(addr[0],addr[1]))
42         data_handler(conn,bufsize)
43 
44 if __name__ == '__main__':
45     ip_port=('127.0.0.1',9999)
46     bufsize=1024
47     server_handler(ip_port,bufsize)
server
验证客户端的链接合法性和socketserver模块实现并发验证客户端的链接合法性和socketserver模块实现并发
 1 from socket import *
 2 import hmac,os
 3 
 4 secret_key=b'Jedan has a big key!'
 5 def conn_auth(conn):
 6     '''
 7     验证客户端到服务器的链接
 8     :param conn:
 9     :return:
10     '''
11     msg=conn.recv(32)
12     h=hmac.new(secret_key,msg)
13     digest=h.digest()
14     conn.sendall(digest)
15 
16 def client_handler(ip_port,bufsize=1024):
17     tcp_socket_client=socket(AF_INET,SOCK_STREAM)
18     tcp_socket_client.connect(ip_port)
19 
20     conn_auth(tcp_socket_client)
21 
22     while True:
23         data=input('>>: ').strip()
24         if not data:continue
25         if data == 'quit':break
26 
27         tcp_socket_client.sendall(data.encode('utf-8'))
28         respone=tcp_socket_client.recv(bufsize)
29         print(respone.decode('utf-8'))
30     tcp_socket_client.close()
31 
32 if __name__ == '__main__':
33     ip_port=('127.0.0.1',9999)
34     bufsize=1024
35     client_handler(ip_port,bufsize)
client

  

  介绍下代码中使用的两个方法:

  1.os.urandom(n)

  其中os.urandom(n) 是一种bytes类型的随机生成n个字节字符串的方法,而且每次生成的值都不相同。再加上md5等加密的处理,就能够成内容不同长度相同的字符串了。

验证客户端的链接合法性和socketserver模块实现并发验证客户端的链接合法性和socketserver模块实现并发
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内部自带的随机函数的。有特异性)
os.urandom(n)函数官方解释

   使用方法:

验证客户端的链接合法性和socketserver模块实现并发验证客户端的链接合法性和socketserver模块实现并发
import os
from hashlib import md5

for i in range(10):
    print md5(os.urandom(24)).hexdigest()
os.urandom

  

  2.hmac:

    python自带的hmac模块实现了标准的Hmac算法,我们首先需要准备待计算的原始消息,随机key,哈希算法,这里采用MD5,使用hmac的代码如下:(我们完全可以用hashlib来实现,但这个没什么不好的,而且这个操作更方便一些)

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)

 

二、socketserver模块实现并发

  为什么要讲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就行了。

  

  ThreadingTCPServer,多线程,看图

验证客户端的链接合法性和socketserver模块实现并发

  那么我们的一个服务端程序里就有多个线程,每个线程去帮你处理一个socket连接,也就是处理一个对应的客户端,那么我们的程序就可以同时和多个客户端进行沟通了。(简单理解)

 

  我们来分析下socket的源码:

在整个socketserver这个模块中,其实就干了两件事情:
1、一个是循环建立链接的部分,每个客户链接都可以连接成功 
 2、一个通讯循环的部分,就是每个客户端链接成功之后,要循环的和客户端进行通信。

看代码中的:server=socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)

还记得面向对象的继承吗?来,大家自己尝试着看看源码:

查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer

实例化得到server,先找ThreadMinxIn中的__init__方法,发现没有init方法,然后找类ThreadingTCPServer的__init__,在TCPServer中找到,在里面创建了socket对象,进而执行server_bind(相当于bind),server_active(点进去看执行了listen)
找server下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....
源码分析总结:

基于tcp的socketserver我们自己定义的类中的

  self.server即套接字对象
  self.request即一个链接
  self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的

  self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  self.client_address即客户端地址

 

  一个完整的sockeserver代码实例:

#客户端.py

import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): self.data = self.request.recv(1024).strip() print("{} wrote:".format(self.client_address[0])) print(self.data) self.request.sendall(self.data.upper()) if __name__ == "__main__": HOST, PORT = "127.0.0.1", 9999 # 设置allow_reuse_address允许服务器重用地址 socketserver.TCPServer.allow_reuse_address = True # 创建一个server, 将服务地址绑定到127.0.0.1:9999 #server = socketserver.TCPServer((HOST, PORT),Myserver) server = socketserver.ThreadingTCPServer((HOST, PORT),Myserver) # 让server永远运行下去,除非强制停止程序 server.serve_forever()

  

#服务端.py

import socket HOST, PORT = "127.0.0.1", 9999 data = "hello" # 创建一个socket链接,SOCK_STREAM代表使用TCP协议 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((HOST, PORT)) # 链接到客户端 sock.sendall(bytes(data + "\n", "utf-8")) # 向服务端发送数据 received = str(sock.recv(1024), "utf-8")# 从服务端接收数据 print("Sent: {}".format(data)) print("Received: {}".format(received))