21网络编程(socket、黏包现象、socketserver模块)
前言:鉴于socket实际工作基本不会直接接触,但是面试经常问。所以不花太多时间在这里。
1、利用socket来实现最基础的网络通信。
服务端:
# coding:utf-8 import socket # 创建socket对象 server = socket.socket() # 绑定IP和端口 server.bind(('127.0.0.1', 8000)) # 连接数 server.listen(5) # 等待客户端来连接,如果没人来就等待 # conn是客户端和服务端连接的对象(伞),服务端以后要通过该对象进行收发数据。 # addr是客户端的地址信息 conn, addr = server.accept() # 通过对象去获取,(客户端通过伞发送的消息) data = conn.recv(1024) print(data) # 服务端通过连接对象(伞)给客户端回复消息 conn.send(b'stop') # 与客户端断开连接(放开伞) conn.close() # 关闭服务端的连接。 server.close()
用户端:
import socket client = socket.socket() # 向服务端发起连接请求(递伞) # 阻塞,知道连接成功后才会继续向下走 client.connect(('127.0.0.1', 8000)) # 链接上服务端后,向服务端发送消息。 client.send(b'hello') # 等待服务端回消息 data = client.recv(1024) print(data) client.close()
上述代码实现了一个很简单的功能。服务端等待用户连接。用户端连接之后发送一个hello然后关闭连接。服务端收到之后回复一个stop。然后断开连接,关闭服务。
2、黏包
黏包产生的原因:连续多次消息的快速发送到接收方的缓存池中。接收方取的时候无法正确判断每次接收的消息长度(也就是无法正确区分消息与消息之间的边界值)。
黏包产生的结果:最简单的是消息会乱掉。上次没接收完的消息,会混在下次接收的头部。具体看场景。
解决黏包的方法:struct模块的pack和unpack两个方法。首先用pack对于字符串进行压缩,这样长度就固定了。然后接收后unpack解压缩,循环取值拼接(笨方法就是接收方每次接收完给个回复,然后发送方再进行下一次发送。还有一种就是发送方每次发送等待个1s左右。)
如下:一个简单的文件上传的例子
项目结构如下:
客户端代码:
# coding:utf-8 import socket import os import json import struct import hashlib sock = socket.socket() sock.connect(('127.0.0.1', 8800)) while True: cmd = input("请输入命令:") if cmd == 'exit': break action, filename = cmd.strip().split(" ") filesize = os.path.getsize(filename) if action == 'put': file_info = { "action": action, # put "filename": filename, # 文件名 "filesize": filesize, # 文件大小 } file_info_json = json.dumps(file_info).encode('utf8') ret = struct.pack('i', len(file_info_json)) print(len(ret)) # 发送file_info_json的打包长度 sock.send(ret) # 发送file_info_json字节串 sock.send(file_info_json) md5 = hashlib.md5() # 发送文件数据 with open(filename, 'rb') as f: for line in f: sock.send(line) md5.update(line) data = sock.recv(1024).decode('utf8') if data == '200': print(md5.hexdigest()) md5_val = md5.hexdigest() sock.send(md5_val.encode('utf8')) valid = sock.recv(1024).decode('utf8') if valid == '100': print('文件上传成功') else: print('文件上传失败!') else: print('服务器出错了!') else: print('请输入正常的命令')
服务端代码:
# coding:utf-8 import socket import json import struct import hashlib sock = socket.socket() sock.bind(('127.0.0.1', 8800)) sock.listen(5) while True: print("server is working.......") conn, addr = sock.accept() while True: # 接收json的打包长度 file_info_lenght_pack = conn.recv(4) file_info_lenght = struct.unpack('i', file_info_lenght_pack)[0] # 接收json字符串 file_info_json = conn.recv(file_info_lenght).decode('utf8') file_info = json.loads(file_info_json) action = file_info.get('action') filename = file_info.get('filename') filesize = file_info.get('filesize') md5 = hashlib.md5() # 循环接收文件 with open("put/"+filename, "wb") as f: recv_data_length = 0 while recv_data_length < filesize: data = conn.recv(1024) recv_data_length += len(data) f.write(data) md5.update(data) print("文件总大小:%s,已成功接收%s" % (filesize, recv_data_length)) print('接收成功') conn.send(b'200') print(md5.hexdigest()) md5_val = md5.hexdigest() # 服务端的md5 client_md5 = conn.recv(1024).decode('utf8') # 接收用户端的md5。会黏包。要处理下。 if md5_val == client_md5: conn.send(b'100') else: conn.send(b'10000')
注:这里因为最后用md5做了个文件一致性的对比。所以struct显得反倒没那么突出了。理解能用就行。
3、socketserver模块
import socketserver ''' 在socketserver模块中 conn = self.request ''' class Myserver(socketserver.BaseRequestHandler): def handle(self): ''' 这里写逻辑代码 :return: ''' pass # 相当于 1、创建socket对象 2、self.socket.bing() 3、self.socket.listen(5) server = socketserver.ThreadingTCPServer(('127.0.0.1', 8899), Myserver) # 相当于执行server.accept(),返回两个参数。 server.serve_forever()
socketserver就相当于把最开始的几行代码封装起来了。并且还开了个多线程。不过这里稍微了解下感觉就ok了。