6.网络编程
大约 36 分钟约 10744 字
6.网络编程
10.1 cs 架构与 bs 架构
## cs架构
## Client ----------------------------------------- Server
# 客服端软件 <==============================> 服务端软件
# 操作系统<===================================> 操作系统
# 计算机硬件 <================================> 计算机硬件
## bs架构
## Brower ----------------------------------------------- Server10.2 网络通信
## 网络存在的意义就是跨地域数据传输---------》 称之为通信
## 网络 = 物理链接介质 + 互联网通信协议10.3 OSI 七层协议
## 五层协议
# 应用层
# 传输层
# 网络层
# 数据链路层
# 物理层
## 协议:规定数据的组织格式 格式:头部 + 数据部分10.4 网络协议
## 计算机1 ----------------------------计算机2、
## 应用层 ------------------------------应用层
## 传输层 ----------------------------- 传输层
## 网络层 ----------------------------- 网络层 ------> (源IP地址,目标IP地址) 数据1
## 数据链路层 --------------------------数据链路层 --->(源mac地址,目标mac地址) 数据2((源IP地址,目标IP地址) 数据1)
## 物理层1 ------------二层交互机------------物理层2
# 二层交互机将从 物理层1接受的 二进制数据接收到以后可以解析到数据链路层(源mac地址,目标mac地址) 数据2 再转换成二进制 发给物理层210.4.1 物理层
## 物理层由来:上面提到,孤立的计算机之间要想一起玩,就必须接入internet,言外之意就是计算机之间必须完成组网
## 物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
## 物理层负责发送电信号
## 单纯的电信号毫无意义,必须对其进行分组
10.4.2 数据链路层(帧)
## 数据链路层: Ethernet以太网协议
## 规定一:一组数据称之为一个数据帧
## 规定二:数据帧分成两部分 头 + 数据两部分
## 头: 源地址与目标地址,该地址是Mac地址
## 数据:包含的是网络层整体的内容
## 规定三:但凡介入互联网的主机,必须有网卡,每一块网卡在出场时标志好一个全世界独一无二的地址该地址称之为-----> mac地址
## mac地址:(了解)
'''
head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
'''
## head包含:(固定18个字节) ----------------- 源地址与目标地址,该地址是Mac地址
'''
发送者/源地址,6个字节
接收者/目标地址,6个字节
数据类型,6个字节
data包含:(最短46字节,最长1500字节)
'''
## 数据包的具体内容
## head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
## 注意:计算机通信基本靠吼,既以太网协议的工作方式是广播10.4.3 网络层(包)

## 网络层:IP协议
## 划分IP协议
## 每一个广播域但凡要接通外部,一定要有一个网关帮内部的计算机转发包到公网网关与外界通信走的是路由协议
## 规定1:一组数据称之为一个数据包
## 规定2: 数据帧分成两个部分----> 头 + 数据
## 头包含:源地址与目标地址,该地址是IP地址
## 数据包含:传输层整体的内容3-1 ip 协议
# IP协议:
'''
1,规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
2,范围0.0.0.0-255.255.255.255
3,一个ip地址通常写成四段十进制数,例:172.16.10.1
'''
## ip地址分成两部分
# 网络部分:标识子网
# 主机部分:标识主机
# 注意:单纯的ip地址段只是标识了ip地址的种类,从网络部分或主机部分都无法辨识一个ip所处的子网
# 例:172.16.10.1与172.16.10.2并不能确定二者处于同一子网
#ipv4地址:
# 8bit.8bit.8bit.8bit
0.0.0.0 ~ 255.255.255.255
## ipv6
## 目前在逐渐普及3-2 子网掩码
## 子网掩码
# 8bit.8bit.8bit.8bit
## 一个合法的IPv4地址组成部分=ip地址/子网掩码 ---------------------> 区分广播域
## 172.16.10.1/255.255.255.0
## ## 172.16.10.1/24 -----------------> 表示24位二进制数
'''
知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都 为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
'''
## 同为1结果为1,有0结果为0
## 计算机1的
## IP地址
172.16.10.1: 10101100.00010000.00001010.000000001
## 子网掩码地址
255255.255.255.0: 11111111.11111111.11111111.00000000
## 网络地址
10110101100.00010000.00001010.000000001->172.16.10.0
## 计算机2的
## IP地址
172.16.10.2: 10101100.00010000.00001010.000000010
## 子网掩码地址
255255.255.255.0: 11111111.11111111.11111111.00000000
## 网络地址
10101100.00010000.00001010.000000001->172.16.10.0
## 两个计算机网络地址一样,所以属于一个局域网内3-3 APR 协议
## 事先知道的是对方的IP地址
## 但是计算机的底层通信是基于ethernet以太网协议的mac地址通信
##API协议 -----------> 能够将IP地址解析成mac地址
## 两台计算机再同一个局域网内,直接发包就可以
计算计1 直接 计算机2
ARP:
自己的IP,对方的IP
#-1 计算二者的网络地址,如果一样,那ARP协议拿到计算机2的mac地址就可以
#-2 发送广播包
## 两台计算机不在同一个局域网内
计算计1 网关 计算机2
ARP:
自己的IP,对方的IP
# 计算二者的网络地址,如果不一样,应该拿到网关的mac地址
## FF:FF:FF:FF:FF:FF ---------------------->意思就是要对方的Mac地址
##-1 如果在同一个局域网内,那就拿到了对方的Mac地址
##-2 发送广播包

3-4 总结性知识
## Mac地址标识的是局域网内的一台机器
## IP地址 + Mac地址 -------------------> 可以标识全世界范围内独一无二的一台计算机
## IP+ Mac + port -----------------------> 可以找到全世界范围内独一无二的应用程序
## 或者
## IP地址 ----------> 可以标识全世界范围内独一无二的一台计算机
##10.4.4 传输层(段)
## 传输层功能:建立端口到端口的通信
## 补充:端口范围0-65535,0-1023为系统占用端口
## 自定义协议需要注意的问题:
#-1 两大组成部分= 头部 + 数据部分
# 头部:放对数据的描述信息
# 比如:数据要发给谁,数据的类型,数据的长度
# 数据部分:想要发的数据
#-2 头部的长度必须固定
# 因为接受端要通过头部获取所接收数据的详细信息4-1 tcp 协议
# Ethernet头 + IP头 + TCP头 + 应用层的头 + 应用层数据
## TCP头部:源端口,目标端口,....... (20字节)
## TCP协议工作方式:建立一个双向通信的链接
C -------------------------------> S 客户端向服务端发数据
C <-------------------------------> S 服务端向客户端发请求
## 三次握手建立链接------------为传数据做准备
C S
| -------------------------------------> | 第一次:发起跟服务端的链接请求
| |
|<---------------------------------------| 第二次:同意客户端的请求,并向客户端发链接请求
| |
|--------------------------------------->| 第三次:同意服务端同意
| |
## 四次握手断开链接-----------------由于断开链接时,由于链接内有数据传输,所以必须分四次断开
## tcp 发数据是可靠的(效率不高) ---------> 因为客户端发数据给服务端,服务端有个确认信息,客户端才会把内存数据清理掉
## 若果客户端发数据到服务端,服务端没回应,客户端会重发一份给服务端,反过来也是
## 发送数据必须等到对方确认后才算完成,才会将自己内存中的数据清理掉
## 当服务端大量处于TIME_WAIT状态时意味着服务端正在经历高并发4-2 UDP 协议
## 为了提高传输效率,可以使用UDP协议,发数据不需要确认,发完就清理数据,不需要对方确认--------------但是不可靠10 -------- scoket 抽象层(套接字)

10.4.5 应用层
## 应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式
## 应用层功能:规定应用程序的数据格式。
## 例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
10.4.6 总结


10.5 网络通信实现
## 想实现网络通信,每台主机需具备四要素
## 本机的IP地址
## 子网掩码
## 网关的IP地址
## DNS的IP地址
## 获取这四要素分两种方式10.5.1 DHCP 协议(计算机获取自己的 IP,子网掩码等等信息)
#获取这四要素分两种方式
# 1.静态获取
# 即手动配置
# 2.动态获取
#通过dhcp获取#(1)最前面的”以太网标头”,设置发出方(本机)的MAC地址和接收方(DHCP服务器)的MAC地址。前者就是本机网卡的MAC地址,后者这时不知道,就填入一个广播地址:FF-FF-FF-FF-FF-FF。
#(2)后面的”IP标头”,设置发出方的IP地址和接收方的IP地址。这时,对于这两者,本机都不知道。于是,发出方的IP地址就设为0.0.0.0,接收方的IP地址设为255.255.255.255。
#(3)最后的”UDP标头”,设置发出方的端口和接收方的端口。这一部分是DHCP协议规定好的,发出方是68端口,接收方是67端口。
# 这个数据包构造完成后,就可以发出了。以太网是广播发送,同一个子网络的每台计算机都收到了这个包。因为接收方的MAC地址是FF-FF-FF-FF-FF-FF,看不出是发给谁的,所以每台收到这个包的计算机,还必须分析这个包的IP地址,才能确定是不是发给自己的。当看到发出方IP地址是0.0.0.0,接收方是255.255.255.255,于是DHCP服务器知道”这个包是发给我的”,而其他计算机就可以丢弃这个包。
# 接下来,DHCP服务器读出这个包的数据内容,分配好IP地址,发送回去一个”DHCP响应”数据包。这个响应包的结构也是类似的,以太网标头的MAC地址是双方的网卡地址,IP标头的IP地址是DHCP服务器的IP地址(发出方)和255.255.255.255(接收方),UDP标头的端口是67(发出方)和68(接收方),分配给请求端的IP地址和本网络的具体参数则包含在Data部分。
# 新加入的计算机收到这个响应包,于是就知道了自己的IP地址、子网掩码、网关地址、DNS服务器等等参数10.5.2 DNS 域名解析
# DNS的作用:在互联网中,其实没有类似于www.xxx.com这种域名方式,而替代的是以IP地址,如222.222.222.222,那我们在IE地址栏中应当输入222.222.222.222才能打开网站www.xxx.com,但我们细想一下,互联网上的网站成千上万,如果每个网站登陆都需要记住一大串数字,那是不是特别不方便,对于记忆力不强的人,根本无法记住这么烦琐的数字。这个时候DNS就出现了,它的作用就是将222.222.222.222解析为www.xxx.com,那么我们登陆的时候就直接输入域名就可以了。
# 为什么一定要设置DNS才能上网?有些朋友可能会发现,为什么我可能登陆QQ、MSN,但却打不开网页呢?其实大部分原因都是因为DNS服务器故障造成的,DNS服务器地址是唯一的,是运营商提供给终端用户用来解析IP地址及域名的关系,而如果不设定DNS服务器地址,那么就无法查询地址的去向,自然也就打不开网页,而QQ、MSN等即时聊天软件,采用的是UDP传输协议,即不可靠传输协议,无需提供DNS服务器地址,也同样可以登陆。
## DNS 查询域名用 UDP协议10.5.3 dns 的两种查询方式
#一 :递归
##主机向本地域名服务器的查询一般都是采用递归查询。所谓递归查询就是:如果主机所询问的本地域名服务器不知道被查询的域名的IP地址,那么本地域名服务器就以DNS客户的身份,向其它根域名服务器继续发出查询请求报文(即替主机继续查询),而不是让主机自己进行下一步查询。因此,递归查询返回的查询结果或者是所要查询的IP地址,或者是报错,表示无法查询到所需的IP地址。
# 二:迭代
## 本地域名服务器向根域名服务器的查询的迭代查询。迭代查询的特点:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”。然后让本地服务器进行后续的查询。根域名服务器通常是把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询。顶级域名服务器在收到本地域名服务器的查询请求后,要么给出所要查询的IP地址,要么告诉本地服务器下一步应当向哪一个权限域名服务器进行查询。最后,知道了所要解析的IP地址或报错,然后把这个结果返回给发起查询的主机。

10.5.4 域名解析例子
#下面举一个例子演示整个查询过程:
'''
假定域名为m.xyz.com的主机想知道另一个主机y.abc.com的IP地址。例如,主机m.xyz.com打算发送邮件给y.abc.com。这时就必须知道主机y.abc.com的IP地址。下面是图2的几个查询步骤:
1、主机m.abc.com先向本地服务器dns.xyz.com进行递归查询。
2、本地服务器采用迭代查询。它先向一个根域名服务器查询。
3、根域名服务器告诉本地服务器,下一次应查询的顶级域名服务器dns.com的IP地址。
4、本地域名服务器向顶级域名服务器dns.com进行查询。
5、顶级域名服务器dns.com告诉本地域名服务器,下一步应查询的权限服务器dns.abc.com的IP地址。
6、本地域名服务器向权限域名服务器dns.abc.com进行查询。
7、权限域名服务器dns.abc.com告诉本地域名服务器,所查询的主机的IP地址。
8、本地域名服务器最后把查询结果告诉m.xyz.com。
# 整个查询过程共用到了8个UDP报文。
为了提高DNS查询效率,并减轻服务器的负荷和减少因特网上的DNS查询报文数量,在域名服务器中广泛使用了高速缓存,用来存放最近查询过的域名以及从何处获得域名映射信息的记录。
例如,在上面的查询过程中,如果在m.xyz.com的主机上不久前已经有用户查询过y.abc.com的IP地址,那么本地域名服务器就不必向根域名服务器重新查询y.abc.com的IP地址,而是直接把告诉缓存中存放的上次查询结果(即y.abc.com的IP地址)告诉用户。
由于名字到地址的绑定并不经常改变,为保持告诉缓存中的内容正确,域名服务器应为每项内容设置计时器并处理超过合理时间的项(例如每个项目两天)。当域名服务器已从缓存中删去某项信息后又被请求查询该项信息,就必须重新到授权管理该项的域名服务器绑定信息。当权限服务器回答一个查询请求时,在响应中都指明绑定有效存在的时间值。增加此时间值可减少网络开销,而减少此时间值可提高域名解析的正确性。
不仅在本地域名服务器中需要高速缓存,在主机中也需要。许多主机在启动时从本地服务器下载名字和地址的全部数据库,维护存放自己最近使用的域名的高速缓存,并且只在从缓存中找不到名字时才使用域名服务器。维护本地域名服务器数据库的主机应当定期地检查域名服务器以获取新的映射信息,而且主机必须从缓存中删除无效的项。由于域名改动并不频繁,大多数网点不需花精力就能维护数据库的一致性。
'''
10.5.5 DNS 解析流程举例

'''
如上图所示,我们将详细阐述DNS解析流程。
1、首先客户端位置是一台电脑或手机,在打开浏览器以后,比如输入http://www.zdns.cn的域名,它首先是由浏览器发起一个DNS解析请求,
如果本地缓存服务器中找不到结果,则首先会向根服务器查询,根服务器里面记录的都是各个顶级域所在的服务器的位置,当向根请求http://www.zdns.cn的时候,
根服务器就会返回.cn服务器的位置信息。
2、递归服务器拿到.cn的权威服务器地址以后,就会寻问cn的权威服务器,知不知道http://www.zdns.cn的位置。这个时候cn权威服务器查找并返回http://zdns.cn服务器的地址。
3、继续向http://zdns.cn的权威服务器去查询这个地址,由http://zdns.cn的服务器给出了地址:202.173.11.10
4、最终才能进行http的链接,顺利访问网站。
5、这里补充说明,一旦递归服务器拿到解析记录以后,就会在本地进行缓存,如果下次客户端再请求本地的递归域名服务器相同域名的时候,就不会再这样一层一层查了,
因为本地服务器里面已经有缓存了,这个时候就直接把http://www.zdns.cn的A记录返回给客户端就可以了。
'''10.5.6 DNS 缓存及分类
# DNS缓存指DNS返回了正确的IP之后,系统就会将这个结果临时储存起来。并且它会为缓存设定一个失效时间 (例如N小时),在这N小时之内,当你再次访问这个网站时,系统就会直接从你电脑本地的DNS缓存中把结果交还给你,而不必再去询问DNS服务器,变相“加速”了网址的解析。
# 当然,在超过N小时之后,系统会自动再次去询问DNS服务器获得新的结果。所以,当你修改了 DNS 服务器,并且不希望电脑继续使用之前的DNS缓存时,就需要手动去清除本地的缓存了。
## 分类
'''
1)浏览器DNS缓存(内存中): 浏览器会按照一定频率缓存DNS记录
2)本地操作系统DNS缓存(内存中): 如果浏览器缓存中找不到需要的DNS记录,那就去操作系统找。
3)本地HOSTS文件(硬盘中): Windows系统中位于C:\Windows\System32\drivers\etc
4)路由器指定的DNS(远程): 路由器自动获取DNS地址,也可以手动修改-登录后台设置DNS服务器地址
ps:路由器DNS被篡改会造成域名劫持,你访问的网址都会被定位到同一个位置,但是IP直接可以访问
5)ISP的DNS服务器(远程): ISP(Internet Service Provider互联网服务提供商、联通电信移动),ISP有专门的DNS服务器应 对DNS查询请求
6)根服务器(远程,跨国): ISP的DNS服务器还找不到的话,它就会向根服务器发出查询请求
'''10.5.7 浏览器 DNS 查找顺序

## 浏览器DNS缓存->本地系统DNS缓存->本地计算机HOSTS文件->ISP DNS缓存->递归or迭代搜索
## 期间如果查询到了,也就直接访问ip地址了,这个就像三级缓存原理一样,例如,能够在hosts文件中找到就不会再去查其他的10.5.8 清除 DNS 缓存
##打开cmd执行命令:ipconfig /all
## 全国通用DNS地址(国内用户推荐使用,速度较快!)
## 首先DNS服务器地址添:114.114.114.114 (位于北京人民英雄纪念碑)
## 全球通用DNS地址(此DNS地址为谷歌服务器的)
## 首选DNS服务器地址添:8.8.8.8
## 备用DNS服务器地址添:8.8.4.4
# 查看本地dns缓存命令:ipconfig /displaydns
# 清除本地dns缓存命令:ipconfig /flushdns
# 清除浏览器缓存:
# 我们在开发的时候,有时候会给某个域名绑hosts,用于本地开发测试,但是绑了之后,用谷歌浏览器访问会发现并没有生效,按F12会 发现访问的还是线上的ip,说明浏览器是有该域名的dns缓存的,那么如何清除浏览器的dns缓存呢?
# 1、针对谷歌浏览器
#谷歌浏览器清除方法如下:打开浏览器,访问如下地址 chrome://net-internals/#dns
# 点击 clear host cache,就清楚了浏览器的dns缓存,再访问绑hosts的域名,就会发现ip变啦
# 2、针对火狐浏览器
# 如果是firefox火狐浏览器的话,可以按照以下方式:
# 在地址栏中 about:config 并回车,可能会出现一个警告信息,直接点击按钮进入,会出现firefox的所有配置信息,通过搜索dns 进 行过滤,
# 可以看到一项名为 network.dnsCacheExpirationGracePeriod 项,它对应的值就是DNS缓存的时间,双击此项,会出现修改的提示 框,填入 010.6 网络通信流程
## 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: … …
'''
## 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响应以后,就可以将网页显示出来,完成一次网络通信。
'''10.7 socket
10.7.1 套接字是什么
## Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
## 所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
## 也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
## 而程序的pid是同一台机器上不同进程或者线程的标识10.7.2 套接字工作流程
'''
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。
'''
## 先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束10.7.3 socket()模块函数用法
3-1 基础用法
### 服务端.py
import socket
## 以打电话为例
# 1,买手机
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
## 2,绑定手机卡
phone.bind(('127.0.0.1',8080))
## 3,开机
phone.listen(5) ## 5 值得是半连接池的大小
print(f'服务端启动,服务运行在127.0.0.1:8080')
## 4,等待电话连接请求,拿到电话连接conn
conn,alient_addr = phone.accept() ## 是个元祖
print(conn) ## 套接字对象
print('客户端的IP和端口',alient_addr) ## 客户端的IP和端口 ('127.0.0.1', 57424)
## 5,通信:收消息/发消息
data = conn.recv(1024) ## 1024 指最大接受的数据量为1024Bytes,收到的是Bytes类型
print('客户端发来的消息:',data.decode('utf-8'))
conn.send(data.upper())
## 6,关闭电话连接(一个电话连接结束了就应该断掉)
conn.close() ## 挂断电话
## 7,可选:关机手机
phone.close()
### 客户端.py
import socket
## 以打电话为例
# 1,买手机
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
## 2,拨通服务端电话
## 服务端的IP和端口
phone.connect(('127.0.0.1',8080)) ##
## 3.通信
phone.send('hello aini 哈哈哈哈'.encode('utf-8')) ## 发送的是bytes类型的
data = phone.recv(1024)
print(data.decode('utf-8'))
## 4,关闭连接(必选)
phone.close()3-2 通信循环
## 服务端.py
import socket
## 以打电话为例
# 1,买手机
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
## 2,绑定手机卡
phone.bind(('127.0.0.1',8080))
## 3,开机
phone.listen(5) ## 5 值得是半连接池的大小
print(f'服务端启动,服务运行在127.0.0.1:8080')
## 4,等待电话连接请求,拿到电话连接conn
conn,alient_addr = phone.accept() ## 是个元祖
print(conn) ## 套接字对象
print('客户端的IP和端口',alient_addr) ## 客户端的IP和端口 ('127.0.0.1', 57424)
## 5,通信:收消息/发消息
while True:
data = conn.recv(1024) ## 1024 指最大接受的数据量为1024Bytes,收到的是Bytes类型
print('客户端发来的消息:',data.decode('utf-8'))
conn.send(data.upper())
## 6,关闭电话连接(一个电话连接结束了就应该断掉)
conn.close() ## 挂断电话
## 7,可选:关机手机
phone.close()
## 客户端.py
import socket
## 以打电话为例
# 1,买手机
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
## 2,拨通服务端电话
## 服务端的IP和端口
phone.connect(('127.0.0.1',8080)) ##
## 3.通信
while True:
user_input = input(">>>>>:").strip()
if user_input == 'quit': break
phone.send(user_input.encode('utf-8')) ## 发送的是bytes类型的
data = phone.recv(1024)
print(data.decode('utf-8'))
## 4,关闭连接(必选)
phone.close()3-3 socket 收发消息的原理
## 客户端收发 和 服务端收发是独立的,并不是一次发对应一次收
## 客户端sned都是数据交给缓存,让操作系统调度网卡发,服务端发数据也是,而不是客户端和服务端直接一对一的传输3-4 修复 bug1 (空格-阻塞)
## bug 原因:输入空格以后,input时被去掉空格了,等于user_input = ''
## 客户端可以send的空容缓存,再由网卡发送,由于空数据,等于没数据,所以不会发空数据的
## 修正: 判断user_input 的长度
## 客户端.py
import socket
## 以打电话为例
# 1,买手机
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
## 2,拨通服务端电话
## 服务端的IP和端口
phone.connect(('127.0.0.1',8080)) ##
## 3.通信
while True:
user_input = input(">>>>>:").strip()
if user_input == 'quit': break
if len(user_input) == 0: continue
phone.send(user_input.encode('utf-8')) ## 发送的是bytes类型的
data = phone.recv(1024)
print(data.decode('utf-8'))
## 4,关闭连接(必选)
phone.close()3-5 bug2(客户端非正常死亡------导致服务端也异常)
### 客户端直接关机,非正常停止脚本
# window 服务端异常:ConnectionReseatError
# Linux : 通讯死循环(不断收空内容)
## 在unix系统中,一旦data收到的是空
## 意味着是以后昂异常行为:客户端非法断开了链接
## 解决服务端bug
# linix: 服务端添加判断内容,对客户端传过来的数据进行判断,如果为空,直接break,
## 针对linix 系统修复bug
## 5,通信:收消息/发消息
while True:
data = conn.recv(1024) ## 1024 指最大接受的数据量为1024Bytes,收到的是Bytes类型
if len(data) == 0: break ## 空内容意味着异常,直接break
print('客户端发来的消息:',data.decode('utf-8'))
conn.send(data.upper())
## 针对Windows系统修复bug(添加异常处理)
while True:
try:
data = conn.recv(1024) ## 1024 指最大接受的数据量为1024Bytes,收到的是Bytes类型
print('客户端发来的消息:',data.decode('utf-8'))
conn.send(data.upper())
except Exception:
## 6,关闭电话连接(一个电话连接结束了就应该断掉)
conn.close() ## 挂断电话
break3-6 链接循环(让服务端一直提供服务)
## 服务端满足的条件,应该一直提供服务--------------添加链接循环
## 服务端代码
import socket
## 以打电话为例
# 1,买手机
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
## 2,绑定手机卡
phone.bind(('127.0.0.1',8080))
## 3,开机
phone.listen(5) ## 5 值得是半连接池的大小
print(f'服务端启动,服务运行在127.0.0.1:8080')
## 链接循环
while True:
## 4,等待电话连接请求,拿到电话连接conn
conn,alient_addr = phone.accept() ## 是个元祖
print(conn) ## 套接字对象
print('客户端的IP和端口',alient_addr) ## 客户端的IP和端口 ('127.0.0.1', 57424)
## 5,通信:收消息/发消息
while True:
try:
data = conn.recv(1024) ## 1024 指最大接受的数据量为1024Bytes,收到的是Bytes类型
print('客户端发来的消息:',data.decode('utf-8'))
conn.send(data.upper())
except Exception:
## 6,关闭电话连接(一个电话连接结束了就应该断掉)
conn.close() ## 挂断电话
break
phone.close()3-7 半链接池
phone.listen(5) ## 5 值得是半连接池的大小
## 如果不是并发编程,服务端只能与一个链接进行会话,其他客户端的链接进入半连接池,等待链接,如果超过了半连接池的大小,则客户端无法跟服务端进行连接10.8 基于 UDP 写一个套接字通信
## 服务端
import socket
## socket.SOCK_STREAM(流式协议) === TCP协议 sock.SOCK_DGRAM ===== UDP协议
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
print(f'服务端启动,服务运行在127.0.0.1:8080')
while True:
data,client_adddr = server.recvfrom(1024)
print(data)
print(client_adddr)
server.sendto(data.decode('utf-8').upper().encode('utf-8'), client_adddr)
server.close()
## 服务端可以发送空数据
## 客户端断了,服务端不会有影响
## 客户端
import socket
## socket.SOCK_STREAM(流式协议) === TCP协议 socket.SOCK_DGRAM ===== UDP协议
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
user_input = input(">>>>>:").strip()
client.sendto(user_input.encode('utf-8'),('127.0.0.1',8080))
data,server_addr = client.recvfrom(1024)
print(data.decode('utf-8'),server_addr)
client.close()10.9 粘包
## 须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来
## 首先需要掌握一个socket收发消息的原理
## 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。10.9.1 粘包出现的原因及解决方案
## tcp是流式协议。数据像水流一样粘在一起,没有任何边界区分
# 两种情况下会发生粘包。
##发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
## 服务端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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()
## 客户端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_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'))
## 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
## 服务端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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()
## 客户端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_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'))
## 收数据没收干净,有残留,就跟下一次结果混在一起
## 解决的核心法门就是:每次都收干净,不要有残留
'''
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
'''10.9.2 粘包问题普通解决(不是很好的解决方法)
## 第一种解决方法:-----------该接受的最大字节数(但不是很好的办法,也不能无限放大)
while True:
msg = input(">>>:").strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
res = client.recv(70000) ## 本次接受最大接受1024个字节
print(res.decode('gbk'),end=' ')
## 解决方法二
while True:
msg = input(">>>:").strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
lang = 1024
while lang> 1023:
res = client.recv(1024) ## 本次接受最大接受1024个字节
lang = len(res)
print(res.decode('gbk'),end=' ')10.9.3 自定义协议解决粘包问题
## 先收固定长度的头:解析出数据的描述信息,包括数据的总大小total_size
## 根据解析出的描述信息,total_size
## 服务端
from socket import *
import subprocess
import struct
## 服务端应该满足两个条件
## 第一件事:一直对外提供服务
## 第二件事:能够并发的给多个服务端提供服务
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
## 第一件事:循环的从半连接池中取出链接请求,与其建立双向链接,拿到链接对象
while True:
con,client_addr = server.accept()
## 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
res = con.recv(1024) ## 最大8096 就可以
if len(res) == 0: break
obj = subprocess.Popen(
res.decode('utf-8'),
shell = True,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
std_succ = obj.stdout.read() ## 拿到的结果都是是字节类型,但是window编码用的是gbk
std_err = obj.stderr.read()
total_size = len(std_succ) + len(std_err)
## 先发头信息(固定长度的bytes):对数据的描述信息
header = struct.pack('i',total_size) ## 把数字处理成长度固定的四个字节
con.send(header)
## 发送正确内容
con.send(std_succ)
## 发送错误内容
con.send(std_err)
except Exception:
break
## 客户端
from socket import *
import struct
client = socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
msg = input(">>>:").strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
header = client.recv(4) ## 拿到数据总长度
total_size = struct.unpack('i',header)[0] ## 解析数据总长度
rec_size = 0
while rec_size < total_size:
res = client.recv(1024) ## 本次接受最大接受1024个字节
rec_size += len(res)
print(res.decode('gbk'),end=' ')
else:
print()10.9.4 解决粘包终极大招
## 服务端
from socket import *
import subprocess
import struct
from hashlib import md5
import json
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
rn bs
## 第一件事:循环的从半连接池中取出链接请求,与其建立双向链接,拿到链接对象
while True:
con,client_addr = server.accept()
## 第二件事:拿到链接对象,与其进行通信循环
while True:
try:
res = con.recv(1024) ## 最大8096就可以
if len(res) == 0: break
obj = subprocess.Popen(
res.decode('utf-8'),
shell = True,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE
)
std_succ = obj.stdout.read() ## 拿到的结果都是是字节类型,但是window编码用的是gbk
std_err = obj.stderr.read()
total_size = len(std_succ) + len(std_err)
## 制作头
header = {
'filename':'a.txt',
'total_size':total_size,
'md5':'ainiainiainiaini'
}
str_json = json.dumps(header)
json_str_bytes = str_json.encode('utf-8')
## 先发头布长度信息
header_size = struct.pack('i',len(json_str_bytes))
con.send(header_size)
## 发送头部
con.send(json_str_bytes)
## 发送正确内容
con.send(std_succ)
## 发送错误内容
con.send(std_err)
except Exception:
break
# 客户端
from socket import *
import struct
import json
client = socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
msg = input(">>>:").strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
## 收四个字节的头部长度
header_size_bytes = client.recv(4) ## 拿到数据总长度
header_size = struct.unpack('i',header_size_bytes)[0] ## 解析头部总长度
## 根据头部长度,读取头部信息
header = client.recv(header_size).decode('utf-8')
header_dic = json.loads(header)
print(header_dic)
## 拿到数据大小
total_size = header_dic.get('total_size')
rec_size = 0
while rec_size < total_size:
res = client.recv(1024) ## 本次接受最大接受1024个字节
rec_size += len(res)
print(res.decode('gbk'),end=' ')
else:
print()10.10 socketserver 模块实现并发
10.10.1 基于 TCP 协议并发
## 服务端
import socketserver
class MyRequestHandele(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
res = self.request.recv(1024) ## 最大8096 就可以
if len(res) == 0: break
self.request.send(res.upper())
except Exception:
break
self.request.close()
s = socketserver.ThreadingTCPServer(('127.0.0.1',8888),MyRequestHandele)
s.serve_forever()
## 客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8888))
while True:
msg = input(">>>:").strip()
if len(msg) == 0: continue
client.send(msg.encode('utf-8'))
res = client.recv(1024) ## 本次接受最大接受1024个字节
print(res.decode('utf-8'))10.10.2 基于 UDP 协议并发
## 服务端
import socketserver
class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
client_data = self.request[0]
server = self.request[1] # 套接字对象
client_addr = self.client_address
print(client_data)
server.sendto(client_data.upper(),client_addr)
s = socketserver.ThreadingUDPServer(('127.0.0.1',8080),MyRequestHandle)
s.serve_forever()
## 客户端
import socket
## socket.SOCK_STREAM(流式协议) === TCP协议 socket.SOCK_DGRAM ===== UDP协议
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
user_input = input(">>>>>:").strip()
client.sendto(user_input.encode('utf-8'),('127.0.0.1',8080))
data,server_addr = client.recvfrom(1024)
print(data.decode('utf-8'),server_addr)
client.close()