软件开发架构
软件开发架构有很多,但是涉及到两个程序之间通讯的应用大致可以分为两种,一种是应用类比如: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 socketserver = 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 socketclient = 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_REUSEADDRserver = socket.socket()server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加
原因:操作系统没有来的及释放该端口
基于UDP协议的Socket
UDP是无连接的,启动服务后可以直接接受消息,不需要提前建立连接
服务端
import socketserver = 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 socketip_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 socketfrom socket import SOL_SOCKET, SO_REUSEADDRserver = 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("客户端断开连接")breakprint("来自服务端的消息:", data.decode('utf8'))while True:msg = input("请回复消息>>>").strip()if len(msg) == 0: # msg为0如果是continue会跳转到recv,使两边进入'冷战',可以替代消息给他发出去,或者再嵌套一层whileprint("消息不能为空")continuebreaksock.send(msg.encode('utf8'))except Exception:print("客户端断开连接")break
客户端
import socketclient = socket.socket()client.connect(('127.0.0.1', 8080))while True:msg = input("请输入你想要发送的消息>>>").strip()if len(msg) == 0:print("消息不能为空")continueclient.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 structimport socketimport osimport jsonfrom socket import SOL_SOCKET, SO_REUSEADDRserver = 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 jsonimport socketimport structclient = 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)
