网络编程

软件开发架构

什么是架构?
编写项目之前需要遵循的代码层面上的一些规范(例如,运行的步骤,流程等)

c/s架构

c:client 客户端
s:server 服务端
计算机或手机下载的各类app本质上就是客户端
优势是:下载对应的客户端,可以在客户端体验高度定制服务
劣势是:使用必须下载!!

b/s架构

b:broswer 浏览器
s:server 服务器
通过浏览器来充当各个服务器的客户端,用于想要体验服务又不用下载客户端
劣势是:网页的各种功能花里胡哨,观看效果不佳

架构发展趋势

1.统一接口原则
(可以通过一个接口去往其他程序)
2.cs和bs交叉使用,避免各自的劣势

网络编程

网络编程简介
基于网络编写代码,程序可以实现远程数据交互
网络编程的起源
目的:为了解决计算机之间远程数据交互

网络编程的意义:学习完就可以编写cs架构

网络编程的起源:任何先进的技术一般都来源于军事

网络编程的要求:计算机之间想要实现远程数据交互,首要条件就是要有物理连接介质

OSI七层协议

规定了计算机涉及到远程交互的时候必须要经过相同的流程

数据发送出去的时候 是从上往下走
数据接收回来的时候 是从下往上走

应用层

主要是程序员自己编写代码的地方,有什么协议和规范取决于程序员自己
常见协议有:HTTP,HTTPS等等

表示层

会话层

传输层

1.port协议

  端口协议:规定了一台计算机上的每一个正在运行的app都必须要有一个端口号
  端口号相当于计算机用于管理多个app的标记
  
  端口号特征:
    1.端口号范围:0-65535  
	2.端口号是动态分配的
	3.同一时间同一台计算机的端口号是不能冲突的
	4. 
	    0-1024:一般是操作系统内部需要使用的
        1024-8000:一般是常见的软件已经使用了
        8000+:我们平时写代码可以使用8000之后的端口号
		
IP:用于标识全世界任意一台接入互联网的计算机
PORT:用于标识一台计算机上的某个app
IP+PORT:用于标识任意一台接入互联网的计算机上的某个app、

网址(URL):统一资源定位符
URL的本质就是IP+PORT

2.TCP协议与UDP协议
规定了数据传输所遵循的规则
ps:数据传输能够遵循的协议有很多,TCP协议与UDP协议是比较常见的两个
2.TCP协议
1.三次握手 (建连接)
1.三次握手 (建连接)建立双向通道 
 在发数据前必须和对方建立可靠的连接,连接必须要经过三次,对话才能建立起来
 eg图1:
  c(表示客户端)s(表示服务端)
  第一次握手:是c朝s发出请求,请求可以建立连接
  第二次握手:是s收到c的请求,并发出可以建立连接的信号
  (此时的数据通道是单向的,只有c可以向s发送数据)
  第三次握手:是c朝s发送确认,并允许s和自己建立连接,双方建立连接,c和s可以传输数据了
  (此时双方都可以基于彼此建立的连接互相发送数据了)

ps:基于tcp传输数据很安全的原因是TCP会二次确认,数据不容易丢失。每次发送出去的消息都必须要返回确认消息,否则的话再短时间内会反复发送
  
洪水攻击:同时有多个客户端向服务端发送TCP连接请求!

image

2.四次挥手 (断连接)
 基于三次挥手之后,想要断开连接
 eg下图:
 第一次挥手:c朝s发起请求,想要断开连接
 
 第二次挥手:s向c发起确认,允许断开连接
 (此时的c已经不可以向s发送数据了 ,但是s还是可以给c发送数据)
 从第二次到第三次挥手之间有一个时间间隔,s可能还需要有接受数据的时间
 
 第三次挥手:s朝c发起请求,想要断开连接
 
 第四次挥手:c向s发起确认,允许断开连接
 (此时的双方都不可以再互相发送数据了)

image

3.UDP协议
 基于UDP发送的数据没有任何通道也没有任何的限制,缺点是数据容易丢失
(可以基于发送过程做一些优化操作)
服务端代码
import socket

res = socket.socket(type=socket.SOCK_DGRAM)
res.bind(('127.0.0.1',8080))

msg,address = res.recvfrom(1024)
# 接收客户端发送过来的消息
print('msg>>>%s'%msg.decode('utf8'))
# 由于UDP没有双向通道 所以每次发送消息都会带着它的地址
print('address>>>>:',address)
res.sendto('服务端'.encode('utf8'),address)


客户端代码
import socket

c = socket.socket(type=socket.SOCK_DGRAM)
server_address = ('127.0.0.1',8080)
c.sendto('客户端'.encode('utf8'),server_address)
msg,address = c.recvfrom(1024)
print('msg>>>"%s'%msg.decode('utf8'))
print('address>>>>>:',address)

网络层

ip协议
  :规定了任何接入互联网的计算机都必须有IP地址
  每个ip地址都自带定位
  IP地址:
    IPV4:点分十进制
	最小:0.0.0.0
	最大:255.255.255.255
    IPV6:
	   无限大了 能够给地球上的每一粒沙子都分一个ip地址

数据链路层

1.规定了电信号的分组方式
2.规定了每台计算机都必须有一块网卡,网卡上必须有一串记录(电脑的以太网址也称mac地址,身份证号)
3.mac地址:由1216进制数组成
  前6位:厂商编号
  后6位:生产流水线号
可以根据该地址查找到计算机,基于mac地址实现数据交互

物理连接层

保证物理连接介质的条件,传递电信号(主要研究插网线情况)

网络相关设备

1.交换机
  能够让接入交换机的多台计算机实现彼此互联
2.以太网通信(mac通信)
   有了交换机以后,根据电脑的mac地址就可以实现数据交互
   广播:就是在交换机中发出请求,所有接入交换机的设备都能收到
   单播:只有被查找设备才会回复相应信息
   
   缺陷:1.mac地址通信仅限于局域网
        2.接入交换机的设备过多的话 可能会造成广播风暴
		
3.局域网
  有某个固定的区域组成的网络
 
4.路由器
  将多个局域网连接到一起的设备

socket套接字

主要作用于软件层和应用层中间 ,作为一个接口。如果没有socket,那么我们就需要手动操作各个层之间的代码了!!
image

发展史

1.一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
2.基于文件类型的套接字家族:套接字家族的名字:AF_UNIX
3.基于网络类型的套接字家族:套接字家族的名字:AF_INET,所有地址家族中,AF_INET是使用最广泛的一个

socket实例

先运行服务端再运行客户端
1.服务端
import socket

# 1.先创建一个socket对象
server = socket.socket()
# 绑定一个固定的地址IP和port
server.bind(('192.168.1.169',8080))
# 半连接池
server.listen(5)
# 开业等待接口
sock,address = server.accept()
print(sock,address)    # sock是双向通道  address是客户端地址65459
# 数据交互
sock.send(b'hello')   # 朝客户端发送数据
res=sock.recv(1024)  # 接收客户端发送的数据
print(res)
# 断开连接
sock.close()  # 断连接
server.close() # 关机

------------
2.客户端
import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('192.168.1.169', 8080))
# 3.数据交互
res=client.recv(1024)  # 接收服务端发送的数据
print(res)
client.send(b'hei')  # 朝服务端发送数据
# 关闭
client.close()  # 直接把双向通道关闭

代码优化

1.send与recv
	客户端与服务端不能同时执行同一个
    	有一个收 另外一个就是发
       有一个发 另外一个就是收
    不能同时收或者发!!!
2.消息自定义
	input获取用户数据即可(主要编码解码)
3.循环通信
	给数据交互环节添加循环即可
4.服务端能够持续提供服务
	不会因为客户端断开连接而报错
		异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待
5.消息不能为空
	判断是否为空 如果是则重新输入(主要针对客户端)
# 服务端
import socket

# 1.先创建一个socket对象
server = socket.socket()
# 绑定一个固定的地址IP和port
server.bind(('192.168.1.169',8080))
# 半连接池
server.listen(5)
# 开业等待接口
while True:
    sock,address = server.accept()
    print(sock,address)    # sock是双向通道  address是客户端地址65459
    # 数据交互
    while True:
        try:
            msg = input('请输入你要发送的内容》》》:').strip()
            if len(msg) == 0:
                continue
            sock.send(msg.encode('utf8'))   # 朝客户端发送数据
            res = sock.recv(1024)  # 接收客户端发送的数据
            print(res.decode('utf8'))
        except ConnectionResetError:
            break

# 客户端
import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('192.168.1.169', 8080))
# 3.数据交互
while True:
    res = client.recv(1024)  # 接收服务端发送的数据
    print(res.decode('utf8'))
    msg = input('请输入你要发送的内容》》》》:').strip()
    if len(msg) == 0:
        msg = '重来重来!!!'
    client.send(msg.encode('utf8'))  # 朝服务端发送数据

半连接池
server.listen(5)
# 括号里的数字就是等待和服务端连接数据的客户端
这个功能主要是为了优化代码,避免无效等待
假设 现在有数不清的客户端想向服务端发数据 ,如果没有这个半连接池的话,是不是所有的用户都要等在外面 让服务端和上一个客户端交互完成再接着下一个,那么这个半连接池就是再告诉后面排队等待的用户 你的前面还有几个人再等着 你可以现在干别的事情,避免无效等待!!

粘包现象

粘包问题的产生
1.TCP特性
	流式协议:所有的数据类似于水流 连接在一起的
    	ps:数据量很小 并且时间间隔很多 那么就会自动组织到一起
2.recv
	我们不知道即将要接收的数据量多大 如果知道的话不会产生黏包

代码实例
# 服务端
import socket

s = socket.socket()
s.bind(('192.168.1.169',8080))
s.listen(5)
sock,address =s.accept()
sock.send(b'hello')
sock.send(b'hai')
sock.send(b'hahaha')


------------


# 客户端
import socket

res = socket.socket()
res.connect(('192.168.1.169', 8080))
c = res.recv(1024)
print(c.decode('utf8'))
c = res.recv(1024)
print(c.decode('utf8'))
c = res.recv(1024)
print(c.decode('utf8'))


打印结果:
hello
haihahaha  # 出现了粘包现象

struct模块

简介
struct模块无论数据长度是多少 都可以帮你打包成固定长度
然后基于该固定长度 还可以反向解析出真实长度
ps : 对于数据量特别大的模块,会直接报错
粘包问题的解决方案
# 服务端
1.先将真实数据的长度制作成固定长度 4
2.先发送固定长度的报头
3.再发送真实数据

# 客户端
1.先接收固定长度的报头  4
2.再根据报头解压出真实长度 
3.根据真实长度接收即可
代码实操
服务端
1.先构造一个详细的数据字典
2.对字典数据进行打包,获得一个打包之后的固定长度
3.发送打包以后的数据给客户端
4.将字典数据发送给客户端
4.发送真实数据给客户端
import os
import socket
import struct
import json

res = socket.socket()
res.bind(('192.168.1.169', 8080))
res.listen(5)

while True:
    sock, address = res.accept()
    while True:
        # 1.先构建数据文件的字典
        file_dict = {
            'file_name': '视频合集',
            'file_size': os.path.getsize(r'视频合集'),
            'file_content': 'python课程',
            'file_root': 'summer'

        }
        # 2.将文件字典打包成固定长度数据
        dict_json = json.dumps(file_dict)
        file_bytes_dict = len(dict_json.encode('utf8'))
        dict_len = struct.pack('i',file_bytes_dict )
        # 3.发送固定字典的长度
        sock.send(dict_len)
        # 4.发送真实字典数据 (先把字典转为json格式 再发送 )
        res = json.dumps(file_dict)
        sock.send(res.encode('utf8'))
        # 5.发送真实数据
        with open(r'视频合集','rb')as f:
            for line in f:
                sock.send(line)
        break
客户端
1.先接收固定长度的数据
2.根据固定长度解析出即将要接收的字典的真实长度
3.接收字典数据
4.根据字典长度获取真实字典的数据长度
5.接收真实长度
import json
import socket
import struct


s = socket.socket()
s.connect(('192.168.1.169', 8080))


# 1.先接收固定长度的数据报头
res = s.recv(4)
# 2.根据报头解析出字典的长度
dict_len = struct.unpack('i',res)[0]
# 3.接收字典数据
dict_data = s.recv(dict_len)
# 4.解码并反序列化出字典
real_dict = json.loads(dict_data)
print(real_dict)
# 5.从数据字典中获取真实数据的各项信息
total_size = real_dict.get('file_size')
# 获取数据  由于获取到的是一个文件 必须先读出来再循环打印
# file_size = 0
# with open(r'%s'%real_dict.get('file_name'),'wb')as f:
#     while file_size < total_size:
#         data = s.recv(1024)
#         f.write(data)
#         file_size += len(data)
#         print('文件接收完毕')
#         break
# 这里也可以直接接收具体字节数 因为我们前面已经知道他这个文件到底有多大了嘛 就不需要写的这么复杂
dict1 = s.recv(75)
print(dict1.decode('utf8'))
dict1 = s.recv(75)
print(dict1.decode('utf8'))
dict1 = s.recv(75)
print(dict1.decode('utf8'))

代码运行结果:
image

posted @   Hsummer  阅读(96)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示