软件开发架构
软件开发架构有很多,但是涉及到两个程序之间通讯的应用大致可以分为两种,一种是应用类比如:QQ、微信,需要安装的桌面应用,另一种是WEB类比如百度、知乎、等使用浏览器访问就可以直接使用的应用
这些应用的本质其实是两个程序之间的通讯,而这两个分类又对应了两个软件开发的架构
C/S架构
C/S
即Client
与Server
客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的
这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。
B/S架构
B/S
即Browser
与Server
浏览器端与服务器端架构,这种架构是从用户层面来划分的。
Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查
socket套接字
概念
socket
简称套接字,是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多数是基于socket
来完成通信的。
socket
是基于C/S
架构的,也就是说socket
网络编程,通常需要写两个文件,一个服务端,一个客户端。
发展史
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
Unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
socket模块
官方文档:https://docs.python.org/zh-cn/3/library/socket.html
语法
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
参数详解
family
:地址系列应为AF_INET
(默认值),AF_INET6
,AF_UNIX
,AF_CAN
或AF_RDS
。(AF_UNIX
域实际上是使用本地socket
文件来通信)type
:套接字类型应为SOCK_STREAM
(默认值),SOCK_DGRAM
,SOCK_RAW
或其他SOCK_
常量之一。SOCK_STREAM 是基于TCP
的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET
,多用于资料传送。SOCK_DGRAM 是基于UDP
的,无保障的面向消息的socket
,多用于在网络上发广播信息。fileno
:如果指定了fileno
,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。与socket.fromfd()
不同,fileno
将返回相同的套接字,而不是重复的。这可能有助于使用socket.close()
关闭一个独立的插座。protocol
:一般不填默认为0.
Socket 对象(内建)方法
函数 | 描述 |
---|---|
服务器端套接字 | |
s.bind() | 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。 |
s.listen() | 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。 |
s.accept() | 被动接受TCP客户端连接,(阻塞式)等待连接的到来 |
客户端套接字 | |
s.connect() | 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数 | |
s.recv() | 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。 |
s.send() | 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。 |
s.sendall() | 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 |
s.recvfrom() | 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 |
s.sendto() | 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。 |
s.close() | 关闭套接字 |
s.getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 |
s.getsockname() | 返回套接字自己的地址。通常是一个元组(ipaddr,port) |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值。 |
s.getsockopt(level,optname[.buflen]) | 返回套接字选项的值。 |
s.settimeout(timeout) | 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect()) |
s.gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 如果 flag 为 False,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用 recv() 没有发现任何数据,或 send() 调用无法立即发送数据,那么将引起 socket.error 异常。 |
s.makefile() | 创建一个与该套接字相关连的文件 |
基于TCP协议的Socket
TCP是基于连接的,必须先启动服务端,然后再启动客户端去链接服务端
服务端
import socket
server = socket.socket() # 买手机
# 查看socket源码可以看出,括号内不写参数,就是基于网络的遵循TCP协议的套接字
server.bind(('127.0.0.1', 8080)) # 插电话卡
# 127.0.0.1是计算机的本地回环地址 只有当前计算机本身可以访问,8080是端口
server.listen(5) # 最大连接数,超过后排队
# 半连接池的大小
sock, addr = server.accept() # 等待并,接听电话
# 查看accept源码可以看出,最终返回两个值
# listen和accept对应TCP三次握手服务端的两个状态
print(addr) # 客户端地址
data = sock.recv(1024) # 听别人说话
print(data.decode('utf8'))
sock.send('我来自是服务端'.encode('utf8')) # 回复别人的话
# recv和send接收和发送的都是bytes类型的数据
sock.close() # 挂电话
server.close() # 关机
客户端
import socket
client = socket.socket() # 买手机
# 产生一个socket对象
client.connect(('127.0.0.1', 8080)) # 拨号
# 根据服务端的地址连接
client.send('我来自是客户端'.encode('utf8')) # 给服务端发送消息
data = client.recv(1024) # 接收服务端回复的消息
print(data.decode('utf8'))
client.close() # 关闭客户端
补充:服务端与客户端首次交互一边是recv
那么另一边必须是send
两边不能相同,否则就’冷战’了
重启可能遇到以下报错
解决办法
from socket import SOL_SOCKET, SO_REUSEADDR
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加
原因:操作系统没有来的及释放该端口
基于UDP协议的Socket
UDP是无连接的,启动服务后可以直接接受消息,不需要提前建立连接
服务端
import socket
server = socket.socket(type=socket.SOCK_DGRAM) # 创建一个服务器的套接字
server.bind(('127.0.0.1', 8080)) # 绑定服务器套接字
msg, addr = server.recvfrom(1024)
print(msg.decode('utf8'))
server.sendto('我来自服务端'.encode('utf8'), addr) # 对话(接收与发送)
server.close() # 关闭服务器套接字
客户端
import socket
ip_port = ('127.0.0.1', 8080)
client = socket.socket(type=socket.SOCK_DGRAM)
client.sendto('我来自是客户端'.encode('utf8'), ip_port)
msg, addr = client.recvfrom(1024)
print(msg.decode('utf8'))
client.close()
Socket长连接
服务端
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True: # 客户端如果异常断开,服务端代码应该重新回到accept等待新的客户端加入
print("等待客户端连接中")
sock, addr = server.accept()
print("客户端连接成功", addr)
while True:
try: # 兼容windows,客户端异常退出之后服务端会直接报错,用异常处理
print("等待客户端的消息")
data = sock.recv(1024)
if len(data) == 0: # 客户端异常退出,mac或linux 服务端会接收到一个空消息,同过len来判断
print("客户端断开连接")
break
print("来自服务端的消息:", data.decode('utf8'))
while True:
msg = input("请回复消息>>>").strip()
if len(msg) == 0: # msg为0如果是continue会跳转到recv,使两边进入'冷战',可以替代消息给他发出去,或者再嵌套一层while
print("消息不能为空")
continue
break
sock.send(msg.encode('utf8'))
except Exception:
print("客户端断开连接")
break
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
msg = input("请输入你想要发送的消息>>>").strip()
if len(msg) == 0:
print("消息不能为空")
continue
client.send(msg.encode('utf8'))
print("等待服务端的消息")
data = client.recv(1024)
print("来自服务端的消息:", data.decode('utf8'))
半连接池
当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接
半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光
产生半连接的两种情况:
- 客户端无法返回ACK信息
- 服务器来不及处理客户端的连接请求
黏包
黏包问题
只有TCP有黏包问题,UDP永远不会黏包
TCP黏包是指发送方发送的若干包数据到接收方接收时成一包,造成数据在接收方缓冲区的堆积。
两种情况下会发生黏包
- 发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据了很小,会合到一起,产生黏包)
- 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生黏包)
拆包的发生情况
当发送端缓冲区的长度大于网卡的MTU
时,TCP
会将这次发送的数据拆成几个数据包发送出去。
补充问题一:为何**TCP**
是可靠传输,**UDP**
是不可靠传输
TCP
在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1
,发送端则清理缓存中的数据,对端返回ack=0
,则重新发送数据,所以TCP
是可靠的,而UDP
发送数据,对端是不会返回确认信息的,因此不可靠
补充问题二:**send**
(字节流)和**recv**
(1024)及**sendall**
recv
里指定的1024意思是从缓存里一次拿出1024个字节的数据
send
的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall
就会循环调用send
,数据不会丢失
黏包问题出现的原因
TCP
是流式协议,数据像水流一样黏在一起,没有任何边界区分- 收数据没收干净,有残留,就会下一次结果混淆在一起
解决黏包问题
核心问题是不知道即将要接收的数据多大,如果能够精准的知道数据量多大,那么黏包问题就可以解决了
服务端
import struct
import socket
import os
import json
from socket import SOL_SOCKET, SO_REUSEADDR
server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加
server.bind(('127.0.0.1', 8888))
server.listen(5)
client, addr = server.accept()
# 将关键信息打包成字典,文件名,描述,和文件大小
data_dict = {
'file_name': 'music.mp3',
'file_desc': '这是一个音频文件',
'file_size': os.path.getsize(r'/root/music.mp3') #
}
# 将字典变成json数据
dict_json_str = json.dumps(data_dict)
# 将json数据转换字节类型
dict_byte = dict_json_str.encode('utf8')
# 产生的字节串对象的大小
dict_package_header = struct.pack('i', len(dict_byte))
# 发送报头
client.send(dict_package_header)
# 发送字典
client.send(dict_byte)
# 发送真实数据
with open(r'/root/music.mp3', 'rb') as f:
for line in f:
client.send(line)
客户端
import json
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8888))
# 先接收固定长度的字典的报头
dict_header_len = client.recv(4)
# 解析出字典的真实长度
dict_real_len = struct.unpack('i', dict_header_len)[0]
# 根据真实长度来接收字典数据
dict_data_bytes = client.recv(dict_real_len)
# 将已编码的JSON字符串解码为字典
dict_data = json.loads(dict_data_bytes)
# 打印字典内容
print(dict_data)
recv_size = 0
# 循环接收文件数据,针对大文件的接收采用循环的形式一次接受一点点
with open(dict_data.get('file_name'), 'wb') as f:
while recv_size < dict_data.get('file_size'):
data = client.recv(1024)
recv_size += len(data)
f.write(data)