网络编程基础

内容概要

  • 软件开发架构
  • 架构发展趋势
  • 网络编程简介
  • OSI七层协议
  • 各种重点协议
  • 黏包现象
    • 解决黏包逻辑思路
    • 代码实操
  • UDP基本代码使用

软件开发架构

软件开发架构规定了程序的请求逻辑、功能分块

1.C/S 架构
	Client:客户端
    Server:服务端
# 我们使用计算机下载下来的app本质是各大互联网公司的客户端软件
# 通过客户端我们就可以体验互联网公司给我们提供的服务
一般情况下客户端与服务端需要互联网,但是单机版本的软件就不需要,因为单机版本相当于将客户端和服务端都在同一台计算机上

客户端就相当于准备消费的客人,服务端就相当于提供服务的店铺
所以作为服务端需要具备多个条件:
'''
1.24小时不间断的提供服务
2.固定的地址
3.能够同时服务多个客人(高并发)
'''
2.B/S 架构
	Browser:浏览器
    Server:服务端/器
# 浏览器可以充当所有服务端的客户端
# B/S架构的本质还是C/S架构
'''
C/S架构 
	优势: 不同公司的客户端是由不同公司独立开发,因此可以高度定制化客户端功能
	劣势: 需要进行下载使用
B/S架构
	优势:不需要下载直接就可以使用
	劣势:无法高度定制化,必须遵循很多规则
'''

架构总结

软件设计的大方向>>>>>>>>统一接口
	ps:  微信小程序  
         支付宝小程序
ATM项目
选课系统项目
本质上也属于软件开发架构的范畴

网络编程简介

1.网络编程的定义:
	网络编程是基于网络编写代码,能够实现数据的远程交互
2.学习网络编程的目的:
	能够开发C/S架构的软件
3.网络编程的起源:
	'''
	最早起源于美国军事领域
	是为了实现计算机之间的数据交互
	当时只能通过硬盘拷贝,不能实现远距离的信息传输
	'''

网络相关专业名词

计算机想要实现数据交互必须要链接到一起

子网掩码与IP

我们在接入网络设备之后需要一个IP去代指这个电脑
IP是一个32位的二进制,为了方便记忆就将他分为了四组,每组8位,通过小数点分开
如:
# 二进制表示:00000000.10010111.11111111.00001111
# 十进制表示:251.151.255.15
网络中的每一台电脑都会有一个IP与之绑定,那么我们就可以通过IP找到相应的电脑

IPv4,长度为 32 位(4 个字节), 格式:A.B.C.D
IPv6,长度为 128 位(16 个字节),用":"分成8段,格式:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX(每个X是一个16进制数)。
  • 网络地址与主机地址
# IP地址可以划分为两个部分,一个是网络地址,一个是主机地址
我们可以通过子网掩码去去确定网络地址和主机地址
'''
  1.  	IP:192.168.1.199      11000000.10101000.00000001.11000111
	子网掩码:255.255.255.0     11111111.11111111.11111111.00000000
此时,网络地址就是前24位 + 主机地址是后8位。你可能见过有些IP这样写 192.168.1.199/24,意思也是前24位是网络地址。

  2.  	IP:192.168.99.254     11000000.10101000.01100011.11111110
	子网掩码:255.255.240.0     11111111.11111111.11111100.00000000
此时,网络地址就是前22位 + 主机地址是后10位。你可能见过有些IP这样写 192.168.99.254/22,意思也是前22位是网络地址。
'''
  • 划分网络地址与主机地址的意义
网络地址相同的IP,也属于同一个网段
在局域网中,只有同一个网段的IP才能相互通信,不同网段IP想要通信需要借助路由器的转发才能通信。

DCHP(动态主机配置协议)

  • 配置主机IP地址
# 在一个局域网内想要给某台电脑分配IP有两种方式:
自动获取,通过DCHP服务自动分配IP、子网掩码、网关
手动设置

image

内网和公网IP

我们在局域网内为电脑分配的IP都称为'内网IP',基于内网IP可以在一个局域网内进行相互通信(也需要相关的配置)

那么我们如果想通过互联网去进行通信的话,我们就需要借助公网IP

image

  • 第一步:左边公司,去运营商申请公网的固定IP(办理专线宽带时运营商会分配至少1个固定的IP地址),其实运营商就是将你拉的这个专线和固定IP创建绑定关系。(假设公网IP:123.206.15.88)
  • 第二步:配置公网IP与指定服务器的转发规则。
  • 第二步:右边家庭,如果想要访问某个公司服务器上的网网站,只需要执行指定IP:123.206.15.88,运营商就会根据IP找到与之关联的公司专线,并通过公司路由器、防火墙等设备找到指定的服务器。
'''按理说,每个从运营商接入网的用户都可以有一个外网IP,但由于全球用户太多而IP根本就不够分配,所以,运营商网络会进行划分,让多个家庭宽带用户共用一个公网IP(动态,可能每次上网公网IP都不一样)。

让家庭用户想要通过网络访问访问其他IP时,先发给运营商由运营商向外转发到其他IP。

注意:外部用户想要访问家庭宽带的IP时,运营商不会把请求转发到我们的电脑。'''

我们如果想要开发一个可以供全球用户访问的网站,那么我们就需要以下几点

  • 拉专线,申请固定公网IP
  • 买一台服务器(就是性能好的电脑)
  • 公网IP绑定至此服务器
  • 将写好的代码放在服务器上并运行起来

云服务器

  • 云服务器的概念

    简单来说,云服务器就相当于运营商买了很多高性能的服务器(电脑),然后通过租赁这些服务器资源,让用户不需要自己去拉专线、配置网络、买服务器
    

    就相当于高性能的电脑分出很多虚拟机,租赁给用户使用

image

域名

假设你创业开发了一个网站,用户很难记住你的公网IP: 123.203.15.88:82
所以,域名就诞生了,让域名和IP创建对应关系,用户只需要记住域名就可以了,例如:

域名只是与IP创建了对应的关系,但是与端口无关!

在用户在自己的电脑或手机上输入域名去访问时,其实要执行两个步骤:

  • 根据域名寻找IP。(寻找IP)
  • 获得IP之后,再通过IP再去访问指定服务器。

交换机

能够将所有接入交换机的计算机彼此互联起来

广播

首次查找接入同一个交换机的其他计算机,需要朝交换机里面说一声

单播

首次被查找的计算机回应查找它的计算机,并且附带自己的MAC地址

广播风暴

接入同一台交换机的多台计算机同时发广播

局域网

可以简单的理解为有单个交换机组成的网络 在局域网内可以直接使用mac地址通信

广域网

可以简单的理解为范围更大的局域网

互联网

由所有的局域网、广域网连接到一起形成的网络

路由器

不同的局域网计算机之间是无法直接实现数据交互的 需要路由器连接

OSI七层协议简介

'''
OSI七层协议:规定了所有的计算机在远程数据交互的时候必须经过相同的处理流程,在制造过程中必须拥有相同的功能硬件
'''
应用层
表示层
会话层
传输层
网络层
数据链路层
物理连接层
'''常见的是整合之后五层或者四层'''
应用层
传输层
网络层
数据链路层
物理连接层

应用层
传输层
网络层
网络接口层
'''
接收网络消息 数据从下往上传递
发送网络消息 数据由上往下传输
'''

物理链接层

主要用于确保计算机之间的物理链接介质,接收数据(bytes类型、二进制)

数据链路层

1.规定了电信号的分组方式
2.以太网协议
	规定了计算机在出场时都必须有一块网卡,网卡上有遗传数字
    '''
    该数字就相当于计算机的身份证号码是独一无二的
    该数字的特征:12位16进制数据
    	前6位:产商编号
    	后6位:流水线号
    该数字也称为:以太网地址/MAC地址
    '''

网络层

IP协议:规定了所有接入互联网的计算机必须有一个IP地址,类似于子身份证号码
	MAC地址是物理地址,无法进行修改
    IP地址是动态分配的 在不同的场地,IP地址也会发生改变
IP地址特征:
	IPV4:点分十进制
    范围:
    0.0.0.0
    255.255.255.255
	IPV6:能够给地球上每一粒沙分一个IP地址 
	IP地址可以跨局域网传输
# IP地址可以用来标识全世界独一无二的一台计算机

image

传输层

PORT协议(端口协议)
	用来识别一台计算机上面的某一个应用程序
    范围:0-65535
    特征:动态分配
建议:
0-1024  # 系统默认需要使用
1024-8000  # 创建软件的端口号
使用8000之后的端口号

URL:统一资源定位符(网址)
	网址本质上是有IP和PORT组成的!!!
    
IP+PORT:能够定位全世界独一无二的一台计算机上面的某一个应用程序

域名解析:将网址解析成IP+PORT

我们之所以不直接使用IP+PORT的原因是太难记 所以发明了域名(网址)

IP:PORT  实际使用冒号连接
    114.55.205.139:80

传输层TCP与UDP协议

TCP与UDP协议都是用来规定通信方式的
1.TCP协议(Transmission Control Protocol) # 传输控制协议
	是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。
	TCP协议也成为可靠协议(数据不容易丢失)
数据不容易丢失的点不是在于有双向通道,而是由反馈的机制,
不管是链接或者是断开链接的时候客户端与服务端双方都会有反馈
给对方发数据之后会保留一个副本 直到对方回应消息收到了才会删除
否则会在一定的时间内反复发送
# 洪水攻击
	同一时间有大量的客户端请求与服务端建立链接,这会导致服务端一直处于SYN_RCVD状态
# 服务端如何区分客户端建链接的请求
	可以对请求做唯一标识
四次挥手断链接
	1.四次不能合并为三次
	因为中间需要确认消息是否发完(TIME_WAIT)
2.UDP协议(User Data Protocol)
	是⼀个⽆连接的简单的⾯向数据报的传输层协议。 UDP不提供可靠性, 它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。 由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快。
	也称之为数据报协议,不可靠协议
UDP协议没有复杂的链接,只需要指定对方地址就可以发消息了
不需要管对方收到了还是没收到,反正我发了

三次握手具体流程

 0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

三次握手

  1.  SYN-SENT    --> <seq=100><CTL=SYN>               --> SYN-RECEIVED

  2.  ESTABLISHED <-- <seq=300><ack=101><CTL=SYN,ACK>  <-- SYN-RECEIVED

  3.  ESTABLISHED --> <seq=101><ack=301><CTL=ACK>       --> ESTABLISHED

四次挥手

  1.  FIN-WAIT-1  --> <seq=100><ack=300><CTL=FIN,ACK>  --> CLOSE-WAIT

  2.  FIN-WAIT-2  <-- <seq=300><ack=101><CTL=ACK>      <-- CLOSE-WAIT

  3.  TIME-WAIT   <-- <seq=300><ack=101><CTL=FIN,ACK>  <-- LAST-ACK

  4.  TIME-WAIT   --> <seq=101><ack=301><CTL=ACK>      --> CLOSED

在收发数据的过程中,只有有数据的传送就会有应答(ack),如果没有ack,那么内部会尝试重复发送。

应用层

应用层就相当于为程序员自己写的应用程序 里面的协议很多
	如:HTTPS、HTTP、FTP
详细内容到框架部分详解

Socket编程

Scocket模块

我们如果需要基于网络去编写进行数据交互的程序,那么意味着我们需要自己去用代码控制我们之前所学习的OSI七层协议(复杂,繁琐)
所以socket模块就类似于操作系统,提供了很多简单快捷的接口供我们使用
socket也叫套接字
	基于文件类型的套接字家族(单机)
  	AF_UNIX
  基于网络类型的套接字家族(联网)
  	AF_INET

服务端

import socket

# 产生一个socket对象,并指定采用的通信版本和协议 不写默认为TCP协议
# family=AF_INET基于网络的套接字 type=SOCK_STREAM流式协议即TCP
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 绑定一个固定的地址  127.0.0.1为本地回环地址 只有自己的电脑可以访问  8080为端口
server.bind(('127.0.0.1', 8080))
# 设置半链接池
server.listen(5)
# 等待客户端发送请求
sock, addr = server.accept()  # return sock, addr  三次握手
print(sock, addr)  # sock就是双向通道 addr就是客户端地址
data = sock.recv(1024)  # 接收客户端发送过来的消息 1024字节
print(data.decode('utf8'))
# 给客户端发送消息 注意消息必须是bytes类型
sock.send('你好~'.encode('utf8'))
# 6.关闭双向通道
sock.close()  # 四次挥手
# 7.关闭服务端
server.close()  # 店关了

客户端

import socket
# 1.生成socket对象指定类型和协议
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2.通过服务端的地址链接服务端
client.connect(('127.0.0.1', 8080))
# 3.直接给服务端发送消息
client.sendall('你好~'.encode('utf8'))
# 4.接收服务端发送过来的消息
data = client.recv(1024)
print(data.decode('utf8'))
# 5.断开与服务端的链接
client.close()

代码优化

1.消息内容自定义
	针对性消息采用input获取
2.让消息循环
	将消息部分使用循环包起来
3.用户输入的消息不能为空
	本质上是服务端和客户端不能都是recv或send 一定为一边发一边收
4.服务端多次重启可能会报错
	Address alrady in use 主要是MAC电脑会报
    解决方法1:更改端口号
    解决方法2#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8085))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)   #接收客户端信息
print(ret)              #打印客户端信息
conn.send('hi'.encode('utf8'))        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)
5.当客户端异常断开的情况下 如何让服务端继续服务其他客人
	windows服务端会直接报错
  mac服务端会有一段时间反复接收空消息延迟报错	
  	异常处理、空消息判断

半连接池概念

sever.listen(5)  # 半连接池

当有多个客户端来链接的情况下 我们可以设置等待数量(不考虑并发问题)
假设服务端只有一个人的情况下
我们在测试半连接池的时候可以不使用input获取消息,直接把消息写死即可

黏包现象

1.服务端连续执行三次recv
2.客户端连续执行三次send

# 客户端
import socket

client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
client.connect(('127.0.0.1',9029))

client.send(b"hello world")
client.send(b"hello")
client.send(b"he")

client.close()
#  服务端
import socket
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.bind(('127.0.0.1',9029))
server.listen(5)
sock,addr = server.accept()
data = sock.recv(1024)
print(data.decode('utf8'))
data = sock.recv(1024)
print(data.decode('utf8'))
data = sock.recv(1024)
print(data.decode('utf8'))
server.close()

# 打印结果为  hello worldhellohe
'''服务端一次性接受到了客户端三次的消息,该现象称黏包现象'''

黏包问题产生的原因:
	1.不知道每次的数据到底有多大
    2.TCP本身也称之为流式协议:数据就像水流一样绵绵不绝没有间隔(TCP)会针对数据量较小且发送间隔较短的多条数据一次性合并打包发送

避免黏包现象的核心思路、关键点
	如何明确即将要接受的数据具体是多大
    如何动态的将长度变换的数据全部制作成固定长度的数据?

Struct模块


"""
解决黏包问题初级版本
    客户端
        1.将真实数据转成bytes类型并计算长度
        2.利用struct模块将真实长度制作一个固定长度的报头
        3.将固定长度的报头先发送给服务端 服务端只需要在recv括号内填写固定长度的报头数字即可
        4.然后再发送真实数据
        

"""
# 客户端代码
import struct
import socket

client = socket.socket()  # 创建客户端socket对象
client.connect(('127.0.0.1',9029))  # 绑定服务端ip地址及端口号

data = 'hello world'
data1 = 'hello'
data2 = 'he'

data_head = struct.pack('i', len(data))  # 打包data数据的 报头
client.send(data_head)  # 发送data报头给服务端
client.send(data.encode('utf8'))  # 在发送 data数据给服务端

data_head1 = struct.pack('i', len(data1))  # 打包data1数据的 报头
client.send(data_head1)  # 发送data1报头给服务端
client.send(data1.encode('utf8'))  # 在发送 data1数据给服务端

data_head2 = struct.pack('i', len(data2))  # 打包data2数据的 报头
client.send(data_head2)  # 发送data2报头给服务端
client.send(data2.encode('utf8'))  # 在发送 data2数据给服务端

'''
服务端
1.服务端先接收固定长度的报头
2.利用struct模块反向解析出真实数据长度
3.recv接收真实数据长度即可
'''

# 服务端代码
import socket
import struct

server = socket.socket()  # 创建socket服务端对象
server.bind(('127.0.0.1',9029))  # 绑定本机地址及本机端口
server.listen(5)  # 建立半连接池 设置连接数5

sock, addr = server.accept()  # 等待接收客户端请求及客户端地址



data_head = sock.recv(4)  # 接收data数据的报头
data_len = struct.unpack('i',data_head)[0]  # 解压data数据报头,拿到data数据字节长度
data = sock.recv(data_len)  # 接收 这个数据长度的字节,这样就确保了我们接收到的就是这个数据而不是其他的数据
print(data.decode('utf8'))

data_head1 = sock.recv(4)  # 接收data数据的报头
data_len1 = struct.unpack('i',data_head1)[0]  # 解压data1数据报头,拿到data1数据字节长度
data1 = sock.recv(data_len1)  # 接收 这个数据长度的字节,这样就确保了我们接收到的就是这个数据而不是其他的数据
print(data1.decode('utf8'))


data_head2 = sock.recv(4)  # 接收data数据的报头
data_len2 = struct.unpack('i',data_head)[0]  # 解压data2数据报头,拿到data2数据字节长度
data2 = sock.recv(data_len2)  # 接收 这个数据长度的字节,这样就确保了我们接收到的就是这个数据而不是其他的数据
print(data2.decode('utf8'))
    


'''问题1:struct模块无法打包数据量较大的数据 就算换更大的模式也不行'''
'''问题2:报头能否传递更多的信息  比如电影大小 电影名称 电影评价 电影简介'''
'''终极解决方案:字典作为报头打包 效果更好 数字更小'''
"""
黏包问题终极方案
    客户端 
        1.制作真实数据的信息字典(数据长度、数据简介、数据名称)
        2.利用struct模块制作字典的报头
        3.发送固定长度的报头(解析出来是字典的长度)
        4.发送字典数据
        5.发送真实数据     
"""
# 客户端代码
import socket
import os
import struct
import json

client = socket.socket()
client.connect(('127.0.0.1',9029))

file_path = r'D:\pythonproject\text\内容\文言文.txt'  # 获取文件路径
file_size = os.path.getsize(file_path)  # 获取文件字节大小

data_dict ={'file_name':'文言文.txt',  # 创建信息字典,这样我们不仅仅可以发文件,并且可以发送一些关于文件的信息
            'file_size':file_size,
            'file_data':'这里面有很多字',
            'file_info':'很多没用的字',}
data_dict_bytes = json.dumps(data_dict).encode('utf8') # 因为字典不能转换为二进制,那么我们通过json序列化字典,然后读取序列化后的字符即可
data_dict_head = struct.pack('i',len(data_dict_bytes))  # 生成序列化后的字典报头

client.send(data_dict_head)  # 发送给服务端!
client.send(data_dict_bytes)

with open(file_path,'rb') as f:  
    for line in f:
        client.send(line)


'''
服务端
1.接收固定长度的字典报头
2.解析出字典的长度并接收
3.通过字典获取到真实数据的各项信息
4.接收真实数据长度
'''
# 服务端代码
import socket
import struct
import json


server = socket.socket()
server.bind(('127.0.0.1',9029))
server.listen(5)

sock,addr = server.accept()
data_dict_head = sock.recv(4)  # 接受字典报头
data_dict_bytes_len = struct.unpack('i', data_dict_head)[0]  # 解压报头获取字典序列化后的长度
data_dict_bytes = sock.recv(data_dict_bytes_len)
data_dict = json.loads(data_dict_bytes)

total_size = data_dict.get('file_size')  # 获取文件字节大小
recv_size = 0
with open(data_dict.get('file_name'), 'wb') as f:
    while recv_size < total_size:
        data = sock.recv(1024 if total_size>1024 else total_size)
        f.write(data)
        recv_size += len(data)
        print(recv_size)

UDP协议

1.UDP服务端和客户端'各自玩各自的'
2.UDP不会出现多个消息发送合并
posted @   dd随风  阅读(123)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示