网络基础 => socket
socket 是应用层与TCP/IP协议族通信的中间软件抽象层 , 它是一组接口. 在设计模式中 , socket 其实就是一个门面模式 , 把复杂的TCP/IP协议族隐藏在socket接口后面 , 对用户来说 , 一组简单的接口就是全部 , 让socket去组织数据 , 以符合指定的协议
基于 socket 开发一个 C/S 或者 B/S 结构的软件
Client---------------网络---------------Server
Browser-----------网络---------------Server
网络 : 底层的物理连接介质 + 互联网通信协议
网络存在的意义 : 通信
1. 应用层 / 1. 应用层 / 1. 应用层
2. 表示层
3. 会话层
4. 传输层 / 2. 传输层 / 2. 传输层
5. 网络层 / 3. 网络层 / 3. 网络层
6. 数据链路层 / 4. 数据链路层 / 4. 网络接口层
7. 物理层 / 5. 物理层
socket 意义: 网络通信过程中 , 信息拼接的工具 (中文:套接字)
基于文件类型的套接字家族: AF_UNIX
基于网络类型的套接字家族: AF_INET
开发中 , 一个端口只对一个程序生效 , 在测试时 , 允许端口重复捆绑 (开发时删掉)
在 bind 方法之前加下面的代码 , 可以让一个端口重复使用
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
TCP (Transmission Control Protocol) 一种面向连接的 可靠的 传输层通信协议 (流式协议)
优点: 可靠 稳定 传输完整稳定 不限制数据大小
缺点: 慢 效率低 占用系统资源高 一发一收都需要对方确认
应用: Web 浏览器 电子邮件 文件传输 大量数据传输的场景
套接字工作流程
1. 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
2. 在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
3. 客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
import socket
# 1. 创建一个 socket 对象
sk = socket.socket()
# 2. 绑定对应的 ip 和端口号 (让其他主机在网络中可以找得到)
# 127.0.0.1 代表本地 ip
sk.bind(("127.0.0.1",9000))
# 3. 开启监听
sk.listen()
# 4. 建立三次握手
conn,addr = sk.accept()
# 5. 处理收发数据的逻辑 recv 接收 send 发送
res = conn.recv(1024) # 一次最多接收 1024 字节
print(res.decode("utf-8"))
# 6. 四次挥手
conn.close()
# 7. 退还端口
sk.close()
import socket
# 1. 创建一个 socket 对象
sk = socket.socket()
# 2. 与服务器建立连接
sk.connect(("127.0.0.1",9000))
# 3. 发送数据 (只能发送二进制的字节流)
"""send(字节流)"""
sk.send("北京昨天暴雨,如今不仅得有车,还得有船!".encode("utf-8"))
# 4. 关闭连接
sk.close()
import socket
# 1. 创建 socket 对象
sk = socket.socket()
# 2. 绑定 ip 和端口号 (在网络中注册该主机做服务器)
sk.bind(("127.0.0.1",8080))
# 3. 开启监听
sk.listen()
# 4. 三次握手
conn,addr = sk.accept()
# 5. 处理收发数据的逻辑 recv 接收 send 发送
"""send(字节流)"""
"""
conn.send("我去北京先买船".encode("utf-8"))
"""
while True:
# 4. 三次握手
conn,addr = sk.accept()
while True:
res = conn.recv(1024)
print(res.decode())
strvar = input("请输入服务端要给客户端发送的内容")
conn.send(strvar.encode())
if strvar.upper() == "Q":
break
# 6. 四次挥手
conn.close()
# 7. 退还端口
sk.close()
import socket
# 1. 创建 socket 对象
sk = socket.socket()
# 2. 连接服务端
sk.connect(("127.0.0.1",8080))
# 3. 收发数据
"""
res = sk.recv(1024) # 一次最多接收1024个字节
print(res.decode())
"""
while True:
strvar = input("请输入要发送的内容: ")
sk.send(strvar.encode())
res = sk.recv(1024)
if res == b'q' or res == b'Q':
break
print(res.decode())
# 4. 关闭连接
sk.close()
UDP (User Datagram Protocol) 一种无连接的 , 不可靠的传输层通信协议
优点: 速度快 , 可以多人同时聊天 , 耗费资源少 , 不需要建立连接
缺点: 不稳定 , 不能保证每次数据都能接收到
应用: IP电话 , 实时视频会议 , 聊天软件 , 少量数据传输的场景
import socket
# 1. 创建 udp 对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2. 绑定地址和端口号
sk.bind(("127.0.0.1",9000))
# 3. udp 服务器,在一开始只能接收数据
msg,cli_addr = sk.recvfrom(1024)
print(msg.decode())
print(cli_addr)
# 服务端给客户端发送数据
msg = "我是你老娘,赶紧回家吃饭"
sk.sendto(msg.encode(),cli_addr)
# 4. 关闭连接
sk.close()
import socket
# 1. 创建 udp 对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2. 收发数据的逻辑
# 发送数据
msg = "你好,你是MM还是GG"
# sendto( 消息,(ip,端口号) )
sk.sendto(msg.encode("utf-8"),("127.0.0.1",9000))
# 接收数据
msg,server_addr = sk.recvfrom(1024)
print(msg.decode())
print(server_addr)
# 3. 关闭连接
sk.close()
import socket
# 1. 创建 udp 对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2. 绑定地址和端口号
sk.bind(("127.0.0.1",9000))
# 3. udp 服务器,在一开始只能接收数据
while True:
# 接收消息
msg,cli_addr = sk.recvfrom(1024)
print(msg.decode())
message = input("服务端给客户端发送的消息是: ")
# 发送数据
sk.sendto(message.encode("utf-8") , cli_addr)
# 4. 关闭连接
sk.close()
import socket
# 1. 创建 udp 对象
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2. 收发数据的逻辑
while True:
# 发送数据
message = input("客户端给服务端发送的消息是: ")
sk.sendto(message.encode() , ("127.0.0.1",9000))
# 接收数据
msg,addr = sk.recvfrom(1024)
print(msg.decode("utf-8"))
# 3. 关闭连接
sk.close()
1. 数据黏包是因为在客户端/服务端都会有一个数据缓冲区 , 缓冲区用来临时保存数据
为了保证能够完整的接收到数据 , 因此缓冲区都会设置的比较大
2. 在收发数据频繁是 , 由于tcp 传输消息的无边界 , 不清楚应该截取多少长度
导致客户端/服务端 , 都有肯把多条数据当成是一条数据进行截取 , 造成黏包
现象一: 在发送端 , 由于两个数据短 , 发送的时间间隔较短 , 所以在发送端形成黏包
现象二: 在接收端 , 由于两个数据几乎同时被发送到对方的缓存中 , 所以在接收端形成黏包
**总结: **
发送端 包之间时间间隔短 或者 接收端 接收不及时 , 就会黏包
核心是因为 tcp 对数据无边界截取 , 不会按照发送的顺序判断
tcp 协议
缺点: 接收时数据之间无边界 , 有可能黏合几条数据成一条数据 , 造成黏包
优点: 不限制数据包的大小 , 稳定传输不丢包
udp 协议
优点: 接收时数据之间有边界 , 传输速度快 , 不黏包
缺点: 限制数据包的大小 (受带宽路由器等因素影响) , 传输不稳定 , 可能丢包
tcp和udp对于数据包来说都可以进行拆包和解包 , 理论上来讲 , 无论多大都能分次发送
但tcp一旦发送失败 , 对方无响应(对方无回执) , tcp可以选择再发 , 直到对应响应完毕为止
而udp一旦发送失败 , 是不会询问对方是否有响应的 , 如果数据量过大 , 易丢包
自定义应用层协议
解决黏包场景
应用场景在实时通讯时 , 需要阅读此次发的消息是什么
不需要解决黏包场景
下载或者上传文件的时候 , 最后要把包都结合在一起 , 黏包无所谓
网络协议的最底层就是 socket , 基于原有 socket 模块 , 又封装了一层 , 就是 socketserver
socketserver 为了实现 tcp 协议 , server端的并发
import socket
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(("127.0.0.1",9000))
sk.listen()
conn,addr = sk.accept()
# 处理收发数据的逻辑
# 先发送接下来要发送数据的大小
conn.send("5".encode())
# 发完长度之后,再发数据
conn.send("hello".encode())
conn.send(",world".encode())
conn.close()
sk.close()
import socket
sk = socket.socket()
sk.connect(("127.0.0.1",9000))
# 处理收发数据的逻辑
# 先接收接下来要发送数据的大小
res = sk.recv(1)
num = int(res.decode())
# 接收 num 这么多个字节数
res1 = sk.recv(num)
res2 = sk.recv(1024)
print(res1)
print(res2)
sk.close()
import socket
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(("127.0.0.1",9000))
sk.listen()
conn,addr = sk.accept()
# 处理收发数据的逻辑
# 先发送接下来要发送数据的大小
conn.send("00000100".encode())
# 发完长度之后,再发数据
msg = "hello" * 20
conn.send(msg.encode())
conn.send(",world".encode())
conn.close()
sk.close()
import socket
sk = socket.socket()
sk.connect(("127.0.0.1",9000))
# 处理收发数据的逻辑
# 先接收接下来要发送数据的大小
res = sk.recv(8)
num = int(res.decode())
# 接收 num 这么多个字节数
res1 = sk.recv(num)
res2 = sk.recv(1024)
print(res1)
print(res2)
sk.close()
pack: 把任意长度数字转化成具有固定4个字节长度的字节流
unpack: 把4个字节值恢复成原来的数字 , 返回的最终是元组
import struct
# pack
# i => int 要转化的当前数据是整型
res = struct.pack("i",999999999)
res = struct.pack("i",1)
res = struct.pack("i",4399999)
# pack 的范围 -2147483648 ~ 2147483647 21个亿左右
res = struct.pack("i",2100000000)
print(res , len(res))
# unpack
# i => 把对应的数据转换成 int 整型
tup = struct.unpack("i",res)
print(tup) # (2100000000,)
print(tup[0])
import socket
import struct
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(("127.0.0.1",9000))
sk.listen()
conn,addr = sk.accept()
# 处理收发数据的逻辑
strvar = input("请输入你要发送的数据")
msg = strvar.encode()
length = len(msg)
res = struct.pack("i",length)
# 第一次发送的是字节长度
conn.send(res)
# 第二次发送真实的数据
conn.send(msg)
# 第三次发送真实的数据
conn.send("世界真美好".encode())
conn.close()
sk.close()
import socket
import struct
sk = socket.socket()
sk.connect(("127.0.0.1",9000))
# 处理收发数据的逻辑
# 第一次接收的是字节长度
n = sk.recv(4)
tup = struct.unpack("i",n)
n = tup[0]
# 第二次接收真实的数据
res = sk.recv(n)
print(res.decode())
# 第三次接收真实的数据
res = sk.recv(1024)
print(res.decode())
sk.close()
服务端套接字函数
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() 创建一个与该套接字相关的文件
import socket
# 1. 买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2. 插手机卡
phone.bind(("127.0.0.1",8080)) # 本地回环
# 3. 开机
phone.listen(5)
print("starting %s:%s" % ("127.0.0.1",8080))
# 4. 等电话连接
conn,client_addr = phone.accept()
# 5. 收/发消息
data = conn.recv(1024) # 最大接收的字节个数
print("收到的客户端数据: ",data.decode('utf-8'))
conn.send(data.upper())
# 6. 关闭
conn.close()
phone.close()
import socket
# 1. 买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2. 拨电话
phone.connect(("127.0.0.1",8080))
# 3. 发收消息
phone.send("hello".encode("utf-8"))
data = phone.recv(1024)
print("服务端返回的数据: ",data.decode("utf-8"))
# 4. 关闭
phone.close()
# 加上循环
"""
服务端应该满足的特性:
1. 一直对外提供服务
2. 并发的提供服务
"""
import socket
# 1. 买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2. 插手机卡
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",8080)) # 本地回环
# 3. 开机
phone.listen(5)
print("starting %s:%s" % ("127.0.0.1",8080))
# 4. 等电话连接 => 连接循环
while True:
conn,client_addr = phone.accept()
print(client_addr)
# 5. 收/发消息 => 通信循环
while True:
try:
data = conn.recv(1024) # 最大接收的字节个数 1024
if len(data) == 0: # 针对linux系统
break
print("收到的客户端数据: ",data.decode("utf-8"))
conn.send(data.upper())
except Exception: # 针对 windows 系统
break
# 6. 关闭
conn.close()
phone.close()
import socket
# 1. 买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2. 拨电话
phone.connect(("127.0.0.1",8080))
# 3. 发/收消息 => 通信循环
while True:
msg = input(">>: ").strip()
phone.send(msg.encode("utf-8"))
data = phone.recv(1024)
print("服务端返回的数据: ",data.decode("utf-8"))
# 4. 关闭
phone.close()
# 远程执行命令
import socket
import subprocess
# 1. 买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 2. 插手机卡
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",8080))
# 3. 开机
phone.listen(5)
print("starting %s:%s" % ("127.0.0.1",8080))
# 4. 等电话连接 => 连接循环
while True:
conn,client_addr = phone.accept()
print(client_addr)
# 5. 收/发数据 => 通信循环
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell = True,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
res = obj.stdout.read() + obj.stderr.read()
print(res)
conn.send(res)
except Exception:
break
# 6. 关闭
conn.close()
phone.close()
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",8080))
while True:
cmd = input("[root@localhost]# ").strip()
phone.send(cmd.encode('utf-8'))
data = phone.recv(1024)
print(data.decode('gbk'))
phone.close()
import subprocess
import struct
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,client_addr = server.accept()
print(conn)
print(client_addr)
while True:
try:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
total_size = len(stdout) + len(stderr)
# 先发送数据的长度
conn.send(struct.pack('i',total_size))
# 再发送真正的数据
conn.send(stdout)
conn.send(stderr)
except Exception:
break
conn.close()
server.close()
import struct
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
cmd = input(">>: ").strip()
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8'))
# 先收数据的长度
n = 0
header = b''
while n < 4:
data = client.recv(1)
header += data
n += len(data)
total_size = struct.unpack('i',header)[0]
# 收真正的数据
recv_size = 0
res = b''
while recv_size < total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(res.decode('gbk'))
client.close()
# 定制复杂的报头
# 版本一
import subprocess
import os
import struct
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5)
while True:
conn,client_addr = server.accept()
print(conn)
print(client_addr)
while True:
try:
msg = conn.recv(1024).decode('utf-8')
cmd,file_path = msg.split()
if cmd == "get":
# 先发送报头
total_size = os.path.getsize(file_path)
conn.send(struct.pack('q',total_size))
# 再发送文件
with open(r'%s' %file_path,mode='rb') as f:
for line in f:
conn.send(line)
except Exception:
break
conn.close()
server.close()
import struct
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8080))
while True:
cmd = input('>>: ').strip() # get 文件路径
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8'))
# 先收数据的长度
n = 0
header = b''
while n < 8:
data = client.recv(1)
header += data
n += len(data)
total_size = struct.unpack('q',header)[0]
print(total_size)
# 收真正的数据
recv_size = 0
with open('aaa.jpg',mode='wb') as f:
while recv_size < total_size:
data = client.recv(1024)
f.write(data)
recv_size += len(data)
client.close()
# 定制复杂的报头
# 版本二
import os
import json
import struct
import subprocess
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,client_addr = server.accept()
print(conn)
print(client_addr)
while True:
try:
msg = conn.recv(1024).decode('utf-8')
cmd,file_path = msg.split()
if cmd == 'get':
# 一. 制作报头
header_dic = {
"total_size":os.path.getsize(file_path)
"file_name":os.path.basename(file_path)
"md5":"4681351846813151879441351"
}
header_json = json.dumps(header_dic)
header_json_bytes = header_json.encode('utf-8')
# 二. 发送数据
# 1. 先发送报头的长度
header_size = len(header_json_bytes)
conn.send(struct.pack('i',header_size))
# 2. 再发送报头
conn.send(header_json_bytes)
# 3. 最后发送真实的数据
with open(r'%s' %file_path,mode='rb') as f:
for line in f:
conn.send(line)
except Exception:
break
conn.close()
server.close()
import json
import struct
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8080))
while True:
cmd = input(">>: ").strip()
if len(cmd) == 0:
continue
client.send(cmd.encode('utf-8'))
# 1. 先接收报头的长度
res = client.recv(4)
header_size = struct.unpack('i',res)[0]
# 2. 再接收报头
header_json_bytes = client.recv(header_size)
header_json = header_json_bytes.decode('utf-8')
header_dic = json.loads(header_json)
print(header_dic)
# 3. 最后接收真实的数据
total_size = header_dic['total_size']
file_name = header_dic['file_name']
recv_size = 0
with open(r'%s' %file_name,mode='wb') as f:
while recv_size < total_size:
data = client.recv(1024)
f.write(data)
recv_size += len(data)
client.close()