软件开发架构

软件开发架构有很多,但是涉及到两个程序之间通讯的应用大致可以分为两种,一种是应用类比如:QQ、微信,需要安装的桌面应用,另一种是WEB类比如百度、知乎、等使用浏览器访问就可以直接使用的应用

这些应用的本质其实是两个程序之间的通讯,而这两个分类又对应了两个软件开发的架构

C/S架构

C/SClientServer客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的

这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。

B/S架构

B/SBrowserServer浏览器端与服务器端架构,这种架构是从用户层面来划分的。

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

语法

  1. socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

参数详解

  • family:地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CANAF_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是基于连接的,必须先启动服务端,然后再启动客户端去链接服务端

服务端

  1. import socket
  2. server = socket.socket() # 买手机
  3. # 查看socket源码可以看出,括号内不写参数,就是基于网络的遵循TCP协议的套接字
  4. server.bind(('127.0.0.1', 8080)) # 插电话卡
  5. # 127.0.0.1是计算机的本地回环地址 只有当前计算机本身可以访问,8080是端口
  6. server.listen(5) # 最大连接数,超过后排队
  7. # 半连接池的大小
  8. sock, addr = server.accept() # 等待并,接听电话
  9. # 查看accept源码可以看出,最终返回两个值
  10. # listen和accept对应TCP三次握手服务端的两个状态
  11. print(addr) # 客户端地址
  12. data = sock.recv(1024) # 听别人说话
  13. print(data.decode('utf8'))
  14. sock.send('我来自是服务端'.encode('utf8')) # 回复别人的话
  15. # recv和send接收和发送的都是bytes类型的数据
  16. sock.close() # 挂电话
  17. server.close() # 关机

客户端

  1. import socket
  2. client = socket.socket() # 买手机
  3. # 产生一个socket对象
  4. client.connect(('127.0.0.1', 8080)) # 拨号
  5. # 根据服务端的地址连接
  6. client.send('我来自是客户端'.encode('utf8')) # 给服务端发送消息
  7. data = client.recv(1024) # 接收服务端回复的消息
  8. print(data.decode('utf8'))
  9. client.close() # 关闭客户端

补充:服务端与客户端首次交互一边是recv那么另一边必须是send两边不能相同,否则就’冷战’了

重启可能遇到以下报错

网络编程 - 图1

解决办法

  1. from socket import SOL_SOCKET, SO_REUSEADDR
  2. server = socket.socket()
  3. server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加

原因:操作系统没有来的及释放该端口

基于UDP协议的Socket

UDP是无连接的,启动服务后可以直接接受消息,不需要提前建立连接

服务端

  1. import socket
  2. server = socket.socket(type=socket.SOCK_DGRAM) # 创建一个服务器的套接字
  3. server.bind(('127.0.0.1', 8080)) # 绑定服务器套接字
  4. msg, addr = server.recvfrom(1024)
  5. print(msg.decode('utf8'))
  6. server.sendto('我来自服务端'.encode('utf8'), addr) # 对话(接收与发送)
  7. server.close() # 关闭服务器套接字

客户端

  1. import socket
  2. ip_port = ('127.0.0.1', 8080)
  3. client = socket.socket(type=socket.SOCK_DGRAM)
  4. client.sendto('我来自是客户端'.encode('utf8'), ip_port)
  5. msg, addr = client.recvfrom(1024)
  6. print(msg.decode('utf8'))
  7. client.close()

Socket长连接

服务端

  1. import socket
  2. from socket import SOL_SOCKET, SO_REUSEADDR
  3. server = socket.socket()
  4. server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加
  5. server.bind(('127.0.0.1', 8080))
  6. server.listen(5)
  7. while True: # 客户端如果异常断开,服务端代码应该重新回到accept等待新的客户端加入
  8. print("等待客户端连接中")
  9. sock, addr = server.accept()
  10. print("客户端连接成功", addr)
  11. while True:
  12. try: # 兼容windows,客户端异常退出之后服务端会直接报错,用异常处理
  13. print("等待客户端的消息")
  14. data = sock.recv(1024)
  15. if len(data) == 0: # 客户端异常退出,mac或linux 服务端会接收到一个空消息,同过len来判断
  16. print("客户端断开连接")
  17. break
  18. print("来自服务端的消息:", data.decode('utf8'))
  19. while True:
  20. msg = input("请回复消息>>>").strip()
  21. if len(msg) == 0: # msg为0如果是continue会跳转到recv,使两边进入'冷战',可以替代消息给他发出去,或者再嵌套一层while
  22. print("消息不能为空")
  23. continue
  24. break
  25. sock.send(msg.encode('utf8'))
  26. except Exception:
  27. print("客户端断开连接")
  28. break

客户端

  1. import socket
  2. client = socket.socket()
  3. client.connect(('127.0.0.1', 8080))
  4. while True:
  5. msg = input("请输入你想要发送的消息>>>").strip()
  6. if len(msg) == 0:
  7. print("消息不能为空")
  8. continue
  9. client.send(msg.encode('utf8'))
  10. print("等待服务端的消息")
  11. data = client.recv(1024)
  12. print("来自服务端的消息:", data.decode('utf8'))

半连接池

当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接

半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光

产生半连接的两种情况:

  • 客户端无法返回ACK信息
  • 服务器来不及处理客户端的连接请求

黏包

黏包问题

只有TCP有黏包问题,UDP永远不会黏包

TCP黏包是指发送方发送的若干包数据到接收方接收时成一包,造成数据在接收方缓冲区的堆积。

两种情况下会发生黏包

  1. 发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据了很小,会合到一起,产生黏包)
  2. 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生黏包)

拆包的发生情况

当发送端缓冲区的长度大于网卡的MTU时,TCP会将这次发送的数据拆成几个数据包发送出去。

补充问题一:为何**TCP**是可靠传输,**UDP**是不可靠传输

TCP在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以TCP是可靠的,而UDP发送数据,对端是不会返回确认信息的,因此不可靠

补充问题二:**send**(字节流)和**recv**(1024)及**sendall**

recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

黏包问题出现的原因

  1. TCP是流式协议,数据像水流一样黏在一起,没有任何边界区分
  2. 收数据没收干净,有残留,就会下一次结果混淆在一起

解决黏包问题

核心问题是不知道即将要接收的数据多大,如果能够精准的知道数据量多大,那么黏包问题就可以解决了

网络编程 - 图2

服务端

  1. import struct
  2. import socket
  3. import os
  4. import json
  5. from socket import SOL_SOCKET, SO_REUSEADDR
  6. server = socket.socket()
  7. server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 在bind前加
  8. server.bind(('127.0.0.1', 8888))
  9. server.listen(5)
  10. client, addr = server.accept()
  11. # 将关键信息打包成字典,文件名,描述,和文件大小
  12. data_dict = {
  13. 'file_name': 'music.mp3',
  14. 'file_desc': '这是一个音频文件',
  15. 'file_size': os.path.getsize(r'/root/music.mp3') #
  16. }
  17. # 将字典变成json数据
  18. dict_json_str = json.dumps(data_dict)
  19. # 将json数据转换字节类型
  20. dict_byte = dict_json_str.encode('utf8')
  21. # 产生的字节串对象的大小
  22. dict_package_header = struct.pack('i', len(dict_byte))
  23. # 发送报头
  24. client.send(dict_package_header)
  25. # 发送字典
  26. client.send(dict_byte)
  27. # 发送真实数据
  28. with open(r'/root/music.mp3', 'rb') as f:
  29. for line in f:
  30. client.send(line)

客户端

  1. import json
  2. import socket
  3. import struct
  4. client = socket.socket()
  5. client.connect(('127.0.0.1', 8888))
  6. # 先接收固定长度的字典的报头
  7. dict_header_len = client.recv(4)
  8. # 解析出字典的真实长度
  9. dict_real_len = struct.unpack('i', dict_header_len)[0]
  10. # 根据真实长度来接收字典数据
  11. dict_data_bytes = client.recv(dict_real_len)
  12. # 将已编码的JSON字符串解码为字典
  13. dict_data = json.loads(dict_data_bytes)
  14. # 打印字典内容
  15. print(dict_data)
  16. recv_size = 0
  17. # 循环接收文件数据,针对大文件的接收采用循环的形式一次接受一点点
  18. with open(dict_data.get('file_name'), 'wb') as f:
  19. while recv_size < dict_data.get('file_size'):
  20. data = client.recv(1024)
  21. recv_size += len(data)
  22. f.write(data)