14、python网络编程之Socket
一、什么是socket
Socket也叫套接字,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字:AF_UNIX
基于网络类型的套接字:AF_INET
二、套接字工作流程
1、套接字基于tcp协议的工作流程
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
服务端
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 网络类型基于tcp的流式协议的套接字对象
server.bind(('127.0.0.1', 9939)) # 绑定IP+PORT
server.listen(5) # 设置半连接池大小
connect, client_addr = server.accept() # 进入监听状态, 拿到连接和客户端地址
print(client_addr)
data = connect.recv(1024) # 接收数据,最大接收1024Bytes
connect.send(data.upper()) # 返回数据,原数据大写后返回
connect.close() # 关闭连接
客户端
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 网络类型基于tcp的流式协议的套接字对象
client.connect(('127.0.0.1', 9939)) # 连接服务端
client.send('hello socket'.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
client.close()
先运行服务端,后运行客户端,得到运行结果
# 客户端结果
HELLO SOCKET
# 服务端结果
('127.0.0.1', 50140)
2、套接字基于udp协议的工作流程
udp协议不同tcp协议的地方在于:
- 不需要建立链接
- udp协议为报文式协议而tcp协议是基于链接的流式协议
下面我们来模拟socket基于udp协议进行通信
服务端
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 网络类型基于UDP协议的报式套接字对象
server.bind(('127.0.0.1', 9939)) # 绑定IP+PORT
data, client_addr = server.recvfrom(1024) # 接收数据,最大接收1024Bytes
print(data.decode('utf-8'))
server.sendto(data.upper(), client_addr) # 返回大写内容数据
server.close() # 关闭连接
客户端
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 网络类型基于UDP协议的报式套接字对象
client.sendto('hello socket'.encode('utf-8'), ('127.0.0.1', 9939)) # 发送数据"hello socket"
data, client_addr = client.recvfrom(1024) # 接收返回数据
print(data.decode('utf-8'))
client.close() # 关闭链接
运行结果
# 客户端
HELLO SOCKET
# 服务端
hello socket
三、基于tcp实现远程命令
服务端
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import subprocess
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 9939))
server.listen(5)
while True:
connect, client_addr = server.accept()
while True:
try:
cmd = connect.recv(1024)
if not cmd:
break
obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = obj.stdout.read()
err = obj.stderr.read()
connect.send(out)
connect.send(err)
except Exception:
break
connect.close()
客户端
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 9939))
while True:
cmd = input('请输入命令:').strip()
if not cmd:
continue
client.send(cmd.encode('utf-8'))
cmd_res = client.recv(1024)
print(cmd_res.decode('gbk')) # windows系统默认以gbk编码,所以使用gbk解码,linux使用utf-8进行解码
运行结果
请输入命令:dir
驱动器 D 中的卷是 Data
卷的序列号是 72DE-32A1
D:\Code\PyCode\chaney02\4.python网络编程\03、基于TCP协议远程执行命令 的目录
2021/02/12 17:28 <DIR> .
2021/02/12 17:28 <DIR> ..
2021/02/12 16:56 333 Client.py
2021/02/12 17:28 581 Server.py
2 个文件 914 字节
2 个目录 127,736,856,576 可用字节
四、tcp协议粘包问题
什么是粘包?
在tcp协议中,发送端为了将多个发往接收端的包更有效的发送到对方,使用了优化方法Nagle算法,它会将多次间隔较小切数据量较小的数据合并成一个大的数据块,然后进行封包。这样导致接收端就很难分别出数据的边界了,就会出现我们常说的“粘包”现象。此时就需要我们提供科学的从拆包机制。所以面向流的通信是无消息保护边界的。
如何解决粘包?
我们可以发现粘包问题是在于接收端不知道数据的边界,无法对数据进行一个拆分形成有效数据。针对这一点我们可以得到解决粘包问题就是要让接收端知道如何去对数据进行拆包,找到数据与数据间的边界。这时候我们可以给数据封包时添加一个头部,头部信息中包含数据的描述信息,如这一份数据的信息的大小,这样接收端就可以根据头部信息对数据包进行拆包,对接收到的数据进行划分。
这其实就是在自定一个协议,在这个过程中,需要注意的是头部信息的长度需要固定(利用struct.pack()等),方便接收端进行解析。同时要注意对头部信息进行序列化(json, pickle等)方便反解。