网络编程
1.C/S和B/S架构
1.1C/S架构
C指的是客户端 , S指的是服务端 , 然后你按照软件开发规范开发出客户端和服务端 , 让客户端和服务端相互之间能够通信 , 但是两个软件不能直接通信 , 要通过操作系统 , 然后系统控制网卡硬件发送出去 , 常见你安装的腾讯视频 , QQ ,WX都是C/S架构的软件
1.2B/S架构
B/S架构是一种特殊的C/S架构 , 其中的客户端被指定成了浏览器(Browser) , S依然是服务端 , 所有的web应用程序都是B/S架构 , 特殊之处 : 你不需要开发客户端了 , 因为客户端是浏览器 , 别人已经写好了 , 你只需要按照浏览器的规范开发出服务端即可
2.网络通信
我不管你是cs架构还是bs架构 , 那么都需要通过网络通信 , 那么什么是网络通信呢?
什么是网络
网络=物理连接介质(交换机,路由器等) , 这只是为通信做好了准备 , 还需要一个就是通信的标准
网络=物理连接介质+互联网通信协议 , 我们不需要研究物理介质 , 所以我们要把通信协议搞清楚
网络是干啥的
用来通信的
网络存在的意义
网络存在的意义就是跨地域数据传输=>称之为通信
3.osi七层协议
osi是一个国际标准组织 , 他将互联网协议分成了七层 , 分别是 应表会传网数物
, 有些人将应表会
和为一层 , 称之为应用层 , 然后全部称之为5层协议 , 也有人把数据链路层和物理层归为网络接口层 , 然后全部称之为4层协议 , 对于我们开发来说 , 划分5层学习就ok了
应 应用层
表 表示层
会 会话层
传 传输层
网 网络层
数 数据链路层
物 物理层
学习每一层的协议(组织数据的格式, 标准)处理数据的标准是什么
协议
: 规定数据的组织格式 , 格式 : 头部 + 数据部分
OSI七层协议数据传输的封包与解包过程
3.1物理层
一组物理层数据称之为 : 位
物理层由来:上面提到,孤立的计算机之间要想一起玩,就必须接入internet,言外之意就是计算机之间必须完成组网
物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
物理层最终发送出去都是0101010101这样的数据 , 接收方是无法知道发送方发送的是什么 , 因为单纯的01010没有开头和结尾我们怎么知道?不知道哪到哪是一段, 说白了这段数字一定会映射出数据的 , 那么就需要对这段0101001进行分组 , 这就要涉及到物理层之上的数据链路层 , 这一层有一个协议(以太网协议)可以分组
3.2数据链路层
ethernet以太网协议
ethernet规定
- 一组电信号构成一个数据包,叫做‘帧’
- 每一数据帧分成:报头head和数据data两部分
head包含:(固定18个字节)
- 发送者/源地址,6个字节 (mac地址)
- 接收者/目标地址,6个字节
- 数据类型,6个字节
data包含:(最短46字节,最长1500字节)
- 数据包的具体内容 , 即网络层发过来的整体内容
head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
mac地址
:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
注意 : 计算机通信基本靠吼 , 即以太网协议的工作方式是广播
光有以太网协议+广播的传输方式理论是可以实现世界上所有的计算机的通信 , 但是你要把全世界的计算机都连接在一台交换机上 , 显然这是不合理的 , 而且数据量也会特别大
3.3网络层
ip协议 , 划分广播域 每一个广播域但凡要接通外部 , 一定要有一个网关帮内部的计算机转发包到公网
网关与外界通信走的是路由协议
规定1 : 一组数据称之为一个数据包
规定2 : 包分为两部分 头部 + 数据
头部包含 : 原地址 + 目标地址 (ip地址) 长度为20到60字节
数据包含 : 传输层发过来的整体内容
补充 :
网络层由来:有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难 , 于是有了网络层 , 网络层解决了局域网与局域网之间的通信问题 , 在每个局域网中有一个网关 , 他有两个地址 , 一个是局域网地址 , 一个是连通公网(互联网的地址) , 当然不是直接连的 , 中间会有公网路由的转发 , 网关的通信走的是路由协议 , 跟osi七层协议没关系
IP地址
ip地址分成两部分
- 网络部分:标识子网
- 主机部分:标识主机
注意:单纯的ip地址段只是标识了ip地址的种类,从网络部分或主机部分都无法辨识一个ip所处的子网
例:172.16.10.1与172.16.10.2并不能确定二者处于同一子网
子网掩码
所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于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),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
比如,已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,
172.16.10.1:10101100.00010000.00001010.000000001
255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
172.16.10.2:10101100.00010000.00001010.000000010
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
结果都是172.16.10.0,因此它们在同一个子网络。
总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
ARP协议
arp协议即地址解析协议,是根据IP地址获取物理地址mac地址的一个协议
# 两台计算机在同一个局域网内
PC1 直接 PC2
ARP:
自己的ip , 对方的ip
1.计算二者网络地址 , 如果一样 , 拿到pc2的mac地址就可以了
2.发送广播包
发送端mac ff:ff:ff:ff:ff:ff 172.16.10.10/24 172.16.10.11/24 数据
# 两台计算机不在一个局域网内
PC1 网关 PC2
ARP:
自己的ip , 对方的ip
1.计算二者网络地址 , 如果不一样 , 应该拿到网关的mac地址就可以了
2.发送广播包
发送端mac ff:ff:ff:ff:ff:ff 172.16.10.10/24 172.16.10.1/24 数据
总结 : ip + mac 能够找到全世界上唯一的一台计算机 , 实际上ip地址就可以标识全世界的唯一的计算机(因为arp协议会帮助我们通过ip地址解析到mac地址) , 但是我们的计算机上会有很多的软件进行通信 , 那怎么区分是哪个软件呢???引出端口的概念
3.4传输层
tcp/udp协议=>基于端口工作 , 端口的范围 : 0-65535 , 其中0-1023为系统占用端口
ip+port => 标识全世界范围内独一无二的一个基于网络通信的应用
传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,等多个应用程序,
那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序与网卡关联的编号。
传输层功能:建立端口到端口的通信
规定1 : 一组数据称之为一个段 , 比如你下载电影 , 数据是一段一段传过去的 , 最终拼接在一起
规定2 : 包分为两部分 头部 + 数据
头部包含 : tcp头/udp头 , 头部的长度是固定的20字节
数据包含 : 应用层发过来的整体内容
基于tcp协议通信 : 必须建立一个双向通信的链接 , 建立链接的目的 : 是为了传输真正的数据做准备的 , 但是这个道路是双向发送是没有关系的 , 会有一种情况就是客户端断开了与服务端的链接 , 但是服务端可以不断开与客户端的链接 , 继续发送数据
三次握手
客户端执行connect方法时,会给服务端发送一个SYN报文请求连接,
然后服务端收到SYN报文之后,会发送一个SYN+ACK报文,表示允许连接,以及请求连接
客户端,客户端收到SYN+ACK报文之后,会回复一个ACK报文,服务端收到ACK报文之后,
这样三次握手就建立了,这个过程是由accept和connect共同完成的,具体代码没有体现
四次挥手
在server端和client端都有一个close方法,每一端发起一次close方法都是一次fin断开的请求
得到断开确认ack之后就可以结束一端的数据发送,如果两端都发起close,那么就是两次请求
和两次回复,一共四次操作,结束两端的数据发送就表示连接已经断开
为什么握手可以三次 , 挥手要四次呢? 因为建立连接是为了传输数据做准备的 , 中间是没有数据的 , 而断开链接可能同时还有数据在传输 , 所以那两步不同合并成一步 , 所以挥手是4步
tcp和udp的区别
tcp协议特点 : 可靠(重传机制) 慢 全双工 传递的数据长度几乎没有限制udp协议特点 : 快 不可靠 一次性传输的数据长度小tcp 文件的上传下载 发送邮件 网盘 缓存电影 转账udp 即时通讯 qq 微信 飞秋 在线观看电影
3.5应用层
应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式
应用层功能:规定应用程序的数据格式。
例:TCP协议可以为各种各样的程序传递数据,比如Email、http、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”
自定义协议需要主要的问题 :
-
两大组成部分 头部+数据
- 头部 : 放数据的描述信息 , 比如 : 数据要发给谁 , 数据的类型 , 数据的长度
- 数据 : 想要发送的数据
-
头部的长度必须固定 , 因为接收端要通过头部获取所接受数据的详细信息
补充
osi七层以及对应的物理设备:5层 应用层 python代码 HTTP/S SSH DNS Telnet FTP DHCP SMTP POP3 RDP SMB Mysql4层 传输层 tcp/udp协议 端口 四层路由器 四层交换机3层 网络层 ipv4/ipv6 三层路由器 三层交换机 ip icmp 2层 数据链路层 mac地址 arp协议 网卡 交换机 二层交换机1层 物理层 转换2进制
补充
DHCP协议 : 动态获取ip地址DNS协议 : 域名解析
4.socket
首先socket (套接字) 是工作在应用层和传输层之间一个抽像层 , 为什么要有他呢 ? 虽然我们已经有了ip+port可以和世界上任意一台计算机上的软件通信了 , 但是需要我们自己构造数据 , 以及封包 , 以及如何转换成2进制 . 相当麻烦 , 不利于开发 , 于是有了socket , 这个对数据封装的复杂工作交给他完成就好了 , 我们只需要调用相关接口就ok了 , 同样收数据也是基于socket层
所以我们无论用什么编程语言去开发网络通信的软件都不会自己封包解包 , 都是基于套接字的实现的 , 同样最后当应用层的数据传输结束了 , 你要在合适的地方用socket把系统资源给释放了
4.1套接字发展史及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
4.2套接字工作流程
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
4.3socket()模块函数用法
import socketsocket.socket(socket_family,socket_type,proto=0)socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protoc 一般不填,默认值为 0。 #获取tcp/ip套接字tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#获取udp/ip套接字udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。例如 :tcpSock = socket(AF_INET, SOCK_STREAM)
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字s.listen() 开始TCP监听s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据 不丢失,循环调用send直到发完)s.recvfrom() 接收UDP数据s.sendto() 发送UDP数据s.getpeername() 连接到当前套接字的远端的地址s.getsockname() 当前套接字的地址s.getsockopt() 返回指定套接字的参数s.setsockopt() 设置指定套接字的参数s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式s.settimeout() 设置阻塞套接字操作的超时时间s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符s.makefile() 创建一个与该套接字相关的文件
5.网络通信流程
1.本机获取
- 本机的IP地址:192.168.1.100
- 子网掩码:255.255.255.0
- 网关的IP地址:192.168.1.1
- DNS的IP地址:8.8.8.8
2.打开浏览器,想要访问Google,在地址栏输入了网址:www.google.com
3.dns协议(基于udp协议)
4.HTTP部分的内容,类似于下面这样:
GET / HTTP/1.1
Host: www.google.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1) ……
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3
Cookie: … …
我们假定这个部分的长度为4960字节,它会被嵌在TCP数据包之中。
5 TCP协议
TCP数据包需要设置端口,接收方(Google)的HTTP端口默认是80,发送方(本机)的端口是一个随机生成的1024-65535之间的整数,假定为51775。
TCP数据包的标头长度为20字节,加上嵌入HTTP的数据包,总长度变为4980字节。
6 IP协议
然后,TCP数据包再嵌入IP数据包。IP数据包需要设置双方的IP地址,这是已知的,发送方是192.168.1.100(本机),接收方是172.194.72.105(Google)。
IP数据包的标头长度为20字节,加上嵌入的TCP数据包,总长度变为5000字节。
7 以太网协议
最后,IP数据包嵌入以太网数据包。以太网数据包需要设置双方的MAC地址,发送方为本机的网卡MAC地址,接收方为网关192.168.1.1的MAC地址(通过ARP协议得到)。
以太网数据包的数据部分,最大长度为1500字节,而现在的IP数据包长度为5000字节。因此,IP数据包必须分割成四个包。因为每个包都有自己的IP标头(20字节),所以四个包的IP数据包的长度分别为1500、1500、1500、560。
8 服务器端响应
经过多个网关的转发,Google的服务器172.194.72.105,收到了这四个以太网数据包。
根据IP标头的序号,Google将四个包拼起来,取出完整的TCP数据包,然后读出里面的”HTTP请求”,接着做出”HTTP响应”,再用TCP协议发回来。
本机收到HTTP响应以后,就可以将网页显示出来,完成一次网络通信
6.TCP套接字
6.1单次通信
客户端
# 客户端import socket# 1.买手机sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络 , tcp协议,默认不写也是这个# 2.打电话,前提要知道对方的ip和portsock.connect(('127.0.0.1',8080))# 3.发送数据sock.send(b'hello word') # 发送的数据必须是bytes类型# 4.关闭sock.close()
服务端
# 服务端import socket# 1.买手机sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络 , tcp协议,默认不写也是这个# 2.绑定手机卡sock.bind(('127.0.0.1', 8080)) # 1024以前都被系统占用了# 3.开机sock.listen(5) # 半连接池允许的个数5# 4.等待连接请求print("等待客户端连接......")conn, client_addr = sock.accept()print("建立了一个管道: {}".format(conn))print('客户端的地址: {}'.format(client_addr))# 5.接收数据msg = conn.recv(1024) # 最大接收的数据量为1024个字节,收到的是bytes类型# 6.打印数据print(f"接收到的数据 : {msg.decode()}")# 7.关闭管道连接(必选,回收资源的操作)conn.close()# 8.关闭服务端sock对象(可选)sock.close()
注意 :
注意服务端代码中的bind绑定的地址 , 如果放在公网服务器上改成0.0.0.0 , 然后他自己内部找到对应的私网地址
6.2通信循环
客户端
# 客户端import socket# 1.买手机sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络 , tcp协议,默认不写也是这个# 2.打电话,前提要知道对方的ip和portsock.connect(('127.0.0.1', 8080))# 3.发送数据while 1: data = input('请输入你要发送的数据>>>').strip() if not data: continue sock.send(data.encode()) msg = sock.recv(1024).decode() print(msg)
服务端
# 服务端import socket# 1.买手机sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络 , tcp协议,默认不写也是这个# 2.绑定手机卡sock.bind(('127.0.0.1', 8080)) # 1024以前都被系统占用了# 3.开机sock.listen(5)# 4.等待连接请求print("等待客户端连接......")conn, client_addr = sock.accept()while 1: try: # 5.接收数据 msg = conn.recv(1024).decode() # 最大接收的数据量为1024个字节,收到的是bytes类型 if not msg: break # 如果msg为空,意味是一种异常的行为,客户端非法断开,此时应该断开链接 # 6.打印数据 print(f"接收到的数据 : {msg}") # 7.回复客户端数据 conn.send(msg.upper().encode()) except Exception: # 针对win breakconn.close()sock.close()
注意 :
先启动服务端客户端可以发送空 , 但是服务端这边是不能接收空的客户端的send和服务端的recv不是一 一对应的, 所有的send和recv都是和自己打交道, 还记得前面提到的那个应用是无法操作物理硬件的对吧,中间是通过操作系统, 那么你send也是, 操作系统会在缓存中拿到你应用send的数据, 然后交给网卡发送, 接收这边也是, 当应用中有recv操作时, 操作系统会到缓存中取数据, 那为什么发空会阻塞住呢?首先是send这边, 缓存中是空的, 操作系统取不到数据就溜溜球了, 但是接收方这边, 只要recv就会让操作系统去缓存哪里等着, 他不会因为当时没有数据, 就回来了, 他会一直在那里等着.所以阻塞了
6.3一直服务
对于服务端来说 , 最好就是一旦开启就要一直服务 , 从代码层面上来说就是只要有链接 , 就要建立
客户端
# 客户端import socket# 1.买手机sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络 , tcp协议,默认不写也是这个# 2.打电话,前提要知道对方的ip和portsock.connect(('127.0.0.1', 8080))# 3.发送数据while 1: data = input('请输入你要发送的数据>>>').strip() if not data: continue sock.send(data.encode()) msg = sock.recv(1024).decode() print(msg)
服务端
# 服务端# 1.一直提供服务# 2.提供并发的服务import socket# 1.买手机sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络 , tcp协议,默认不写也是这个# 2.绑定手机卡sock.bind(('127.0.0.1', 8080)) # 1024以前都被系统占用了# 3.开机sock.listen(5)# 4.等待连接请求print("等待客户端连接......")# 半链接循环while 1: conn, client_addr = sock.accept() while 1: try: # 5.接收数据 msg = conn.recv(1024).decode() # 最大接收的数据量为1024个字节,收到的是bytes类型 if not msg: break # 如果msg为空,意味是一种异常的行为,客户端非法断开,此时应该断开链接 # 6.打印数据 print(f"接收到的数据 : {msg}") # 7.回复客户端数据 conn.send(msg.upper().encode()) except Exception: # 针对win break # 断开链接 conn.close()
7.UDP套接字
udp是无链接的,先启动哪一端都不会报错
客户端
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 基于网络,udpsock.sendto(b"hello world", ('127.0.0.1', 8080)) # 可以发空sock.close()
服务端
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 基于网络,udpsock.bind(('127.0.0.1', 8080))data, addr = sock.recvfrom(1024) # 因为udp没有链接,所以对方的addr很关键,代表着你要回给谁消息print(data.decode())sock.close()
补充 :
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一 一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠udp协议一次最大传输的有效数据是512个字节
8.粘包
从现象中发现问题 , 让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig)
注意注意注意:
res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE,stdout=subprocess.PIPE)的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码 , 且只能从管道里读一次结果
注意:命令ls -l ; lllllll ; pwd 的结果是既有正确stdout结果,又有错误stderr结果
客户端
import socketBUFSIZE=1024ip_port=('127.0.0.1',8080)s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)res=s.connect_ex(ip_port)while True: msg=input('>>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) act_res=s.recv(BUFSIZE) print(act_res.decode('utf-8'),end='')
服务端
from socket import *import subprocessip_port=('127.0.0.1',8080)BUFSIZE=1024tcp_socket_server=socket(AF_INET,SOCK_STREAM)tcp_socket_server.bind(ip_port)tcp_socket_server.listen(5)while True: conn,addr=tcp_socket_server.accept() print('客户端',addr) while True: cmd=conn.recv(BUFSIZE) if len(cmd) == 0:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr=act_res.stderr.read() stdout=act_res.stdout.read() conn.send(stderr) conn.send(stdout)
当你运行ipconfig
命令后 , 就会发生粘包现象 , 即当前命令的回显不完全 , 下次命令的回显出现错乱
8.1什么是粘包
须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
8.1粘包问题出现的原因
- tcp是流式协议, 数据像水流一样粘在一起, 没有任何边界区分
- 收数据没收干净, 有残留, 就会下一次结果混淆在一起
8.2两种情况下会发生粘包
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
客户端
import socketBUFSIZE=1024ip_port=('127.0.0.1',8080)s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)res=s.connect_ex(ip_port)s.send('hello'.encode('utf-8'))s.send('feng'.encode('utf-8'))
服务端
from socket import *ip_port=('127.0.0.1',8080)tcp_socket_server=socket(AF_INET,SOCK_STREAM)tcp_socket_server.bind(ip_port)tcp_socket_server.listen(5)conn,addr=tcp_socket_server.accept()data1=conn.recv(10)data2=conn.recv(10)print('----->',data1.decode('utf-8'))print('----->',data2.decode('utf-8'))conn.close()
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
客户端
import socketBUFSIZE=1024ip_port=('127.0.0.1',8080)s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)res=s.connect_ex(ip_port)s.send('hello feng'.encode('utf-8'))
服务端
from socket import *ip_port=('127.0.0.1',8080)tcp_socket_server=socket(AF_INET,SOCK_STREAM)tcp_socket_server.bind(ip_port)tcp_socket_server.listen(5)conn,addr=tcp_socket_server.accept()data1=conn.recv(2) #一次没有收完整data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的print('----->',data1.decode('utf-8'))print('----->',data2.decode('utf-8'))conn.close()
8.3解决粘包的思路
-
拿到数据的总大小total_size
-
recv_size=0, 循环接收, 每接收一次, recv+=接收的长度 ,
直到recv_size = total_size , 结束循环
解决的核心法门就是 : 每次都收干净, 不要任何残留
基于我们的思路对代码进行修改 , 这里需要借助一个struct
模块帮助我们解决问题 , 先了解一下struct模块有哪些功能 , 我们主要应用的是其中可以把数字转换成一个固定长度的字节 , 这样接收方就能按照数字接收该接收的数据大小了 . 为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据 (这个操作就是一个简单版的自定义协议 , 突然想到解决了就加一层
现在又多了一个自定义协议 , 哈哈哈哈哈)
struct.pack('i',1111111111111)11212struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
客户端修改版
import socket,time,structs=socket.socket(socket.AF_INET,socket.SOCK_STREAM)res=s.connect_ex(('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')) l=s.recv(4) x=struct.unpack('i',l)[0] # print(struct.unpack('I',l)) r_s=0 data=b'' while r_s < x: r_d=s.recv(1024) data+=r_d r_s+=len(r_d) # print(data.decode('utf-8')) print(data.decode('gbk')) #windows默认gbk编码
服务端修改版
import socket,struct,jsonimport subprocessphone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8080))phone.listen(5)while True: conn,addr=phone.accept() while True: try: 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) suc=res.stdout.read() err=res.stderr.read() total_size = len(suc) + len(err) # 先发送头信息 conn.send(struct.pack('i',total_size) #先发back_msg的长度 # 在发真实的内容 conn.send(suc) conn.send(err) except Exception: break conn.close()
根据上面解决粘包 + 自定义协议 , 编写一个服务端与客户端之间可以实现文件上传下载
客户端
import socketimport structimport jsonimport osclass MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp=input(">>: ").strip() if not inp:continue l=inp.split() cmd=l[0] if hasattr(self,cmd): func=getattr(self,cmd) func(l) def put(self,args): cmd=args[0] filename=args[1] if not os.path.isfile(filename): print('file:%s is not exists' %filename) return else: filesize=os.path.getsize(filename) head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} print(head_dic) head_json=json.dumps(head_dic) head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size=0 with open(filename,'rb') as f: for line in f: self.socket.send(line) send_size+=len(line) print(send_size) else: print('upload successful')client=MYTCPClient(('127.0.0.1',8080))client.run()
服务端
import socketimport structimport jsonimport subprocessimport osclass MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 server_dir='file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn,self.client_addr=self.get_request() print('from client ',self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct:break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) except Exception: break def put(self,args): file_path=os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize=args['filesize'] recv_size=0 print('----->',file_path) with open(file_path,'wb') as f: while recv_size < filesize: recv_data=self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size+=len(recv_data) print('recvsize:%s filesize:%s' %(recv_size,filesize))tcpserver1=MYTCPServer(('127.0.0.1',8080))tcpserver1.run()#下列代码与本题无关class MYUDPServer: """UDP server class.""" address_family = socket.AF_INET socket_type = socket.SOCK_DGRAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' def get_request(self): data, client_addr = self.socket.recvfrom(self.max_packet_size) return (data, self.socket), client_addr def server_activate(self): # No need to call listen() for UDP. pass def shutdown_request(self, request): # No need to shutdown anything. self.close_request(request) def close_request(self, request): # No need to close anything. pass
9.socketserver模块
9.1基于TCP协议的并发
客户端
# 客户端import socketsock = socket.socket()sock.connect(('127.0.0.1', 8080))while 1: msg = input('输入>>>').strip().encode() if msg == b'q': break sock.send(msg) recv_msg = sock.recv(1024) print(recv_msg.decode())sock.close()
服务端
import socketserver# 必须新建一个类,而且要继承socketserver.BaseRequestHandlerclass MyRequestHandler(socketserver.BaseRequestHandler): # 必须要重写handle方法 def handle(self): print('等待连接....') print(self.request) # self.request相当于管道(conn) print(self.client_address) # self.client_address客户端地址 while 1: try: msg = self.request.recv(1024) if not msg: break self.request.send(msg.upper()) except Exception: break self.request.close()# 服务端应做两件事# 1.循环的从半连接池中取出链接请求与其建立连接,s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandler)s.serve_forever()# 等同于# while 1:# conn,addr = socket.accept()# 启动一个线程(conn,addr)# 2.拿到连接对象,与其建立通信循环===>handle# s.serve_forever(),每建立其一个连接,就把线程(conn,addr)的信息封装到一个类(MyRequestHandler)里面,注意不是# 直接传参,每一个线程工作的时候触发的都是对象下的handle方法,线程应该做的事情是通信循环
建立TCP/socketserver的步骤如下:
- 导入socketserver模块
- 创建一个新的类,并继承socketserver.BaseRequestHandler,重写其handle()方法,用于处理TCP请求
- 写入交互逻辑
- 防止客户端发送空信息以致双方卡死(针对Unix平台Client端异常关闭)
- 防止客户端突然断开服务端抛出的ConnectionResetError异常(针对Windows平台Client端异常关闭)
- 实例化socketserver.ThreadingTCPServer类,并传入自定义处理TCP请求的类和绑定ip+port
- 调用socketserver.ThreadingTCPServer实例对象下的serve_forever()方法,启动服务
注意 :
基于tcp的socketserver一定要捕捉异常 , 然后结束连接 , 如果你不这样做线程(小弟)就会一直存在内存中socketserver模块实现的TCP服务器并不会提供粘包优化,所以需要自己手动实现。
9.2基于UDP协议的并发
客户端
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 基于网络,udpwhile 1: cmd = input(">>>").strip() sock.sendto(cmd.encode(), ('127.0.0.1', 8080)) # 可以发空 msg = sock.recvfrom(1024)[0].decode() print(msg)sock.close()
服务端
import socketserverclass Server(socketserver.BaseRequestHandler): def handle(self) -> None: # self.request == (message, server) # self.client_address = clientAddr data = self.request[0] server = self.request[1] print("receive client data : %s" % data.decode("u8")) server.sendto(data.upper(), self.client_address) if __name__ == "__main__": server = socketserver.ThreadingUDPServer( server_address=("127.0.0.1", 8080), RequestHandlerClass=Server ) # run server server.serve_forever()
self.request和TCP的self.request不同,它不是双向链接通道conn,而是包含了信息与服务端本身
self.client_address就是Client端的地址和端口信息
9.3基于TCP的并发解决粘包版
客户端
import socketimport structclient = socket.socket()client.connect(('127.0.0.1', 8080))while 1: cmd = input('请输入命令>>>').strip() if not cmd: continue client.send(cmd.encode()) # 先接受命令执行结果的长度 x = client.recv(4) total_size = struct.unpack('i', x)[0] # 接收真实数据 recv_size = 0 recv_data = b'' BUF_SIZE = 1024 while recv_size < total_size: recv_data += client.recv(BUF_SIZE) recv_size += BUF_SIZE print(recv_data.decode('gbk'))
服务端
import socketserver# 必须新建一个类,而且要继承socketserver.BaseRequestHandlerclass MyRequestHandler(socketserver.BaseRequestHandler): # 必须要重写handle方法 def handle(self): print('等待连接....') #print(self.request) # self.request相当于管道(conn) #print(self.client_address) # self.client_address客户端地址 while 1: try: cmd = self.request.recv(1024) if not self.request: break obj = subprocess.Popen(cmd.decode(), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout_res = obj.stdout.read() stderr_res = obj.stderr.read() # time.sleep(2) total_size = len(stdout_res) + len(stderr_res) # 先发长度 res_len = struct.pack('i', total_size) self.request.send(res_len) # 发送内容 self.request.send(stdout_res) self.request.send(stderr_res) except Exception: break self.request.close()# 服务端应做两件事# 1.循环的从半连接池中取出链接请求与其建立连接,s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandler)s.serve_forever()