socket
应用层和tcp,ucp协议之间的一个接口,用户只需操作接口,复杂的数据组织工作由其内部自行完成。
TCP协议的socket(套接字):
服务端
import socket sk = socket.socket() # 创建一个套接字对象 sk.bind(('127.0.0.1', 8080)) # 绑定本地IP地址和端口 sk.listen() # 监听 conn, address = sk.accept() # 创建连接 while True: ret = conn.recv(1024) # 接收数据 需要指定接收字节数 if ret == b'bye': conn.send(ret) break print(ret.decode('utf-8')) info = input('>>>').encode('utf-8') conn.send(info) # 发送数据 必须是bytes类型 conn.close() # 关闭连接 sk.close() # 关闭套接字
客户端
import socket sk = socket.socket() # 创建套接字 sk.connect(('127.0.0.1', 8080)) # 连接服务端 while True: info = input('>>>').encode('utf-8') sk.send(info) # 发送数据 ret = sk.recv(1024) # 接收数据 if ret == b'bye': break print(ret.decode('utf-8')) sk.close() # 关闭套接字
UDP协议的scoket(套接字):
服务端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 创建ucp套接字对象 sk.bind(('127.0.0.1', 8080)) # 绑定IP和端口 msg, address = sk.recvfrom(1024) # 等待接收数据 ucp必须先接收数据 print(msg.decode('utf-8')) sk.sendto(b'hello', address) # 发送数据 要携带发送数据的地址 sk.close()
客户端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 创建ucp套接字对象 ip_port = ('127.0.0.1', 8080) # 指定服务端IP和端口 sk.sendto(b'hi', ip_port) # 发送数据到指定服务端 msg, address = sk.recvfrom(1024) # 接收返回的数据 print(msg.decode('utf-8')) sk.close()
黏包
先写个例子看下黏包现象。

import socket sk = socket.socket() sk.bind(('127.0.0.1', 8080)) sk.listen() connect, address = sk.accept() while True: cmd = input('>>>') if cmd == 'q': connect.send(b'q') break connect.send(cmd.encode('gbk')) ret = connect.recv(1024).decode('gbk') print(ret) connect.close() sk.close()

import socket import subprocess sk = socket.socket() sk.connect(('127.0.0.1', 8080)) while True: cmd = sk.recv(1024).decode('gbk') if cmd == 'q': break res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) sk.send(res.stdout.read()) sk.send(res.stderr.read()) sk.close()
这里用到了一个subprocess模块,它可以创建子进程,并与进程进行各种交互。
import subprocess ret = subprocess.Popen('dir', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print('stdout:'+ret.stdout.read().decode('gbk')) # 读取正常的返回值 解码方式以操作系统而定 print('stderr:'+ret.stderr.read().decode('gbk')) # 读取错误的返回值 解码方式以操作系统而定 """ subprocess.Popen('dir', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) dir:系统命令 shell:True确认 False报错 stdout:接收正常返回值 stderr:接收错误返回值 """
当执行命令的时候,会出现上次命令的返回的信息在执行下一个命令时才会显示。这种连续传送数据,数据发生混乱的情况就叫做黏包。
在tcp协议的数据传输中,在时间间隔短暂的情况下进行连续的send,且send数据量小,
由于自身的优化算法,会将这些数据打包在一起才发送出去。
这样接收端就无法分辨数据之间的分界,造成接收到的数据和实际不符合的现象
UDP协议不建立连接,也没有什么优化算法,所以不会有黏包,但是会出现丢包。
黏包问题解决
了解黏包问题后,归根结底是因为接收端无法知道发送端所发数据的大小,知道了数据大小我们就可以接收到对应的数据,从而避免黏包造成的数据混乱问题。下面具体代码看下解决办法:

import struct import socket sk = socket.socket() sk.bind(('127.0.0.1', 8080)) sk.listen() connect, address = sk.accept() while True: cmd = input('>>>') if cmd == 'q': connect.send(b'q') break connect.send(cmd.encode('gbk')) num = connect.recv(4) # 因为长度转成了4位的,所以这里接收的一定是要传输数据的长度struct.pack值 num = struct.unpack('i', num)[0] # unpack转回原来的数值,结果是个元组,取第一个值 res = connect.recv(num).decode('gbk') # 接收对应长度的数据,这样就可以接收到自己想要的数据了 print(res) connect.close() sk.close()

import struct import socket import subprocess sk = socket.socket() sk.connect(('127.0.0.1', 8080)) while True: cmd = sk.recv(1024).decode('gbk') if cmd == 'q': break ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ret_out = ret.stdout.read() ret_err = ret.stderr.read() len_num = len(ret_err) + len(ret_out) # 计算传输的数据的长度 num_bytes = struct.pack('i', len_num) # 将int长度转换为4位的bytes类型数据 sk.send(num_bytes) # 将这个数据传给接收端 sk.send(ret_out) sk.send(ret_err) sk.close()
这里用到了一个struct模块,它通过pack(‘i’,v)可以将指定类型的数据转换为特定长度的字符串(bytes),在通过unpack重新取回原来的数据,以元组的方式返回。
我们还可以通过自定制报头来更好的处理TCP数据传输过程中的黏包问题。

import json import struct import socket sk = socket.socket() sk.bind(('127.0.0.1', 8090)) sk.listen() buffer = 4096 connect, address = sk.accept() num = connect.recv(4) head_len = struct.unpack('i', num)[0] json_head = connect.recv(head_len).decode('utf-8') head = json.loads(json_head) file_size = head['file_size'] with open(head['file_name'], 'wb') as f: while file_size: if file_size > buffer: content = connect.recv(buffer) f.write(content) file_size -= len(content) else: content = connect.recv(file_size) f.write(content) file_size -= len(content) connect.close() sk.close()

import os import json import struct import socket sk = socket.socket() sk.connect(('127.0.0.1', 8090)) buffer = 4096 head = {'file_name': r'CentOS-7-x86_64-DVD-1708.iso', 'file_path': r'D:下载安装包', 'file_size': None} path = os.path.join(head['file_path'], head['file_name']) file_size = os.path.getsize(path) head['file_size'] = file_size json_head = json.dumps(head) head_len = struct.pack('i', len(json_head)) sk.send(head_len) sk.send(json_head.encode('utf-8')) with open(path, 'rb') as f: while file_size: if file_size >= buffer: content = f.read(buffer) sk.send(content) file_size -= buffer else: content = f.read(buffer) sk.send(content) break sk.close()
验证客户端合法性
验证客户端的合法性:为了防止一些违法的客户端连接服务端进行数据的盗取或破坏

import os import hmac import socket sk = socket.socket() sk.bind(('127.0.0.1', 8080)) sk.listen() secret_key = b'secret' # 创建秘钥 def check_connect(conn): msg = os.urandom(32) # 随机生成一个32字节的bytes类型的字符串 conn.send(msg) # 将随机字符串发送给连接端 h = hmac.new(secret_key, msg) # 通过一个秘钥创建一个加密对象 digest = h.digest() # 获取加密值 client_digest = conn.recv(1024) return hmac.compare_digest(digest, client_digest) # 对比连接端和服务端的加密值是否相同 返回bool值 connect, address = sk.accept() if check_connect(connect): # 相同 说明为自己的合法客户端 print('合法') else: # 不相同说明为违法客户端 print('不合法') connect.close() sk.close()

import hmac import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) secret_key = b'secret' msg = sk.recv(1024) # 接收服务端的随机字符串 h = hmac.new(secret_key, msg) # 创建加密对象 digest = h.digest() # 获取加密值 sk.send(digest) # 发给服务端验证 sk.close()
秘钥是验证合法性的关键,所以秘钥要加密保存,自己合法的客户端要加密保存好秘钥,这里只是简单的演示。
这里的hmac模块提供hmac算法,作用和hashlib的加盐摘要类似,使用秘钥对内容进行加密算法。
socketserver模块
socketserver模块:可以使tcp服务端同时与多个客户端进行数据传输工作

import socketserver class MY_socket(socketserver.BaseRequestHandler): # 创建一个自己的类,一定要继承socketserver.BaseRequestHandler def handle(self): # 一定要创建一个handle函数 while True: msg = self.request.recv(1024).decode('utf-8') # 这里的self.request就是建立的连接 if msg == 'q': break print(msg) info = input('>>>') self.request.send(info.encode('utf-8')) if __name__ == '__main__': # 创建一个socketserver里TCP多线程服务的对象 参数为IP端口元组以及自己定义的类 server = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MY_socket) # server.allow_reuse_address = True server.serve_forever() # 永远启动服务,除非强制关闭或意外停止。
服务端要使用socketserver模块来处理,客户端则不需要,和正常的连接和数据传输方式一样。

import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) while True: info = input('>>>') if info == 'q': sk.send(b'q') break sk.send(('client:'+info).encode('utf-8')) msg = sk.recv(1024).decode('utf-8') print(msg) sk.close()

import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) while True: info = input('>>>') if info == 'q': sk.send(b'q') break sk.send(('client1:'+info).encode('utf-8')) msg = sk.recv(1024).decode('utf-8') print(msg) sk.close()