网络基础:socket模块

﹥>﹥吖頭↗ 提交于 2020-04-01 05:36:21

socket:

  套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

基于TCP协议的socket:tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

socket参数详解:

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
创建socket对象的参数说明:
family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 
SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。
与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。
这可能有助于使用socket.close()关闭一个独立的插座。

 

server服务端:

#server服务端
import socket
sk = socket.socket() #创建服务套接字
sk.bind(('127.0.0.1',8080))  #把地址绑定到套接字
sk.listen()             #建立监听链接,能否建立需要accept函数去进行检查
conn,addr = sk.accept() #接收客户端链接,接收值分别为客户端套接字对象与地址信息;程序执行到这里会阻塞,等待用户端的连接
ret = conn.recv(1024).decode('utf-8')   #接收客户端消息;recv也会产生阻塞,等待接收数据
print(ret)              #打印客户端消息
conn.send(bytes('你好',encoding='utf-8')) #用接收到的客户端套接字给客户端发送消息
conn.close()            #关闭客户端套接字
sk.close()              #关闭服务器套接字(可选)

client用户端:

import socket
sk = socket.socket()            #创建客户套接字
sk.connect(('127.0.0.1',8080))  #尝试连接服务器
sk.send(bytes('很高兴认识你',encoding='utf-8')) #发送消息
ret = sk.recv(1024).decode('utf-8')     #接收消息
print(ret)                      #打印消息
sk.close()                      #关闭客户套接字

注意:服务端的accept接收客户端的连接请求,返回一个新的用户端套接字对象,和一个地址信息,新的用户端套接字用于接收或者传输消息。服务端用bind绑定地址到套接字,而用户端用connect连接服务器。

 

测试1:

#假装在聊天
#server:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8081))
sk.listen()
conn,addr = sk.accept()
while True:
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    if ret == 'cool:bye':
        conn.send(bytes('柚柚柚切克闹:bye',encoding='utf-8'))
        break
    info = input('柚柚柚切克闹:')
    conn.send(bytes('柚柚柚切克闹:%s'%(info),encoding='utf-8'))
conn.close()
sk.close()


#client:
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8081))
while True:
    chat = input('cool:')
    sk.send(bytes('cool:%s'%(chat),encoding='utf-8'))
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == '柚柚柚切克闹:bye':
        sk.send(bytes('cool:bye',encoding='utf-8'))
        break

sk.close()

测试2:

  每隔十秒用户端向服务端发送一个时间戳,服务端接收后给用户端返回一个格式化的时间

#server:
import socket,time
sk = socket.socket()
sk.bind(('127.0.0.1',8081))
sk.listen()
conn,addr = sk.accept()
while True:
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    info_ret = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(ret))) #需要把字符串强转为整型
    conn.send(bytes(str(info_ret),encoding='utf-8'))
conn.close()
sk.close()

#client
import socket,time
sk = socket.socket()
sk.connect(('127.0.0.1',8081))
while True:
    time.sleep(10)
    timer = int(time.time()) #时间戳返回值为float,需要转换成整型
    sk.send(bytes(str(timer),encoding='utf-8'))#只能传bytes类型,而消息的传输都是以字符串的形式
    ret = sk.recv(1024).decode('utf-8')
    print(ret)

sk.close()

 注意:消息的传输都是字符串形式,需要注意数据类型之间的转换

 

基于UDP协议的socket:udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接

socket参数的详解:

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
创建socket对象的参数说明:
family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 
SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。
与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。
这可能有助于使用socket.close()关闭一个独立的插座。

 

server服务端:

import socket
sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
sk.bind(('127.0.0.1',8080))         #绑定服务器套接字
msg,addr = sk.recvfrom(1024)        #接收消息(元祖类型),一个消息,一个地址(IP地址,端口)
print(msg,addr)
sk.sendto(b'hi',addr)               #发送消息(消息,地址(IP地址,端口))

sk.close()

client服务端:

import socket
sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
ip_port = ('127.0.0.1',8080) #地址和端口
sk.sendto(b'hello',ip_port) #发送消息(消息,元祖(ip地址,端口))
msg,addr = sk.recvfrom(1024)#接收消息(消息,元祖(ip地址,端口))
print(msg,addr)
#server端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8080))
while True:
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    if msg.decode('utf-8').endswith('bye'):
        msg,addr = sk.recvfrom(1024)
        print(msg.decode('utf-8'))
    info = input('>>')
    sk.sendto(info.encode('utf-8'),addr)
sk.close()

#client端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
while True:
    info = input('>>')
    new_info = '来自用户1的消息:'+info
    if info == 'bye':
        sk.sendto(new_info.encode('utf-8'),('127.0.0.1', 8080))
        break
    sk.sendto(new_info.encode('utf-8'), ('127.0.0.1', 8080))
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    if msg.decode('utf-8') == 'bye':
        sk.sendto(b'bye',addr)
        break
sk.close()
聊天

 

黏包:TCP的黏包现象与UDP的丢包现象

什么是黏包:同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种现象就是黏包。

先让我们基于tcp先制作一个远程执行命令的程序:

import subprocess
res = subprocess.Popen('dir',shell=True,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE)#PIPE:管道,是一种数据类型
print('stdout:'+res.stdout.read().decode('gbk'))
print('stderr:'+res.stderr.read().decode('gbk'))

执行结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码且只能从管道里读一次结果

 

测试TCP的黏包现象:

#server端传给client端命令
import socket
import subprocess

sk = socket.socket()
sk.bind(('127.0.0.1',8088))
sk.listen()
conn,addr = sk.accept()
while True:
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    info = input('请输入命令')
    conn.send(info.encode('utf-8'))
conn.close()
sk.close()
#client端接收命令,利用subprocess模块执行,把结果发送给server端
import socket
import subprocess
sk = socket.socket()
sk.connect(('127.0.0.1', 8088))
sk.send(b'1234')
while True:
    key = sk.recv(1024).decode('utf-8')
    res = subprocess.Popen(key,shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    sk.send(('stdout:'+res.stdout.read().decode('gbk')).encode('utf-8'))
    sk.send(('stderr:'+res.stderr.read().decode('gbk')).encode('utf-8'))
    print(key)
sk.close()

结果:执行命令后,接收到的值可能只有一部分,再次执行一条命令后,接收到的值可能是之前执行命令后结果剩余的一部分。

测试UDP的丢包现象:

#server端接收client端的命令,再利用subprocess执行命令,结果发送给client端
import subprocess
import socket
sk = socket.socket(type = socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8080))
while True:
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    res = subprocess.Popen(msg.decode('utf-8'),shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    sk.sendto(('stdout:'+res.stdout.read().decode('gbk')).encode('utf-8'),addr)
    sk.sendto(('stderr:'+res.stderr.read().decode('gbk')).encode('utf-8'),addr)
sk.close()
#client端输入命令发送给server端,接收执行命令的结果
import subprocess
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
while True:
    key = input('请输入命令:')
    sk.sendto(key.encode('utf-8'),('127.0.0.1',8080))
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
sk.close()

结果:执行命令后,接收到的值虽然可能只有一部分,但每一次执行一条命令,接收到的值都不会有黏包现象。

黏包原因

TCP协议中的数据传递:

  tcp协议的拆包机制:

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

  面向流的通信特点和Nagle算法:

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

 基于tcp协议特点的黏包现象成因:

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
socket数据传输过程中的用户态与内核态说明

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

UDP不会发生黏包:

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。 
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),
这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,
而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
 
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

 补充说明:

用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。
用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送) 

用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。
而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

会发生黏包的两种情况:

情况一 发送方的缓存机制:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

#server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
ret = conn.recv(1024)#一次全部接收
print(ret)

conn.close()
sk.close()

#client端
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
sk.send(b'hello')#快速连续发送两条数据
sk.send(b'aike')

sk.close()

情况二 接收方的缓存机制:接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

#server
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
ret = conn.recv(2) #第一次接收只接收到一小部分
ret2 = conn.recv(10)#第二次接收接收上次遗留的数据
print(ret,ret2)
conn.close()
sk.close()



#client端
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
sk.send(b'hello')#快速发送两条数据
sk.send(b'aike')

sk.connect()

总结:

黏包现象只发生在tcp协议中:

1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

 

解决TCP黏包方法:

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

方案:不利用其它模块

#server端
import subprocess
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
while True:
    key = conn.recv(1024).decode('utf-8') #接收命令
    print(key)
    if key == 'q':                      #命令为q退出
        conn.send(b'q')
        break
    res = subprocess.Popen(key,shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE) #利用subprocess模块执行命令,PIPE数据类型与队列类似,只能读取一次
    new_stdout = res.stdout.read()                  #把执行正确命令得到的值赋值,存起来备用
    new_stderr = res.stderr.read()                  #把执行不存在的命令得到的值赋值,存起来备用
    long = len(new_stdout) + len(new_stderr)        #stdout和stderr的值都需要打印,相加得到字节长度
    conn.send(str(long).encode('utf-8'))            #把得到的字节长度发给客户端,以便客户端利用得到准确无误的值
    conn.recv(1024)                                 #接收客户端的反馈,避免服务端的黏包(上下的send发送消息太快)
    conn.send(new_stdout)
    conn.send(new_stderr)                           #执行命令取到的值发送给客户端

conn.close()
sk.close()

 

#client端
import socket
import subprocess
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
    key = input('请输入命令或者q退出:')  #输入命令
    sk.send(key.encode('utf-8'))        #将命令传给服务端
    num = int(sk.recv(1024).decode('utf-8')) #接收执行命令得到的值的字节长度
    if num == 'q':
        break
    sk.send(b'ok') #接收成功反馈给服务端
    ret = sk.recv(num).decode('gbk')  #接收执行命令得到的值,设置为准确的字节长度
    print(ret)
sk.close()
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

 方案进阶:利用struct模块

刚刚的方法,问题在于我们需要先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

我们可以借助struct模块模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据。

  struct模块:该模块可以把一个类型,如数字,转成固定长度的bytes

# 该模块可以把一个类型,如数字,转成固定长度的bytes,每种数据类型的固定长度都不一致
# 转固定长度,pack
# 固定长度转回:unpack
# struct.pack(fmt,values)#转固定长度,fmt值对应为下表参数
# struct.unpack(fmt,string)#转回正常
import struct
num = struct.pack('i',10000000)
print(num)#b'\x80\x96\x98\x00',4个字节的bytes类型

num1 = struct.unpack('i',num)
print(num1)#(10000000,)得到的是一个元祖
print(num1[0])#10000000,取得元素

各数据类型使用方式对照表:

 

使用struct解决黏包 :

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。

发送时 接收时
先发送struct转换好的数据长度4字节 先接受4个字节使用struct转换成数字来获取要接收的数据长度
再发送数据 再按照长度接收数据
import socket,subprocess,struct
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
while True:
    key = conn.recv(1024).decode('utf-8')  #接收命令
    print(key)
    if key == 'q':
        break
    res = subprocess.Popen(key,shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE) #pipe类型只能读一次
    new_stdout = res.stdout.read()#先读出来备用,读出来的默认就是bytes类型
    new_stderr = res.stderr.read()
    res_len = len(new_stdout) + len(new_stderr) #相加stderr和stdout,得到字节长度
    pack_res_len = struct.pack('i',res_len) #将得到的字节长度,固定长度,'i'类型为4字节
    conn.send(pack_res_len)                 #将4个字节固定长度发送给客户端
    conn.send(new_stdout)                   #将stdout内容发送给客户端
    conn.send(new_stderr)

conn.close()
sk.close()
方案进阶,使用struct模块:server端
import socket,subprocess,struct
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
    key = input('请输入命令或者q退出:')      #输入命令
    if key == 'q':
        sk.send(key.encode('utf-8'))
        break
    else:
        sk.send(key.encode('utf-8'))            #发送命令
    res_len = struct.unpack('i',sk.recv(4))[0] #取出stderr和stdout相加得到的字节长度
    ret = sk.recv(res_len).decode('gbk')        #取出stderr和stdout的内容,字节长度设置为得到的字节长度
    print(ret)
sk.close()
方案进阶,使用struct模块:client端

 

利用报头和struct模块发送大文件:

  报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个字节足够用了)

发送时 接收时
先发报头长度 先收报头长度,用struct取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化

最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

例子:

import struct,socket,json
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
read_bytes = 4096
conn,addr = sk.accept()
#接收报头
head_len = struct.unpack('i',conn.recv(4))[0]#接收报头长度
head = json.loads(conn.recv(head_len).decode('utf-8'))#接收报头
#接收报文
filesize = head['filesize']
with open(head['filename'],'wb') as f:
    while filesize: #接收报文,每次接收1024字节
        if filesize >= read_bytes:
            content = conn.recv(read_bytes)
            f.write(content)
            filesize -= read_bytes
        else:
            content = conn.recv(read_bytes)
            f.write(content)
            break
conn.close()
sk.close()
例子:传输大文件,server端
import struct,socket,os,json
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
read_bytes = 4096
#报头
head = {
    'filepath':r'E:\照片',
    'filename':r'2016_01_05_22_21_IMG_1333.JPG',
    'filesize':None
}
file_pash = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_pash) #文件字节大小
head['filesize'] = filesize      #文件大小赋值给字典
bytes_head = json.dumps(head).encode('utf-8') #报头字符串形式的bytes类型
head_len = len(bytes_head)                   #报头的字节长度
pack_head_len = struct.pack('i',head_len) #报头的字节长度长度转换成固定长度,4
sk.send(pack_head_len)                  #发送报文长度
sk.send(bytes_head)                     #发送bytes类型报文
#报文
with open(file_pash,'rb') as f:     #打开需要发送的文件
    while filesize:                 #循环读取文件
        if filesize >= read_bytes:  #每次读1024字节
            content = f.read(read_bytes)
            sk.send(content)
            filesize -= read_bytes
        else:
            content = f.read(read_bytes)
            sk.send(content)
            break
sk.close()
例子:传输大文件,client端

 

验证客户端链接的合法性:

  如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现

hmac模块测试:  

  Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。Python自带的hmac模块实现了标准的Hmac算法。我们来看看如何使用hmac实现带key的哈希。我们首先需要准备待计算的原始消息message,随机key,哈希算法,这里采用MD5和sha1测试,使用hmac的代码如下:

import hmac
secret_key = b'aike' #必须为bytes类型
encryption_obj = hmac.new(secret_key,b'123',digestmod='md5') #加盐加密后的对象,默认算法为md5
encryption_obj1 = hmac.new(secret_key,b'123',digestmod='sha1') #加盐加密后的对象,算法设为sha1
key = encryption_obj.digest() #取得加密后的值
key1 = encryption_obj1.digest()
print(key)#b'\xa6\xfb\x07\xf0\x1f5\x8c\xc3\x0f\xf7G)[1\xbd\xda'
print(key1)#b'\x0b\xf5\xe0\x04(\x80\x01S\x949\x1e\xa2"\x82\xf8\xa0\x81z;\x08'

验证客户端链接合法性:

#server端
import socket
import hmac
import random
import logging
import json

secret_key = 'aike'

def client_logging(addr): #连接日志
    log = logging.getLogger()
    fh = logging.FileHandler(filename='log_in', mode='a', encoding='utf-8')
    formater = logging.Formatter('%(asctime)s - [line: %(lineno)d] - %(levelname)s - %(module)s: %(message)s')
    log.setLevel(logging.INFO)
    fh.setFormatter(formater)
    log.addHandler(fh)
    log.info('客户端请求连接,地址:'+json.dumps(addr))
    return True


def decide_key(conn,bufsize): #判断client秘钥与server秘钥是否一致
    server_key = hmac_key(conn)
    while True:
        client_key = conn.recv(len(server_key))
        if hmac.compare_digest(server_key,client_key):
            conn.send('验证成功'.encode('utf-8'))
            conn.close()
            break
        elif client_key == b'q':
            conn.close()
            break
        else:
            conn.send('验证失败'.encode('utf-8'))

def hmac_key(conn): #服务端秘钥加密
    random_digit = str(random.randint(0,99)).encode('utf-8')
    conn.send(random_digit)
    encryption_obj = hmac.new(secret_key.encode('utf-8'),random_digit)
    server_key = encryption_obj.digest()
    return server_key


def connect(ip_port,bufsize): #基础连接
    sk = socket.socket()
    sk.bind(ip_port)
    sk.listen()
    while True:
        conn,addr = sk.accept()
        print('有新的连接,对象:{},地址:{}'.format(conn,addr))
        client_log(addr)
        decide_key(conn,bufsize)

if __name__ == '__main__':
    ip_port = ('127.0.0.1',8080)
    bufsize = 1024
    connect(ip_port,bufsize)
#client端

import socket
import hmac

def server_key(sk,bufsize): #发送合法秘钥,接收结果
    random_digit = sk.recv(bufsize)
    print(random_digit)
    while True:
        secret_key = input('请输入秘钥或者q退出:')
        if secret_key == 'q':
            sk.send(b'q')
            sk.close()
            break
        encryption_obj = hmac.new(secret_key.encode('utf-8'), random_digit)
        client_key = encryption_obj.digest()
        sk.send(client_key)
        ret = sk.recv(bufsize).decode('utf-8')
        print(ret)
        if ret == '验证成功':
            sk.close()
            break

def connect(ip_port,bufsize):#基础连接
    sk = socket.socket()
    sk.connect(ip_port)
    server_key(sk,bufsize)


if __name__ == '__main__':
    ip_port = ('127.0.0.1',8080)
    bufsize = 1024
    connect(ip_port,bufsize)

socketserver:

socketserver是一个创建服务器的框架,封装了许多功能用来处理来自客户端的请求,简化了自己写服务端代码。比如说对于基本的套接字服务器(socket-based servers),里面就定义了地址族(AF_INET,AF_UNIX等)、套接字类型(SOCK_STREAM,SOCK_DGRAM)等,此外对于基于请求的服务器(request-based servers),里面就详细叙述了如何处理客户端认证,多重请求,以及如何实现多线程多进程等。

class BaseRequestHandler:

    """
请求处理程序类的基类。
该类为要处理的每个请求实例化。
构造函数设置实例变量request, client_address
然后调用handle()方法。实现一个具体的服务,你所需要做的就是派生一个类,定义handle()方法。
handle()方法可以找到作为self的请求。请求,客户端地址为self。client_address和服务器(以防出现这种情况)
需要访问每个服务器的信息)作为self.server。自己为每个请求创建单独的实例handle()方法可以定义其他任意实例变量。
    """

    def __init__(self, request, client_address, server):
        self.request = request
        self.client_address = client_address
        self.server = server
        self.setup()
        try:
            self.handle()
        finally:
            self.finish()

    def setup(self):
        pass

    def handle(self):
        pass

    def finish(self):
        pass
BaseRequestHandler
#固定模式
import socketserver
class Myserver(socketserver.BaseRequestHandler):#必须继承BaseRequestHandler类,父类的方法自己实现
    def handle(self):#建立连接后执行这里
        print(type(self.client_address),self.client_address)
        while True:
            ret = self.request.recv(1024).decode('utf-8')
            print(ret)
            if ret == 'q':
                self.request.close()
                break
            info = input('>>')
            if info == 'q':
                self.request.send(b'q')
                self.request.close()
                break
            self.request.send(info.encode('utf-8'))

    def setup(self):#连接成功执行这里
        print('建立连接:',self.client_address)
    def finish(self): #断开连接执行这里
        print('断开连接:',self.client_address)

if __name__ == '__main__':
    '''
    建立tcp服务
    '''
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),Myserver)
    server.serve_forever()
server端
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
    msg = input('>>')
    if msg == 'q':
        sk.send(b'q')
        break
    sk.send(msg.encode('utf-8'))
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == 'q':
        sk.send(b'q')
        break
sk.close()
client端

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!