python基础(socket)
一、软件开发的架构:
1.1 C/S架构:
C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。
1.2 B/S架构:
B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。
Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查
二、 socket概念:
2.1 定义:
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
2.2 套接字:
2.2.1 基于文件类型的套接字家族:
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
2.2.2 基于网络类型的套接字家族:
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
2.3 tcp与udp的应用:
2.3.1 基于TCP协议的socket:
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
示例:
# 通用测试版(服务端): import socket sk = socket.socket() # socket对象,实际上存储了存储了所有的操作系统提供的网络资源 sk.bind(('127.0.0.1', 9006)) # 只有我自己能访问我自己 # sk.bind(('192.168.16.179', 9006)) # 局域网类的所有的机器都可以访问我,但是如果这个代码移交给别的机器必须修改成别的机器的ip地址 # sk.bind(('0.0.0.0', 9006)) # 所有访问我这台机器的通信ip地址(192.168.12.11),都能够访问到我的服务 sk.listen() # 监听(可以设置接收客户端等待的个数) conn, addr = sk.accept() # 阻塞,连接对象:存储本地的ip,port和对方的ip、port等 print(conn) # 像一个联通的通道,记录server的地址,client的地址等 print(addr) # client的地址 conn.send('我是服务器!'.encode('utf-8')) # 编码发送内容 msg = conn.recv(1024) # 获取1024个字节内容 print(msg.decode('utf-8'), addr) # 解码接收的内容 conn.close() # 关闭连接 sk.close() # 关闭server服务 # 通用测试版(客户端): import socket sk = socket.socket() # 创建socket对象 sk.connect(('127.0.0.1', 9006)) # 连接指定服务器 IP地址 + port content = sk.recv(1024).decode('utf-8') print(content) msg = input('请输入一个内容:') sk.send(msg.encode('utf-8')) # 发送一个内容到服务器 sk.close() # 断开与服务端的连接
# 一个服务端与多个客户端通信(服务端): import socket sk = socket.socket() sk.bind(('127.0.0.1', 9005)) sk.listen() while True: # 等待一个客户端的连接 conn, addr = sk.accept() # 内容 + 地址 while True: msg = input('请输入一个内容:') conn.send(msg.encode('utf-8')) if msg.upper() == 'Q': # 退出当前与客户端的连接 break content = conn.recv(1024).decode('utf-8') print(content) if content.upper() == 'Q': # 接收到一个客户端的请求退出 break # 关闭当前连接 conn.close() sk.close() # 一个服务端与多个客户端通信(客户端): import socket sk = socket.socket() sk.connect(('127.0.0.1', 9005)) while True: content = sk.recv(1024).decode('utf-8') print(content) if content.upper() == 'Q': # 接收到一个服务端的请求退出 break msg = input('请输入一个内容:') sk.send(msg.encode('utf-8')) if msg.upper() == 'Q': # 退出当前与服务端的连接 break sk.close()
2.3.2 基于UDP协议的socket:
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
示例:
# # 通用测试版(服务端): import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 创建一个udp的套接字对象 sk.bind(('127.0.0.1', 9002)) # 绑定 本地回环地址 + 端口 msg, client_addr = sk.recvfrom(1024) # 接收一个内容 内容本身 + 客户端的地址 print(msg.decode('utf-8')) # 打印接收到的客户端信息 print(client_addr) # 客户端地址 sk.sendto('收到'.encode('utf-8'), client_addr) # 发送一个信息到客户端 sk.close() # 关闭当前与客户端的连接 # 通用测试版(客户端): import socket sk = socket.socket(type=socket.SOCK_DGRAM) # SOCK_DGRAM udp服务(默认是tcp服务,不需要指定type) server_addr = ('127.0.0.1', 9002) # 指定需要连接的服务端地址 + 端口 sk.sendto('你好'.encode('utf-8'), server_addr) # 指定发送内容 + server_addr msg, _ = sk.recvfrom(1024) # 接收来自服务端的信息 内容本身 + 服务端的地址 print(msg.decode('utf-8')) # msg = sk.recv(1024).decode('utf-8') # 只接收服务端的信息本身,不接收地址用recv # print(msg) sk.close() # 关闭当前与服务端的连接
# 一个服务端与多个客户端通信(服务端): import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(('127.0.0.1', 9002)) while True: msg, client_addr = sk.recvfrom(1024) print(msg.decode('utf-8')) info = input('请输入一个内容:') sk.sendto(info.encode('utf-8'), client_addr) sk.close() # 一个服务端与多个客户端通信(客户端): import socket sk = socket.socket(type=socket.SOCK_DGRAM) server_addr = ('127.0.0.1', 9002) while True: info = input('请输入一个内容:') if info.upper() == 'Q': break # 客户端自动断开与服务端的连接,服务端不需要接收消息,直接break sk.sendto(info.encode('utf-8'), server_addr) msg, _ = sk.recvfrom(1024) # 通常用_表示不使用这个变量(占位变量) print(msg.decode('utf-8')) if msg.decode('utf-8').upper() == 'Q': # 接收到一个服务端的断开请求 break sk.close()
2.3.3 socket参数的详解:
创建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()关闭一个独立的插座。 |
三、黏包:
3.1 黏包现象:
同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。
注意:只有TCP有粘包现象,UDP永远不会粘包
3.2 TCP协议中的数据传递:
3.2.1 tcp协议的拆包机制:
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。
3.2.2 基于tcp协议特点的黏包现象成因:
在发送端 由于两条消息发送的间隔时间很短,且两条消息本身也很短,在发送之前被合成一条消息(合包机制)
在接收端 由于接收不及时导致两条先后到达的信息在接收端黏在了一起
黏包的本质:信息与信息之间没有边界的流式传输
3.3 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函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
总结:
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
3.4 黏包的解决方案:
自定义协议:struct模块
特点:该模块可以把一个类型,如数字,转成固定长度的bytes
>>> struct.pack('i',1111111111111) struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
3.4.1 使用struct模块的解决方案一(不推荐使用):
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
# 每次文件的上传与下载都需要发送数据长度,浪费系统资源,一般采用方案二的做法,把所有文件大小写在 # 一个字典中,最后通过发送字典得到数据长度来获取每次的内容大小 # 服务器: import struct import socket sk = socket.socket() sk.bind(('127.0.0.1', 9001)) sk.listen() conn, addr = sk.accept() num = conn.recv(4) num = struct.unpack('i', num)[0] # 获得文件名的长度 file_name = conn.recv(num).decode('utf-8') # 根据长度获取文件名 filesize = conn.recv(4) filesize = struct.unpack('i', filesize)[0] # 获取文件长度 with open(file_name, 'wb') as f: content = conn.recv(filesize) # 根据文件长度获取文件内容 f.write(content) conn.close() sk.close()
# 客户端: import os import struct import socket sk = socket.socket() sk.connect(('127.0.0.1', 9001)) filepath = input('请输入文件路径 :') filename = os.path.basename(filepath).encode('utf-8') # 获取文件名的bytes name_len = struct.pack('i', len(filename)) # 数据长度 sk.send(name_len) # 先发送一个数据长度 sk.send(filename) # 发送文件名的真实内容 filesize = os.path.getsize(filepath) # 文件大小 file_len = struct.pack('i', filesize) # 文件长度 sk.send(file_len) # 发送文件长度 # 读取文件,发送文件内容 with open(filepath, 'rb') as f: content = f.read() sk.send(content) sk.close()
3.4.2 使用struct模块的解决方案二:
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时 | 接收时 |
先发报头长度 |
先收报头长度,用struct取出来 |
再编码报头内容然后发送 | 根据取出的长度收取报头内容,然后解码,反序列化 |
最后发真实内容 | 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 |
# 服务器: import json import struct import socket sk = socket.socket() sk.bind(('127.0.0.1', 9001)) sk.listen() conn, addr = sk.accept() num = conn.recv(4) # 接收数据报头信息 num = struct.unpack('i', num)[0] # struct解包后得到一个元组 str_dic = conn.recv(num).decode('utf-8') # 接收指定文件大小 dic = json.loads(str_dic) # 字符串转化成字典 with open(dic['filename'], 'wb') as f: content = conn.recv(dic['filesize']) # 接收这个文件 f.write(content) conn.close() sk.close()
# 客户端: import os import json import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',9001)) filepath = input('请输入文件路径 :') filename = os.path.basename(filepath) # 文件名 filesize = os.path.getsize(filepath) # 文件大小 dic = {'filename': filename, 'filesize':filesize} # 创建一个字典 str_dic = json.dumps(dic) # 序列化成字符串 bytes_dic = str_dic.encode('utf-8') len_dic = len(bytes_dic) # 获取字典中字节长度 bytes_len = struct.pack('i', len_dic) # struct字节的长度 sk.send(bytes_len) # 发送报头 sk.send(bytes_dic) # 发送内容字节 with open(filepath, 'rb') as f: content = f.read() sk.send(content) sk.close()
四、 socketserver服务:
tcp协议能够处理多个client的请求,实现并发请求:
示例:
# 示例: # 服务端 import socketserver import time class Myserver(socketserver.BaseRequestHandler): # 创建一个服务器类方法 def handle(self): # 重写handle这个方法 conn = self.request # 获取一个连接(ip + prot) for i in range(200): conn.send(('hello % i' % i).encode('utf-8')) print(conn.recv(1024)) time.sleep(0.5) server = socketserver.ThreadingTCPServer(('127.0.0.1', 9005), Myserver) # 实例化一个server server.serve_forever() # 等待连接
# 客户端: import socket sk = socket.socket() sk.connect(('127.0.0.1', 9005)) while True: msg = sk.recv(1024) print(msg.decode('utf-8')) sk.send(b'byebye') sk.close()
五、 hmac模块应用:
5.1 定义:
以一个密钥和一个消息为输入,生成一个消息摘要作为输出
通过两个bytes类型的字符串得到一个新的bytes类型的字节码
5.2 hmac与hashlib比较:
hmac:生成一个bytes类型的字节码
hashlib:生成的是一个字符串
示例:
# 示例: # hmac消息摘要算法: import os import hmac print(os.urandom(32)) # 生成一个32位的字节码 hmac = hmac.new(b'alex sb', os.urandom(32)) print(hmac.digest()) # 加密后的字节码
六、 验证消息合法性:
6.1 tcp客服端与服务器的消息合法性验证:
定义:
服务器读取用户数据库中的用户密码与发送的随机值做与客户端一样的hmac运算,
然后与用户发送的结果比较,如果结果一致则验证用户合法
示例:
# 示例: # 服务器: import os import hmac import socket secret_key = b'alex sb' # os.urandom(32) 给每一客户端发送一个随机的字符串,来保证即使数据被拦截你也不能使用这个消息 sk = socket.socket() sk.bind(('127.0.0.1', 9001)) sk.listen() conn, addr = sk.accept() rand = os.urandom(32) conn.send(rand) hmac = hmac.new(secret_key, rand) # hmac摘要算法 res = hmac.digest() ret = conn.recv(1024) if ret == res: print('是合法的客户端') else: print('不是合法的客户端') conn.close()
# 客户端: import hmac import socket secret_key = b'alex sb' sk = socket.socket() sk.connect(('127.0.0.1', 9001)) rand = sk.recv(32) hmac = hmac.new(secret_key, rand) res = hmac.digest() sk.send(res) sk.close()
6.2 端口占用处理的处理方式:
定义:
防止程序因为意外退出之后重启程序出现端口被占用的错
生产环境中不能用,平时开发过程中可以设置
示例:
# 示例: import socket sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重置端口,归还系统资源 sk.bind(('127.0.0.1', 9001)) sk.listen() # 其他代码... sk.close()
七、 非阻塞模型:
# 示例: # 非阻塞实现多用户请求: import socket sk = socket.socket() sk.bind(('127.0.0.1', 9001)) sk.setblocking(False) # blocking阻塞 setblocking设置阻塞,默认为阻塞态 sk.listen() conn_l = [] # 用户列表 while True: try: conn, addr = sk.accept() conn_l.append(conn) except BlockingIOError: for conn in conn_l: try: msg = conn.recv(1024).decode('utf-8') print(msg) conn.send(msg.upper().encode('utf-8')) except BlockingIOError: pass