网络编程
一、网络通信
网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。通信是人与人之间通过某种媒体进行的信息交流与传递。网络通信是通过网络将各个孤立的设备进行连接,通过信息交换实现人与人,人与计算机,计算机与计算机之间的通信。
网络通信中最重要的就是网络通信协议。当今网络协议有很多,局域网中最常用的有三个网络协议:MICROSOFT的NETBEUI、NOVELL的IPX/SPX和TCP/IP协议。应根据需要来选择合适的网络协议。
互联网的本质就是一系列的协议。
二、OSI七层协议
互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层
OSI 七层模型通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,因此其最主要的功能就是帮助不同类型的主机实现数据传输 。
每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件。
2.1 物理层
物理层功能:提供一个物理连接接口,基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
2.2 数据链路层
数据链路层功能: 由于电信号0、1无任何意义,通过规定电信号的分组方式表示不同的数据信息。
以太网协议
统一电信号分组方式标准,即以太网协议ethernet。
ethernet规定:
-
一组电信号构成一个数据包,叫做‘帧’
-
每一数据帧分成:报头head和数据data两部分
head data head包含:(固定18个字节)
- 发送者/源地址,6个字节
- 接收者/目标地址,6个字节
- 数据类型,6个字节
data包含:(最短46字节,最长1500字节)
- 数据包的具体内容
head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
mac地址
以太网协议ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址。
每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
广播
拥有mac地址在同一网络下的两台计算机便能够通信了(一台主机通过arp协议获取另外一台主机的mac地址)。
以太网协议下的计算机通信通过‘吼’的方式传递信息,即广播。在局域网下,如果有四台计算机,其中一台与另一台通信,发送的信息在局域网下的所有计算机都是会接收到的。
2.3 网络层
假如互联网由许多局域网组成,那么采用以太网协议通信的方式就会造成一台计算机发出的消息让整个互联网接收到。
为了解决这个问题,我们需要有方法来区分计算机所在局域网,准确的广播某个局域网的计算机们。
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址。
ip协议
- 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
- 范围0.0.0.0-255.255.255.255
- 一个ip地址通常写成四段十进制数,例:172.16.10.1
ip地址分成两个部分,一是标识子网,二是标识主机。
子网掩码
所谓‘’子网掩码‘’,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。
知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
arp协议
地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址。
收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。
2.4 传输层
传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,等多个应用程序,
那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序与网卡关联的编号。
传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为系统占用端口
tcp协议
可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
tcp的三次握手和四次挥手
udp协议
不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
tcp是可靠传输是由于它的反馈机制,而udp无反馈机制,因此发送者只是把数据发出而不顾接收方是否收到数据。当然因此udp消耗的资源比tcp少。
2.5 应用层
应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式
应用层功能:规定应用程序的数据格式。
例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
三、socket
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
3.1 套接字类型及家族
在python中,默认使用的套接字类型是SOCK_STREAM,套接字家族是AF_INET。
3.1.1 套接字类型
1. 流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。
2. 数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
3. 原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。
3.1.2 套接字家族
1. 基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
2. 基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
3.2 套接字使用
server服务端
import socket
server = socket.socket()
server.bind(('127.0.0.1',8080)) #把地址绑定到套接字
server.listen(5) #监听链接
conn,addr = server.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
server.close() #关闭服务器套接字(可选)
client客户端
import socket
client = socket.socket() # 创建客户套接字
client.connect(('127.0.0.1',8080)) # 尝试连接服务器
client.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
client.close() # 关闭客户套接字
3.3 黏包
黏包现象
比如我们制作一个基于tcp的远程执行windows命令的程序。
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
我们执行一个输出内容可能很大的命令(比如tasklist),发现在执行之后的命令时,返回结果还是先前命令的结果;或者当快速发送多个命令并且客户端多次接收会发现结果整合成一次结果,这便是黏包现象。
关于黏包现象,由于tcp协议有1个特性:
当数据量比较小 且时间间隔比较短的多次数据,那么TCP会自动打包成一个数据包发送。
实际上黏包现象原因主要是接收方不知道数据的具体大小所导致的。
黏包解决方案
我们在发送数据之前,先发送一个报文通知对方即将发送数据的大小。
服务端
import socket,subprocess
server=socket.socket()
server.bind('127.0.0.1',8080)
server.listen(5)
while True:
conn,addr=server.accept()
while True:
msg=conn.recv(1024)
if not msg:break
res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
stdin=subprocess.PIPE,\
stderr=subprocess.PIPE,\
stdout=subprocess.PIPE)
err=res.stderr.read()
if err:
ret=err
else:
ret=res.stdout.read()
data_length=len(ret)
conn.send(str(data_length).encode('utf8'))
conn.send(ret)
conn.close()
客户端
import socket,time
client=socket.socket()
client.connect('127.0.0.1',8080)
while True:
msg=input('请输入执行命令 >>>: ').strip()
if len(msg) == 0:continue
if msg == 'quit':break
s.send(msg.encode('utf-8'))
length=int(s.recv(1024).decode('utf-8'))
data=s.recv(length)
print(data.decode('gbk'))
黏包进阶解决方案
我们可以借助struct模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
服务端
import socket
import subprocess
import json
import struct
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, address = server.accept()
while True:
data = sock.recv(1024) # 接收cmd命令
command_cmd = data.decode('utf8')
sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
res = sub.stdout.read() + sub.stderr.read() # 结果可能很大
# 1.制作报头
data_first = struct.pack('i', len(res))
# 2.发送报头
sock.send(data_first)
# 3.发送真实数据
sock.send(res)
客户端
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
msg = input('请输入cmd命令 >>>: ').strip()
if len(msg) == 0:
continue
client.send(msg.encode('utf8'))
# 1.先接收固定长度为4的报头数据
recv_first = client.recv(4)
# 2.解析报头
real_length = struct.unpack('i',recv_first)[0]
# 3.接收真实数据
real_data = client.recv(real_length)
print(real_data.decode('gbk'))
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struct将序列化后的数据长度打包成4个字节。
发送时:
-
先发报头长度
-
再编码报头内容然后发送
-
最后发真实内容
接收时:
-
先收报头长度,用struct取出来
-
根据取出的长度收取报头内容,然后解码,反序列化
-
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
import socket,struct,json
import subprocess
server=socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,addr=server.accept()
while True:
cmd=conn.recv(1024)
if not cmd:break
print('cmd: %s' %cmd)
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err=res.stderr.read()
print(err)
if err:
back_msg=err
else:
back_msg=res.stdout.read()
headers={'data_size':len(back_msg)}
head_json=json.dumps(headers)
head_json_bytes=bytes(head_json,encoding='utf-8')
conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
conn.send(head_json_bytes) #再发报头
conn.send(back_msg) #在发真实的内容
conn.close()
import socket
import struct,json
ip_port=('127.0.0.1',8080)
client=socket()
client.connect(ip_port)
while True:
cmd=input('请输入cmd命令 >>>: ')
if not cmd:continue
client.send(bytes(cmd,encoding='utf-8'))
head=client.recv(4)
head_json_len=struct.unpack('i',head)[0]
head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
data_len=head_json['data_size']
recv_data=client.recv(data_len)
print(recv_data.decode('gbk'))
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现