一 socket编程
如上图所示,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。对用户来说,一组简单的套接字相关函数就是全部,让Socket去组织数据,以符合指定的协议,如TCP或UDP就可以完成通信过程了。
Python内置了2个常用模块提供给我们完成socket编程。
-
低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统 Socket 接口的全部方法。
-
高级别的网络服务模块 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。
二 实现基于TCP协议的socket通信
服务端:
import socket # 创建TCP套接字对象 # 套接字主要有两种类型:基于文件类型的AF_UNIX,基于网络类型的AF_INET(常用) # 套接字数据传输格式:SOCK_STREAM 字节流传输方式,基于TCP通信(默认值),SOCK_DGRAM 数据报文传输方式,基于UDP通信 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定通信端口 # local 本地,本端 # remote 外地,远程,对端 laddr = ("127.0.0.1", 8000) sk.bind(laddr) # 监听通信端口,backlog参数设置阻塞等待的客户端连接数量,默认为1 sk.listen(5) # 再次增加无限循环,实现让服务端可以和多个客户端不断的建立三次握手、收发消息、四次挥手 while True: # 接收客户端连接(三次握手) conn, raddr = sk.accept() print(f"客户端{raddr}连接了服务器!") # 增加无限循环,让服务端可以多次接收来自对端发送的数据 while True: # 接收客户端返回数据,数据以bytes形式返回 # recv 等待接收对端发送的数据,程序会阻塞,收到数据之后,才会继续执行后面的代码 content = conn.recv(1024).decode('utf-8') # 如果客户端关闭连接,则服务端也要关闭与当前客户端的连接 if content == "exit": break if content: print(f"客户端{raddr}连接过来数据: {content}") # 服务端也可以发送数据给客户端【将来就是通过数据库中的资源数据提供给客户端】 message = input(">:") conn.send(message.encode("utf-8")) # 四次挥手 conn.close()
客户端1:
import socket # 创建TCP套接字对象 sk = socket.socket() # 连接服务端 addr = ("127.0.0.1", 8000) sk.connect(addr) # 增加无限循环,让客户端可以多次发送数据给服务端 while True: message = input(">: ") sk.send(message.encode("utf-8")) if message == "exit": break # 客户端退出 # 接收服务端发送的数据 content = sk.recv(1024).decode("utf-8") if content: print(f"服务端: {content}") sk.close()
附:
1 TCP收发消息不为空原因
TCP是基于数据流的,所以收发的消息不能为空,这就需要在客户端和服务端都添加空消息的判断处理逻辑,防止程序卡住,而udp是基于数据报文的,即便是你输入的是空内容(直接回车),也可以被发送,
udp协议会帮你封装上消息头发送过去。对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 下图UDP的消息头为保护边界
TCP:[三次握手] 数据xx xx xxxxx xxxx [四次挥手]
UDP: [8个字节的消息头]数据xxx [8个字节的消息头]xxx [8个字节的消息头]xxx
三 实现基于UDP协议的socket通信
服务端:
import socket sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) laddr = ('127.0.0.1', 9000) sk.bind(laddr)while True: content, raddr = sk.recvfrom(1024) print(f"来自[{raddr[0]}:{raddr[1]}]的一条消息:\033[1;44m{content.decode('utf-8')}\033[0m") message = input('>:').strip() sk.sendto(message.encode('utf-8'), raddr)
客户端1:
import socket sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) friends = { 'xiaoming': ('127.0.0.1', 9000), 'xiaobai': ('127.0.0.1', 9000), } while True: name = input('请选择聊天对象: ').strip() if name == 'exit': break while True: content = input('>: ').strip() if content == 'exit': break # 如果内容为空,或者name为空,或者name不在friends字典中,则跳过本次循环,不要往下执行 if not content or not name or name not in friends: continue sk.sendto(content.encode('utf-8'), friends[name]) message, raddr = sk.recvfrom(1024) print(f"来自[{raddr[0]}:{raddr[1]}]的一条消息:\033[1;44m{message.decode('utf-8')}\033[0m") sk.close()
四 TCP通信过程中的粘包问题
1 粘包问题:
A端多次发送的数据,到达B端被接收时会存在一次性接受A端多次发送数据的情况。
2 粘包出现原因:
接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。粘包是由TCP协议本身是面向流造成的(三次握手建立连接后开始发送数据),所以只有TCP协议有粘包现象,而UDP是面向消息报文的协议,所以永远不会粘包。
3 粘包现象:
粘包现象一:
在发送端, 由于两个数据短, 发送的时间隔较短, 所以在发送端形成粘包
粘包现象二:
在接收端, 由于两个数据几乎同时被发送到对方的缓冲区中, 所有在接收端形成了粘包
总结:发送数据的时间间隔短 或者 接受数据不及时, 都会出现粘包问题。
4 粘包现象出现原因:
应用程序无法直接操作网络,需要操作系统去管理底层。应用程序会将数据放于应用程序自己的缓存区,操作系统去程序缓存区取数据放于操作系统自己的缓存区,然后调用硬件网卡传输数据到对端网卡,对端操作系统到其网卡取数据后放于系统缓存中,应用程序主动到操作系统缓存中取数据。期间数据的收发存在延时或发送数据时间间隔短,因此存在数据粘包问题。
5 解决方案
使用Struct模块来解决:
struct模块常用方法:
普通数据基本方法:
struct.pack() 打包
struct.unpack 解包
二进制数据打包:
struct.pack_into() 打包
struct.unpack_from() 解包
示例:
5.1 基于struct模块,对普通数据进行打包和解包
import struct """函数式用法""" # content = 'abcabc'.encode() # # 组装数据 # source_data = (内容长度, 内容) # source_data = (len(content), content) # print("源数据:", source_data) # # 源数据: (6, b'abcabc') # # # 打包数据 # packed_data = struct.pack('i6s', *source_data) # i表示整数int,6s表示6个字节长度的字符串str # # # 打包后的结果得到字节流 # print(packed_data) # # b'\x06\x00\x00\x00abcabc' # # 解包 # source_data = struct.unpack("i6s", packed_data) # # 解包的结果是一个元组 # print(source_data) # # (6, b'abcabc') """面向对象用法""" cmd = 101 # 假设101表示商品信息 # 消息体 content = '商品具体信息'.encode() # 消息长度 length = len(content) # 组装数据 source_data = (cmd, content, length) print("源数据:", source_data) # 源数据: (101, b'\xe5..', 18) # 打包数据 st = struct.Struct(f'i{length}si') packed_data = st.pack(*source_data) # i表示整数int,6s表示6个字节长度的字符串str # 打包后的结果得到字节流 print(packed_data) #源数据: (101, b'\xe5\x95\..', 18) # 解包数据 source_data = st.unpack(packed_data) # 解包的结果是一个元组 print(source_data) #(101, b'\xe5\x95\..', 18) print(f"消息类型:{source_data[0]}, 消息体:{source_data[1].decode('utf-8')}, 消息长度: {source_data[2]}") # 消息类型:101, 消息体:商品具体信息, 消息长度: 18
5.2 基于struct模块,对二进制数据进行打包和解包
import struct, binascii, ctypes # 第一条数据 source_data1 = (1, 'abcabc'.encode(), 2.7) s1 = struct.Struct('i6sf') # i 表示int,6s表示三个字符长度的字符串,f 表示 float # 第二条数据 source_data2 = ('defg'.encode(), 101) s2 = struct.Struct('4si') # 获取数据格式的字节长度 print(s1.size) print(s2.size) # 把数据转换成二进制,并保存到缓冲区(buffer) prebuffer = ctypes.create_string_buffer(s1.size+s2.size) # 参数是缓冲区的内存大小[单位是字节] print('打包前缓冲区:', binascii.hexlify(prebuffer)) # 打包前缓冲区 : b'0000000000000000000000000000000000000000' # 从缓冲区中提取数据并进行打包 s1.pack_into(prebuffer, 0, *source_data1) # s1.size 因为前面数据位已经被第一段数据占用了,此处要空出s1.size的长度 print('打包第一段数据:', binascii.hexlify(prebuffer)) # 打包第一段数据: b'010000006162636162630000cdcc2c400000000000000000' s2.pack_into(prebuffer, s1.size, *source_data2) print('打包第二段数据:', binascii.hexlify(prebuffer)) # 打包第二段数据: b'010000006162636162630000cdcc2c406465666765000000' """解包数据""" print(s1.unpack_from(prebuffer, 0)) print(s2.unpack_from(prebuffer, s1.size))
五 socketserver模块
socket模块使用的TCP通信是基于一对一的通信模式。socketserver是python的内置模块,是基于原有socket模块又进行了一层封裝,实现了一个TCP服务端可以同时与多个TCP客户端同时进行通信即实现了并发客户端。
socketserver模块中分两大类:server类(解决与客户端并发连接的问题)和RequestHandler类(解决与客户端的并发通信问题)
基本使用
#socketserver主要提供的操作是给服务端实现并发连接客户端的,所以客户端代码还是使用socket即可 server端: import socketserver class TCPServer(socketserver.BaseRequestHandler): """自定义服务端Request类""" # handle 方法是每当有一个客户端发起connect申请建立连接时, 自动执行handle方法,所以方法名必须固定为handle def handle(self): print("建立连接") if __name__ == '__main__': server = socketserver.ThreadingTCPServer(("127.0.0.1", 9000), TCPServer) # 启动服务器 server.serve_forever() client端: import socket sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) sk.close()