软件开发架构

软件开发架构有很多,但是涉及到两个程序之间通讯的应用大致可以分为两种,一种是应用类比如: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

语法

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是基于连接的,必须先启动服务端,然后再启动客户端去链接服务端

服务端

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黏包是指发送方发送的若干包数据到接收方接收时成一包,造成数据在接收方缓冲区的堆积。

两种情况下会发生黏包

  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. 收数据没收干净,有残留,就会下一次结果混淆在一起

解决的核心法门就是:每次都收干净,不要任何残留

解决黏包问题

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

服务端

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)
Last modification:April 17, 2022
如果觉得我的文章对你有用,请随意赞赏