socket套接字
socket套接字
简介
Socket并不属于TCP/IP协议簇,它只是一个编程接口,即对TCP/IP的封装和应用,简单理解TCP/IP看看作一
个函数,而Socket用来进行调用,Socket可在网络中对两个程序建立通信通道,Socket可分为两个基本模块,一个
服务端一个客户端,链接后进行通信。
网络编程
常见的套接字对象方法和属性
![](https://s1.ax1x.com/2022/04/15/LGPjsK.png)
创建TCP服务端
import socket
server = socket.socket() # 创建一个socket对象(server)
server.bind(('192.168.1.8', 8080)) # 给socket对象(server)绑定ip和端口号(以元组的形式)
server.listen(5) # 设置半连接池,最低为0
sock, addr = server.accept() # 接收客户端连接,获得sock连接 和 addr客户端地址。
data = sock.recv(1024) # 获取客户端发送的消息
print(data.decode('utf8')) # 打印接收到的数据,由于传输的时候是二进制所以需要进行解码
sock.send('你好'.encode('utf8')) # 服务端发送消息给客户端
sock.close() # 断开服务端到客户端的连接
server.close() # 关闭服务端
创建一个TCP客户端
import socket
client = socket.socket() # 创建一个socket对象(client)
client.connect(('192.168.1.8', 8080)) # 根据ip和端口连接服务端(元组的形式),主动发起连接
msg = input('要发送的消息: ').strip() # 获取要发送给服务端的消息
client.send(msg.encode('utf8')) # 将消息先编码后在发送给服务端
data = client.recv(1024) # 获取服务端发送的消息
print(data.decode('utf8')) # 打印接收到的数据,由于传输的时候是二进制所以需要进行解码
client.close() # 断开连接,关闭客户端
在重启服务器的时候可能会遇到的BUG(mac居多),windows频率较少
![](https://s1.ax1x.com/2022/04/15/LGAAQ1.png)
解决办法
import socket
from socket import SOL_SOCKET,SO_REUSEADDR(导入对应的包)
server = socket.socket() # 创建一个socket对象(server)
# -------------------------------------
# 加上它就可以防止重启报错了(注意位置)
# -------------------------------------
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('192.168.1.8', 8080)) # 给socket对象(server)绑定ip和端口号(以元组的形式)
server.listen(5) # 设置半连接池,最低为0
sock, addr = server.accept() # 接收客户端连接,获得sock连接 和 addr客户端地址。
data = sock.recv(1024) # 获取客户端发送的消息
print(data.decode('utf8')) # 打印接收到的数据,由于传输的时候是二进制所以需要进行解码
sock.send('你好'.encode('utf8')) # 服务端发送消息给客户端
sock.close() # 断开服务端到客户端的连接
server.close() # 关闭服务端
通信循环
服务端
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
server = socket.socket() # 创建一个socket对象(server)
# -------------------------------------
# 加上他就可以防止重启报错了(注意位置)
# -------------------------------------
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('192.168.1.8', 8080)) # 给socket对象(server)绑定ip和端口号(以元组的形式)
server.listen(5) # 设置半连接池,最低为0
sock, addr = server.accept() # 接收客户端连接,获得sock连接 和 addr客户端地址。
while True:
data = sock.recv(1024) # 获取客户端发送的消息
print(data.decode('utf8')) # 打印接收到的数据,由于传输的时候是二进制所以需要进行解码
msg = input('要发送的消息: ').strip()
sock.send(msg.encode('utf8')) # 服务端发送消息给客户端
# sock.close() # 断开服务端到客户端的连接
# server.close() # 关闭服务端
客户端
import socket
client = socket.socket() # 创建一个socket对象(client)
client.connect(('192.168.1.8', 8080)) # 根据ip和端口连接服务端(元组的形式),主动发起连接
while True:
msg = input('要发送的消息: ').strip() # 获取要发送给服务端的消息
if len(msg) == 0: continue # 避免消息为空时,造成双方等待
client.send(msg.encode('utf8')) # 将消息先编码后在发送给服务端
data = client.recv(1024) # 获取服务端发送的消息
print(data.decode('utf8')) # 打印接收到的数据,由于传输的时候是二进制所以需要进行解码
client.close() # 断开连接,关闭客户端
链接循环
"""
如果是windows 客户端异常退出之后服务端会直接报错
处理方式
异常处理
如果是mac或linux 服务端会接收到一个空消息
处理方式
len判断
"""
linux、mac断开链接时不会报错,会一直返回空(b‘’)
客户端如果异常断开 服务端代码应该重新回到accept等待新的客人
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
server = socket.socket() # 创建一个socket对象(server)
# -------------------------------------
# 加上他就可以防止重启报错了(注意位置)
# -------------------------------------
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('192.168.1.8', 8080)) # 给socket对象(server)绑定ip和端口号(以元组的形式)
server.listen(5) # 设置半连接池,最低为0
while True:
sock, addr = server.accept() # 接收客户端连接,获得sock连接 和 addr客户端地址。
while True:
try:
data = sock.recv(1024) # 获取客户端发送的消息
print(data.decode('utf8')) # 打印接收到的数据,由于传输的时候是二进制所以需要进行解码
msg = input('要发送的消息: ').strip()
sock.send(msg.encode('utf8')) # 服务端发送消息给客户端
except BaseException:
break # 客户端如果异常断开 服务端代码应该重新回到accept等待新的客人
# sock.close() # 断开服务端到客户端的连接
# server.close() # 关闭服务端
半连接池,允许等待的最大个数
![](https://s1.ax1x.com/2022/04/15/LGmEV0.png)
1.什么是半连接池:当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接
2.半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光
3.产生半连接的两种情况:
客户端无法返回ACK信息
服务器来不及处理客户端的连接请求
设置的最大等待人数 >>>: 节省资源 提高效率
server.listen(5)指定5个等待席位
黏包问题
多次发送被并为一次
![](https://s1.ax1x.com/2022/04/15/LGmfMj.png)
# TCP协议的特点
会将数据量比较小并且时间间隔比较短的数据整合到一起发送
并且还会受制于recv括号内的数字大小(核心问题!!!)
流式协议:跟水流一样不间断
黏包现象只发生在tcp协议中
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
粘包是接收长度没对上导致的
控制recv接收的字节数与之对应(你发多少字节我收多少字节)
在很多情况下并不知道数据的长度,服务端不能写死
"""
问题产生的原因其实是因为recv括号内我们不知道即将要接收的数据到底多大
如果每次接收的数据我们都能够精确的知道它的大小 那么肯定不会出现黏包
"""
思路一如果在不知道数据有多长的情况下就会出现意外,那么我们可以先传一个固定长度的数据过去告诉他
真实数据有多长,就可以对应着收了
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
这里利用struct模块模块的struct.pack() struct.unpack() 方法来实现打包(将真实数据长度变
为固定长度的数字)解包(将该数字解压出打包前真实数据的长度)
pack unpack模式参数对照表(standard size 转换后的长度)
![](https://s1.ax1x.com/2022/04/15/LGnb7t.png)
i 模式的范围:-2147483648 <= number <= 2147483647
在传真实数据之前还想要传一些描述性信息
如果在传输数据之前还想要传一些描述性信息,那么就得在中间再加一步了(传个电影,我告诉你电影名,
大小,大致情节,演员等信息,你再选择要不要),前面的方法就不适用了
粘包问题解决思路
服务器端
先制作一个发送给客户端的字典
制作字典的报头
发送字典的报头
发送字典
再发真实数据
客户端
先接收字典的报头
解析拿到字典的数据长度
接收字典
从字典中获取真实数据的长度
循环获取真实数据
ps:为什么要多加一个字典
pack打包的数据长度(的长度)有限,字典再打包会很小(长度值也会变很小)(120左右)
可以携带更多的描述信息
模板
服务端
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
import os
import json
import struct
server = socket.socket() # 创建一个socket对象(server)
# -------------------------------------
# 加上他就可以防止重启报错了(注意位置)
# -------------------------------------
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 9000)) # 给socket对象(server)绑定ip和端口号(以元组的形式)
server.listen(5) # 设置半连接池,最低为0
sock, addr = server.accept() # 接收客户端连接,获得sock连接 和 addr客户端地址。
# 1.先制作一个字典(可以放入一些描述性的信息)
data_dict = {
'file_name': '11.jpg',
'file_title': '壁纸',
'file_size': os.path.getsize(r'D:\pythonproject\test\11.jpg') # 可以改为自己的文件地址
}
# 2.制作字典报头
data_dict_json_str = json.dumps(data_dict) # 转化成字符串
data_dict_json_str_bytes = data_dict_json_str.encode('utf8') # 将字符串编码
data_dict_package = struct.pack('i', len(data_dict_json_str_bytes)) # 以i模式打包,打包成4个字节
# 3.发送报头
sock.send(data_dict_package) # 服务端将字典发送给客户端
# 4.发送字典
sock.send(data_dict_json_str_bytes)
# 5.发送真实数据
with open(r'D:\pythonproject\test\11.jpg', 'rb') as f:
for i in f:
sock.send(i)
print('发送完成')
客户端
import socket
import struct
import json
client = socket.socket() # 创建一个socket对象(client)
client.connect(('127.0.0.1', 9000)) # 根据ip和端口连接服务端(元组的形式),主动发起连接
# 1.先接收字典的报头
data_dict_package = client.recv(4)
# 2.解析拿到字典的数据长度
data_dict_len = struct.unpack('i', data_dict_package)[0]
# 3.接收字典数据
data_dict = client.recv(data_dict_len)
data_dict = data_dict.decode('utf8')
data = json.loads(data_dict)
# 4.循环接收文件数据 不要一次性接收
recv_size = 0
with open('12.jpg', 'wb') as f:
while recv_size < data.get('file_size'):
write_data = client.recv(1024)
recv_size += len(write_data)
f.write(write_data)
今日作业
项目下载
可以实现上传文件(上传到项目的shipin文件中,可以更改)和下载文件(默认下载在桌面,下载时会从项目的shipin文件中进行下载)
编写一个cs架构的软件
就两个功能
一个视频下载:从服务端下载视频
一个视频上传:从客户端上传视频
服务端
import socket
import json
import os
import sys
import struct
from socket import SOL_SOCKET, SO_REUSEADDR
sys.path.append(os.path.dirname(__file__))
shipin_path = os.path.join(os.path.dirname(__file__), 'shipin')
if not os.path.exists(shipin_path):
os.makedirs(shipin_path)
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8001))
server.listen(5)
while True:
sock, addr = server.accept()
while True:
try:
print('连接成功')
msg = sock.recv(1024)
msg = msg.decode('utf8')
msg1, name = msg.split(' ')
if msg1 == '下载':
if name not in os.listdir(shipin_path): sock.send('文件不存在'.encode('utf8'))
data_dict = {
'file_name': name,
'file_size': os.path.getsize(os.path.join(shipin_path, name))
}
data_dict_json = json.dumps(data_dict)
data_dict_json_bytes = data_dict_json.encode('utf8')
data_dict_json_bytes_package = struct.pack('i', len(data_dict_json_bytes))
sock.send(data_dict_json_bytes_package)
sock.send(data_dict_json_bytes)
with open(os.path.join(shipin_path, name), 'rb') as f:
for i in f:
sock.send(i)
print('发送完成')
else:
print('上传')
data_dict_package = sock.recv(4)
data_dict_len = struct.unpack('i', data_dict_package)[0]
data_dict = sock.recv(data_dict_len)
data = json.loads(data_dict)
recv_size = 0
with open(os.path.join(shipin_path, data.get('file_name')), 'wb') as f:
while recv_size < data.get('file_size'):
write_data = sock.recv(1024)
recv_size += len(write_data)
f.write(write_data)
print('上传完成')
except ConnectionRefusedError:
print('操作失败')
客户端
import socket
import json
import os
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8001))
while True:
msg = input('''格式:上传/下载 文件名(下载 文件名 指定地址(默认桌面))
(上传 地址(绝对地址)))''').strip()
if msg.split(' ')[0] == '下载':
client.send(msg.encode('utf8'))
data_dict_package = client.recv(4)
data_dict_len = struct.unpack('i', data_dict_package)[0]
data_dict = client.recv(data_dict_len)
data = json.loads(data_dict)
recv_size = 0
if msg.split(' ') == 3:
addr = msg.split(' ')[-1]
else:
addr = os.path.join(os.path.expanduser('~'),"Desktop") # 动态获取当前电脑桌面路径
print(addr)
with open(os.path.join(addr,data.get('file_name')), 'wb') as f:
while recv_size < data.get('file_size'):
write_data = client.recv(1024)
recv_size += len(write_data)
f.write(write_data)
print('下载完成')
elif msg.split(' ')[0] == '上传':
client.send(msg.encode('utf8'))
path = msg.split(' ')[1]
name = path.split(os.sep)[-1]
print(path,name,sep=' ')
data_dict = {
'file_name': name,
'file_size': os.path.getsize(path)
}
data_dict_json = json.dumps(data_dict)
data_dict_json_bytes = data_dict_json.encode('utf8')
data_dict_json_bytes_package = struct.pack('i', len(data_dict_json_bytes))
client.send(data_dict_json_bytes_package)
client.send(data_dict_json_bytes)
with open(path, 'rb') as f:
for i in f:
client.send(i)
print('上传成功')