网络编程基础
理论基础
发展(了解)
早期数据交互的格式是没有网络的, 两个文件之间的数据交互需要通过第三个文件:
a, b文件同时和c文件交互, a文件把数据先存放到c文件中, b文件从c文件取, 反之亦然
有网络之后:
a文件中的数据, 通过网络协议, 转化为101010...二进制进行发送
a文件借助socket发送数据, b文件借助socket接收数据, 形成两大架构
socket (套接字): 是一个收发数据的工具
两大架构 **
c/s 架构 : client(客户端)
server(服务端) 一旦开启, 永不关机, 除非宕机
B/S 架构 : Brower(浏览器) server
通过输入网址, 访问对方的服务器, 对方服务响应之后, 把数据返回, 浏览器通过返回的数据, 渲染页面,看到最后的网页
(1)bs 和 cs 架构之间的关系?
(2)哪一种架构更好呢?
未来更倾向于使用B/S架构
1.省去复杂下载安装环节, 节省手机电脑的空间
2.手机带来便捷性, 随时随地使用想用的应用
3.网络发展, 为云空间提供更大发展空间, 也可能会有更多云客户端
网段 ***
网段的作用:
主要用来划分同一区域里的某些机器是否能够互相通信。
在一个网段里可以不同过因特网, 直接对话
判别的依据:
如果IP地址和子网掩码相与得到的值相同就是同一网段
ip1: 192.168,11.251
子网掩码:255.255.255.0
11000000 10101000 00001011 11111011
11111111 11111111 11111111 00000000
11000000 10101000 00001011 00000000 => 192.168.11.0 (网段)ip2: 192.168,12.35
子网掩码:255.255.255.0
11000000 10101000 00001100 00100011
11111111 11111111 11111111 00000000
11000000 10101000 00001100 00000000 => 192.168.12.0 (网段)ip1: 192.168,11.251
子网掩码:255.255.0.0
11000000 10101000 00001011 11111011
11111111 11111111 00000000 00000000
11000000 10101000 00000000 00000000 => 192.168.0.0 (网段)ip2: 192.168,12.35
子网掩码:255.255.0.0
11000000 10101000 00001011 11111011
11111111 11111111 00000000 00000000
11000000 10101000 00000000 00000000 => 192.168.0.0 (网段)
下面的网络相同,意味着可以互相通信
端口 ***
概念: 具体某个程序与外界通讯的出口
取值范围: 0~65535
192.168.2.1:8000: 通过ip和端口, 可以访问这个世界上任何一个电脑里的任何一个软件
自定义端口时, 最好命名8000以上的端口号, 端口大全:
https://blog.csdn.net/l_smalltiger/article/details/81951824
常用端口
20 : FTP文件传输协议(默认数据口)
21 : FTP文件传输协议(控制)
22 : SSH远程登录协议
25 : SMTP服务器所开放的端口,用于发送邮件
80 : http用于网页浏览,木马Executor开放此端口
443 : 基于TLS/SSL的网页浏览端口,能提供加密和通过安全端口传输的另一种HTTP => HTTPS
3306: MySQL开放此端口
OSI ***
网络七层模型: 应表会传网数物
应用层 (应用层, 表示层, 会话层)
封装数据:
根据不同的协议,封装不同格式的数据
http (超文本传输协议)
HTTPS (加密传输的超文本传输协议)
FTP (文件传输协议)
SMTP (电子邮件传输协议)
应用层与传输层和网络层之间, 有个socket抽象层, 进行信息分割和拼接
传输层:
封装端口:
指定传输协议 (TCP协议/UDP协议)
网络层:
封装ip:
ipv4版本 / ipv6 (ICMP/IGMP)
数据链路层:
封装mac地址:
指定mac地址 arp协议[ip->mac] / rarp协议[mac->ip])
物理层:
打成数据包, 变成二进制的字节流, 通过网络进行传输
arp协议 ***
交换机: 从下到上拆2层, 拆到数据链路层
路由器: 从下到上拆3层, 拆到网络层(得到对应的网段)
arp协议: 通过ip -> mac
rarp协议: 通过mac -> ip
arp协议整体是通过: 一次广播 + 一次单播 实现
arp协议的完整过程:
电脑a发现目标主机没有mac, 先发送arp广播包, 把mac标记成全F的广播地址
交换机接受到arp的广播包, 进行从下到上拆包, 拆2层, 拆到数据链路层看到全F广播地址, 开始广播, 把这个广播包发送给每一台主机
每台主机得到广播包后, 都开始拆包, 如果该数据包找寻的主机不是自己, 自动舍弃
路由器得到arp广播包后, 从下到上拆包, 拆3层, 拆到网络层, 得到网段信息
通过路由器的对照信息表, 找到网段对应的网关(接口)
对应网关的这台交换机得到arp广播包后,从下到上拆包, 拆2层, 发现全F广播地址进行广播
数据库主机收到广播包后, 依次从下到上拆包, 发现自己是目标要找的那台主机, 把自己的ip->mac对照信息封装, 变成arp响应包, 发送给对应的交换机
交换机得到arp响应包之后, 依次进行单播, 返回给最终的原主机
注意: 在回来的过程中, 所有得到过相应arp广播包的主机都会自动更新自己的arp解析表, 方便下次使用
TCP/UDP协议 ***
# tcp
TCP(Transmission Control Protocol)一种面向连接的、可靠的、传输层通信协议(比如:打电话)
优点: 可靠, 稳定, 传输完整稳定, 不限制数据大小
缺点: 慢, 效率低, 占用系统资源高, 一发一收都需要对方确认
应用:Web浏览器, 电子邮件, 文件传输, 大量数据传输的场景
# udp
UDP(User Datagram Protocol)一种无连接的, 不可靠的传输层通信协议(比如:发短信)
优点: 速度快, 可以多人同时聊天, 耗费资源少, 不需要建立连接
缺点: 不稳定, 不能保证每次数据都能接收到
应用: IP电话, 实时视频会议, 聊天软件, 少量数据传输的场景
TCP三次握手
客户端发送一个请求消息, 与服务端建立连接
服务端接受这个请求, 发出响应消息, 回应客户端, 也要与客户端a建立连接(看下a是否同意)
客户端接受服务端的响应消息之后, 发送回复消息(表达同意, 到此客户端与服务端建立连接成功)
TCP发送数据
每次发送一次数据, 都会对应一个回执消息, 如果发送方没有接受到回执消息, 那么该数据包再发送一次
TCP四次挥手
客户端向服务端发送一个断开连接请求(代表客户端没有数据给服务端)
服务端接受请求,发出响应
等到服务端所有数据发送完毕之后, 服务端向客户端发送断开连接请求
客户端接受请求, 发出响应
注意: 等到2msl(msl: 最大报文段生存时间)这么长时间之后, 客户端与服务端彻底断开连接
socket
socket的意义:通络通信过程中, 信息拼接的工具 (中文:套接字)
socket的英文原义是“孔”或“插座”,在Unix的进程通信机制中又称为‘套接字’。套接字并不复杂,它是由一个ip地址以及一个端口号组成。
socket模块
常用方法
socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)
# 获取要连接的对端主机地址
sk.bind(address)
# 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
# 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5,这个值不能无限大,因为要在内核中维护连接队列
sk.setblocking(bool)
# 是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
sk.accept()
# 接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。接收TCP 客户的连接(阻塞式)等待连接的到来
sk.connect(address)
# 连接到address处的套接字。一般,address的格式为元组(hostname, port, 如果连接出错,返回socket.error错误。
sk.connect_ex(address)
# 同上,只不过会有返回值,连接成功时返回 0,连接失败时候返回编码,例如:10061
sk.close()
# 关闭套接字
sk.recv(bufsize[,flag])
# 接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。
sk.recvfrom(bufsize[.flag])
# 与recv()类似,但返回值是(data, address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
sk.send(string[,flag])
# 将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
sk.sendall(string[,flag])
# 将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。内部通过递归调用send,将所有内容发送出去。
sk.sendto(string[,flag],address)
# 将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
sk.settimeout(timeout)
# 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
sk.getpeername()
# 返回连接套接字的远程地址。返回值通常是元组(ipaddr, port)。
sk.getsockname()
# 返回套接字自己的地址。通常是一个元组(ipaddr, port)
sk.fileno()
# 套接字的文件描述符
socket.sendfile(file, offset=0, count=None)
# 发送文件, 但目前多数情况下并无什么卵用
TCP收发消息
server端
import socket
import time
# 1.创建一个socket对象
sk = socket.socket()
# 一个端口绑定多个程序(仅在测试时使用)
# sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2.在网络中注册该主机(绑定对应的ip和端口号)
"""默认本地IP: 127.0.0.1 => localhost"""
sk.bind(("127.0.0.1", 9980))
# 3.开启监听
sk.listen()
# 4.三次握手
# conn, addr = sk.accept()
# print(conn, addr)
#accept方法返回一个含有两个元素的元组(connection,address)。
#第一个元素connection是所连接的客户端的socket对象(实际上是该对象的内存地址),服务器必须通过它与客户端通信;
#第二个元素 address是客户的Internet地址。
# 5.收发数据的逻辑
while True:
connection, address = sk.accept()
while True:
content = input("server: ")
if content.lower() == "q":
break
else:
connection.send(content.encode())
print(connection.recv(1024).decode())
connection.close()
# 6.四次挥手
# conn.close()
# 7.退还端口
sk.close()
client端
import socket
# 1.创建一个socket对象
sk = socket.socket()
# 2.与服务端建立连接
sk.connect(("localhost", 9980))
# 3.收发数据
"""
发送的数据类型是bytes
b开头必须是ASCII编码字符
"""
while True:
print(sk.recv(1024).decode())
content = input("client: ")
if content.lower() == "q":
break
else:
sk.send(content.encode())
# 4.断开连接
sk.close()
UDP收发消息
server端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(("localhost", 9980))
while True:
msg, addr = sk.recvfrom(1024)
print(msg.decode())
content = input("server: ")
if content.lower() == "q":
break
else:
sk.sendto(content.encode(), addr)
sk.close()
client端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
while True:
content = input("client: ")
if content.lower() == "q":
break
sk.sendto(content.encode(), ("localhost", 9980))
res, addr = sk.recvfrom(1024)
print(res.decode())
sk.close()
黏包与解决 ***
黏包现象
# tcp协议在发送数据时,会出现黏包现象.
(1)数据粘包是因为在客户端/服务器的发送端和接收端都会有一个数据缓冲区,
缓冲区用来临时保存数据,默认空间都设置较大。在收发数据频繁时,由于tcp传输消息的无边界特点,不清楚应该截取多少长度,导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包
(2)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个数据包。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后在发送,这样接收方就收到了粘包数据。
(3)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接缓冲区,用户进程从该缓冲区取数据,若下一个数据包到达时,上一个数据包尚未被用户进程取走,则系统可能把多条数据当成是一条数据进行截取
# 总结: TCP协议是面向连接的无边界协议
黏包现象一:
在发送端,由于在缓冲区两个数据小,发送的时间隔短,TCP会根据优化算法把这些数据合成一个发送
黏包现象二:
在接收端,由于在缓冲区没及时接受数据,截取数据时把多次发送的数据截取成一条,形成了黏包
黏包对比:tcp和udp
#tcp协议:
缺点: 接收时数据之间无边界, 有可能粘合几条数据成一条数据,造成黏包
优点: 不限制数据包的大小, 稳定传输不丢包
#udp协议:
优点: 接收时候数据之间有边界, 传输速度快, 不黏包
缺点: 限制数据包的大小(受带宽路由器等因素影响), 传输不稳定, 可能丢包
#tcp和udp对于数据包来说都可以进行拆包和解包,理论上来讲,无论多大都能分次发送
但是tcp一旦发送失败,对方无响应(对方无回执),tcp可以选择再发,直到对应响应完毕为止
而udp一旦发送失败,是不会询问对方是否有响应的,如果数据量过大,易丢包
解决黏包问题
目前解决黏包主要是通过控制接收字节数来实现的, 在发数据之前先发送真实数据的长度给接收方, 然后再发送真实数据
可以通过struct模块较方便实现
# 解决黏包场景:
应用场景在实时通讯时, 需要阅读此次发的消息是什么
# 不需要解决黏包场景:
下载或者上传文件的时候, 最后要把包都结合在一起, 黏包无所谓
struck模块
pack()
打包, 把任意长度数字转换成具有固定4个字节长度的字节流
# i => int 要转换的当前类型是整型
"""范围: -21亿~21亿左右 控制在1.8G之内"""
res = struct.pack("i" , 999999998)
print(res , len(res))
res = struct.pack("i" , 1111111119)
print(res , len(res))
res = struct.pack("i" , 3)
print(res , len(res))
res = struct.pack("i" , 2000000000)
print(res , len(res))
unpack()
解包, 把4个字节长度的值恢复成原来的数字,返回元组
# i => 把对应的数据转化成整型
tup = struct.unpack("i" , res)
print(tup) # (2000000000,)
print(tup[0])
server端
import socket
import struct
sk = socket.socket()
sk.bind(("127.0.0.1", 9980))
sk.listen()
conn, addr = sk.accept()
print(conn, addr)
num = int(input("s请输入数据长度: "))
conn.send(struct.pack("i", num))
conn.send(input("s请输入内容: ").encode())
conn.send(input("s请输入内容测试不黏包: ").encode())
conn.close()
sk.close()
client端
import socket
import time
import struct
sk = socket.socket()
sk.connect(("127.0.0.1", 9980))
length = struct.unpack("i", sk.recv(4))[0]
print(length)
time.sleep(3)
print(sk.recv(length).decode())
print(sk.recv(1024).decode())
sk.close()

浙公网安备 33010602011771号