socket(套接字)编程
套接字的工作流程
TCP和UDP对比
TCP(Transmission Control Protocol)
- 可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;文件传输程序。
UDP(User Datagram Protocol)
- 不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
TCP协议下的Socket编程
基于TCP和UDP的区别,所以对于socket编程也分为TCP协议和UDP协议
对于基于TCP协议的socket编程的工作流程如下:
服务端:
- 服务器端先初始化socket对象
- 绑定监听的IP和端口
- 调用accept进行阻塞,等待客户端建立连接
- 如果有客户端连接上来,获取对方的信息,并且简历TCP连接对象
- 通过连接对象以及recv()、send()方法接收和回复客户端的信息
- 信息传输完成以后,通过close()终止TCP连接
客户端:
- 同样先初始化socket对象
- 指定服务端的IP与端口号信息
- 通过connect来与服务端建立TCP连接
- 通过send()向服务端发送信息
- 通过recv()接收服务端发过来的信息
- 信息传输完成以后,通过close()终止TCP连接
示例一:单个客户端与服务端通信
服务端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 初始化socket对象,可以理解为买电话
# socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)。
phone.bind(('127.0.0.1',8080)) # 0 ~ 65535 1024之前系统分配好的端口 绑定电话卡
phone.listen(5) # 同一时刻有5个请求
conn, client_addr = phone.accept() # 调用accetp阻塞代码,等待客户端的连接,可以理解为接电话
print(conn, client_addr, sep='\n') # 如果有客户端连接,获取conn TCP连接对象,client_addr客户端的信息
from_client_data = conn.recv(1024) # 调用recv方法来接收客户端发来的数据,1024是一次接收的字节数Bytes
print(from_client_data.decode('utf-8')) # 打印接收到的信息
conn.send(from_client_data.upper()) # 调用send方法给客户端回复信息
conn.close() # 断开TCP连接,可以理解为挂电话
phone.close() # 关机
客户端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 同样初始化socket对象
phone.connect(('127.0.0.1',8080)) # 主动与服务端建立通信,端口为服务端监听的端口,127.0.0.1代指我们本机
phone.send('hello'.encode('utf-8')) # 连接以后,直接给服务端发信息
from_server_data = phone.recv(1024) # 通过recv方法等待服务端的回信
print(from_server_data) # 打印服务端回复的消息
phone.close() # 挂电话
示例二:循环通信
刚刚写的这个通信是单词的,通信完成一次以后,就close断开的。我们可以加上while循环,让他们循环通信
服务端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
conn,client_addr = phone.accept()
print(conn,client_addr,sep='\n')
while True: # 建立连接以后,循环的接收和发送信息,如果检测到ConnectionResetError异常,就断开连接
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper())
except ConnectionResetError:
break
conn.close()
phone.close()
客户端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while True: # 客户端这边也循环的发送信息,并且等待接收
client_data = input('>>> ')
phone.send(client_data.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close()
示例三:循环连接通信
比如淘宝的机器人客服,有时候我们终止聊天了,机器人还是会继续等待有人跟他聊天,即便别人断开了,他也不会退出,继续等待下一个人建立连接。这个时候,我们让等待客户端连接的代码也加入到循环体中
服务端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while True: # 将accept方法也加入到循环体中,让他循环的监控客户端的连接
conn,client_addr = phone.accept()
print(conn,client_addr,sep='\n')
while True:
try:
from_client_data = conn.recv(1024)
if not from_client_data:
break
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper())
except:
break
conn.close()
phone.close()
客户端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while True:
client_data = input('>>> ')
phone.send(client_data.encode('utf-8'))
if client_data == 'q':break
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close()
示例四:远程代码执行
既然我们可以通过客户端给服务端发送信息,那么我们可不可以在服务端中设计让服务端接收到的信息不在打印出来,而是通过某些模块执行我们发过去的内容,比如windows中的CMD支持很多的命令,我们可以发送命令,让服务端执行,并且返回执行结果。
服务端:
import socket
import subprocess # 通过sunprocess模块来执行cmd命令
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
客户端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while True: # 客户端还是正常发信息即可,只不过注意windows下的默认编码是gbk
client_data = input('>>> ')
phone.send(client_data.encode('utf-8'))
if client_data == 'q':break
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close()
UDP协议下的Socket编程
由于UDP协议是无连接的,所以先启动哪一端都不会报错。直接给对方发消息,不需要建立连接,但是对方能不能收到就不一定了。
所以说UDP协议是不可靠的。
对于基于UDP协议的socket编程的工作流程如下:
服务端:
- 初始化Socket,并且绑定端口和IP地址
- 直接通过recvfrom()来等待接收消息,无需建立连接
- 通过send()回复消息
- 关闭socket
客户端:
- 同样初始化Scket
- 指定服务端的端口和IP地址
- 通过sendto()给服务端发消息,由于没有建立连接,所以发消息的时候要带上对方的地址
- 通过recvfrom()来等待接收消息
- 关闭socket
示例:基于UDP协议的通信
服务端:
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# socket.SOCK_DGRAM 代表选择的是upd协议
udp_socket.bind(('127.0.0.1', 8000))
# 接收消息
msg,addr = udp_socket.recvfrom(1024)
print(addr,':',msg.decode('utf-8'))
# 回复消息
udp_socket.sendto(msg.upper(),addr)
# 关闭Socket
udp_socket.close()
客户端:
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8000)
# 发送消息
msg = input(">>>")
udp_socket.sendto(msg.encode('utf-8'), ip_port)
# 接收消息
back_msg,addr = udp_socket.recvfrom(1024)
print(back_msg.decode('utf-8'))
udp_socket.close()
示例:自制时间服务器
服务端:
import socket
import time
ip_port = ('127.0.0.1', 8080)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(ip_port)
while True:
msg, addr = s.recvfrom(1024)
stru_time = time.localtime() # 当前结构化时间
if not msg:
time_fmt = '%Y %m %d' # 如果没有输入字符串时间格式,默认格式'%Y %m %d'
else:
time_fmt = msg.decode('utf-8')
back_msg = time.strftime(time_fmt, stru_time) #用time.strftime将结构化时间转换为字符串时间
s.sendto(back_msg.encode('utf-8'), addr)
s.close()
客户端:
import socket
ip_port = ('127.0.0.1', 8080)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input('请输入时间格式(默认%Y %m %d)>>: ').strip()
s.sendto(msg.encode('utf-8'), ip_port)
data,addr = s.recvfrom(1024)
print("当前时间:",data.decode('utf-8'))
更多用法
服务端套接字函数
s.bind() | 绑定(主机,端口号)到套接字 |
---|---|
s.listen() | 开始TCP监听 |
s.accept() | 被动接受TCP客户的连接,(阻塞式)等待连接的到来 |
客户端套接字函数
s.connect() | 主动初始化TCP服务器连接 |
---|---|
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数
s.recv() | 接收TCP数据 |
---|---|
s.send() | 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) |
s.sendall() | 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) |
s.recvfrom() | 接收UDP数据 |
s.sendto() | 发送UDP数据 |
s.getpeername() | 连接到当前套接字的远端的地址 |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回指定套接字的参数 |
s.setsockopt() | 设置指定套接字的参数 |
s.close() | 关闭套接字 |
面向锁的套接字方法
s.setblocking() | 设置套接字的阻塞与非阻塞模式 |
---|---|
s.settimeout() | 设置阻塞套接字操作的超时时间 |
s.gettimeout() | 得到阻塞套接字操作的超时时间 |
面向文件的套接字的函数
s.fileno() | 套接字的文件描述符 |
---|---|
s.makefile() | 创建一个与该套接字相关的文件 |
粘包
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
这些I/O缓冲区特性可整理如下:
- I/O缓冲区在每个TCP套接字中单独存在;
- I/O缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
两种情况下会发生粘包
- 接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
服务端:
import socket
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
客户端:
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close()
- 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
服务端:
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
conn, client_addr = phone.accept()
frist_data = conn.recv(1024)
print('1:',frist_data.decode('utf-8')) # 1: helloworld
second_data = conn.recv(1024)
print('2:',second_data.decode('utf-8'))
conn.close()
phone.close()
客户端:
import socket
import time
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
phone.send(b'hello')
# time.sleep(3)
phone.send(b'world')
phone.close()
粘包的解决方案
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
# 将一个数字转化成等长度的bytes类型。
ret = struct.pack('i', 18334)
print(ret, type(ret))
# 通过unpack反解回来 返回一个元组回来
ret1 = struct.unpack('i',ret)[0]
print(ret1, type(ret1))
# 但是通过struct 处理不能处理太大
方案一:
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
# 1. 通过struct制作报头
total_size = len(correct_msg) + len(error_msg)
header = struct.pack('i', total_size)
# 2. 发送报头过去
conn.send(header)
# 3. 发送真实的数据
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。
客户端
import socket
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
# 1. 接受报头
header = phone.recv(4)
# 2. 解析报头
total_size = struct.unpack('i',header)[0]
# 3. 根据报头信息接收真实数据
recv_size = 0
res = b''
while recv_size<total_size:
data = phone.recv(1024)
res += data
recv_size+=len(data)
print(res.decode('gbk'))
# from_server_data = phone.recv(1024)
#
# print(from_server_data.decode('gbk'))
phone.close()
方案二:可自定制报头
整个流程的大致解释:
我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
服务端
import socket
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1:
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
# 1 制作固定报头
total_size = len(correct_msg) + len(error_msg)
header_dict = {
'md5': 'fdsaf2143254f',
'file_name': 'f1.txt',
'total_size': total_size,
}
header_dict_json = json.dumps(header_dict) # str
bytes_headers = header_dict_json.encode('utf-8')
header_size = len(bytes_headers)
header = struct.pack('i', header_size)
# 2 发送报头长度
conn.send(header)
# 3 发送报头
conn.send(bytes_headers)
# 4 发送真实数据:
conn.send(correct_msg)
conn.send(error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
客户端
import socket
import struct
import json
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while 1:
cmd = input('>>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 1,接收固定报头
header_size = struct.unpack('i', phone.recv(4))[0]
# 2,解析报头长度
header_bytes = phone.recv(header_size)
header_dict = json.loads(header_bytes.decode('utf-8'))
# 3,收取报头
total_size = header_dict['total_size']
# 3,根据报头信息,接收真实数据
recv_size = 0
res = b''
while recv_size < total_size:
recv_data = phone.recv(1024)
res += recv_data
recv_size += len(recv_data)
print(res.decode('gbk'))
phone.close()
FTP上传下载文件的代码(简单版)
服务端
import socket
import struct
import json
sk = socket.socket()
# buffer = 4096 # 当双方的这个接收发送的大小比较大的时候,就像这个4096,就会丢数据,改小了就ok的,在linux上也是ok的。
buffer = 1024 #每次接收数据的大小
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()
#接收
head_len = conn.recv(4)
head_len = struct.unpack('i',head_len)[0] #解包
json_head = conn.recv(head_len).decode('utf-8') #反序列化
head = json.loads(json_head)
filesize = head['filesize']
with open(head['filename'],'wb') as f:
while filesize:
if filesize >= buffer: #>=是因为如果刚好等于的情况出现也是可以的。
content = conn.recv(buffer)
f.write(content)
filesize -= buffer
else:
content = conn.recv(buffer)
f.write(content)
break
conn.close()
sk.close()
客户端
import os
import json
import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
buffer = 1024 #读取文件的时候,每次读取的大小
head = {
'filepath':r'C:\Users\Aaron\Desktop\新建文件夹', #需要下载的文件路径,也就是文件所在的文件夹
'filename':'config', #改成上面filepath下的一个文件
'filesize':None,
}
file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize
# json_head = json.dumps(head,ensure_ascii=False) #字典转换成字符串
json_head = json.dumps(head) #字典转换成字符串
bytes_head = json_head.encode('utf-8') #字符串转换成bytes类型
print(json_head)
print(bytes_head)
#计算head的长度,因为接收端先接收我们自己定制的报头,对吧
head_len = len(bytes_head) #报头长度
pack_len = struct.pack('i',head_len)
print(head_len)
print(pack_len)
sk.send(pack_len) #先发送报头长度
sk.send(bytes_head) #再发送bytes类型的报头
#即便是视频文件,也是可以按行来读取的,也可以readline,也可以for循环,但是读取出来的数据大小就不固定了,影响效率,有可能读的比较小,也可能很大,像视频文件一般都是一行的二进制字节流。
#所有我们可以用read,设定一个一次读取内容的大小,一边读一边发,一边收一边写
with open(file_path,'rb') as f:
while filesize:
if filesize >= buffer: #>=是因为如果刚好等于的情况出现也是可以的。
content = f.read(buffer) #每次读取出来的内容
sk.send(content)
filesize -= buffer #每次减去读取的大小
else: #那么说明剩余的不够一次读取的大小了,那么只要把剩下的读取出来发送过去就行了
content = f.read(filesize)
sk.send(content)
break
sk.close()