python开发基础篇:四:socket网络编程
1:网络编程学习参考网址:https://www.cnblogs.com/Eva-J/articles/8244551.html
1:由于不同机器上的程序要通信,才产生了网络,不是单纯的两台机器通信,而是机器上的程序和机器上的程序去通信
(比如:我的qq和别人的qq,不能说两台电脑光联网就能通信聊天了,一般是程序间的通信,网络能够实现通信)
2:软件开发架构
我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:
第一种是应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用,点击就可以运行,用之前需要安装
第二种是web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用
这些应用的本质其实都是两个程序之间的通讯。而这两个分类又对应了两个软件开发的架构~
3:C/S架构
C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的 这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大 Server:服务端,服务端要一直运行,等待服务别人 Client:客户端,想寻求服务的时候才使用服务 所有的网络的通信都基于必须有个Client与Server端, 使用qq和访问百度:我们是客户端
下面的图就是C/S架构,手机和平板电脑等客户端安装一些qq或者微信等应用,然后去向服务器请求服务,这种程序的设计就是C/S架构
4:B/S架构
B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。 Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,
只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查 1.C/S架构: C/S即:Client与Server ,中文意思:客户端与服务器端架构, 这种架构也是从用户层面(也可以是物理层面)来划分的。 这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上, 对用户的电脑操作系统环境依赖较大。 2.B/S架构:所有的bs架构都是基于浏览器能访问到的 B是Browser浏览器 B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。 Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序, 只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源), 客户端Browser浏览器就能进行增删改查。 B/S架构更火,使用C/S架构电脑要装很多应用
B/S架构统一入口,一个浏览器入口进入各种应用
手机端B/S目前架构不火:慢慢也会火起来,比如微信 微信引入了各种小程序和公众号,微信实现了统一入口 B/S架构能够解耦分治的思想,统一入口(从浏览器就可以访问各种服务和网站),而且使用更方便 C/S架构和B/S架构的关系: B/S架构是 一种 C/S架构,B/S架构的客户端换成了浏览器而已,浏览器变成了客户端而已, 有了个专门的名字
把功能程序都放在一起统一入口
请假 一个程序php写的
打卡 一个程序python写的
报销
会议室预定
.....
最后把所有的入口统一起来搞个总的入口
这就是解耦分治的开发思想,大程序分成几个小程序,小程序独立进行开发,
开发速度快,快速的出个功能模块,而不是整个大程序都完善后才是一个程序
这也是一种统一入口的思想
5:计算机网络基础:https://www.cnblogs.com/Eva-J/articles/8066842.html
网络编程:基于网络之间两个程序之间能够互相通信
1:想要实现通信 网卡:mac地址,唯一编号, 网线: 网卡上有全球唯一的 mac地址 mac地址
ip地址:表示一台机器,四个点分十进制,4个8位2进制数:0.0.0.0-255.255.255.255
端口
通过ip地址就能找到对应的mac地址:arp协议(给一个ip地址就能找到mac地址的协议)
不是有了ip地址就可以访问这台机器了
本质上还是通过mac地址访问(ip地址更方便记忆而已(机器编号),ip地址通过arp协议找到mac地址)
ip地址与ip协议: 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4(目前有些使用ipv6了),它规定网络地址由32位2进制表示 范围:0.0.0.0-255.255.255.255 一个ip地址通常写成四段十进制数,例:172.16.10.1 ip保留地址: a类网 10.0.0.0~10.255.255.255 b类网 172.16.0.0~172.31.255.255 c类网 192.168.0.0~192.168.255.255
127.0.0.1:本地回环地址,在机器上写127.0.0.1就知道是自己的地址,不需要写自己的ip地址
走127.0.0.1数据就不会走外部网络了,不走外部的路由器和交换机,没有网络延时,
用127.0.0.1就是一台机器的不同的两个程序之间在通信,这样就不会走网线和外部网络了,完全没有网络延时了(回环地址)
ip地址:一台机器在网络上的位置,通过ip地址就能找到机器,是不是在同一局域网里还是公网的ip
公网ip:大家都可以访问的ip类似百度,所有能访问的网址
局域网ip:192.168.xxx.xxx 10.xxx.xxx.xxx这些都属于局域网ip
如果使用局域网的ip就只能在网络内部去访问
局域网ip是在这一个局域网的范围内之间的网络是通过交换机连接的,之间可以通过广播的方式互相通信
端口号:网络相关的程序才需要开一个端口,普通不涉及网络的程序不需要使用端口,通过文件通信不需要开端口
为的是能找到某台计算机上的唯一一个程序
在同一台机器上同一时间只能由一个程序占用同一个端口
一般情况下使用8000之后的端口,前面的端口可能被占用的,比如操作系统占用的端口,3306是mysql数据库的默认端口,我的程序和数据库通信,数据库可能在另外一台机器上
之间通过网络进行通信的,需要数据库机器的ip地址和数据库开的3306端口
酷狗音乐开启后占据的是8000端口,
广播和交换机:一般多台电脑上所有的网线都连接到交换机上去,交换机解决多台电脑通信问题
交换机:解决多台机器之间的通信问题
广播和单播:
多台机器连接到一个交换机组成的小型局域网的情况,主机4访问主机1,通过交换机访问,
主机4通过交换机告诉所有人要找主机1,只有主机1能回复(由交换机告诉网络当中所有机器的过程叫做广播),
主机这一台机器回复,交换机把回复的消息给主机4不用给别的主机了(这个过程叫单播)
广播:
主机之间“一对所有”的通讯模式,网络对其中每一台主机发出的信号都进行无条件复制并转发,
所有主机都可以接收到所有信息(不管你是否需要),由于其不用路径选择,所以其网络成本可以很低廉。
有线电视网就是典型的广播型网络,我们的电视机实际上是接受到所有频道的信号,但只将一个频道的信号还原成画面。
在数据网络中也允许广播的存在,但其被限制在二层交换机的局域网范围内,禁止广播数据穿过路由器,防止广播数据影响大面积的主机
arp协议:通过ip找mac 某个主机1的数据包发送出去想要找10.0.0.1这个ip地址,主机1把请求发给交换机,让交换机广播出去告诉所有主机,
如果收到广播的主机2发现自己是这个ip就把自己的mac地址发给交换机,交换机再把这个消息发送给主机1(交换机现在只发这一台机器的请求只发给一个人)
(消息发出去的时候是广播,发回来的时候是单播)
这里通过交换机告诉广播找ip地址的主机
(响应想要找主机回复他,把mac地址通过交换机发送给他,所以主机1拿到主机2的ip对应的mac地址,
主机1短暂的把信息缓存起来,下次再找这个ip地址的时候不需要再找mac地址了,直接往这个mac地址出去发就行了)
这个过程就是arp协议去做的
广域网与路由器: 为了避免广播风暴建立一个个小小的网络(局域网),在一个局域网里这些机器连在交换机上,另一个局域网机器也连接在交换机上,
局域网和局域网之间通过路由器连接起来,如下:
网关:局域网种的机器想要访问局域网外的机器需要通过网关访问
传话的人(局域网a的机器和局域网b的机器无法直接通信的,需要借助传话的人网关)
相当于局域网对外统一接口(默认网关)
子网掩码:
机器怎么知道要访问的另外一台机器在不在同一个局域网内,判断两个机器是不是在同一网段(看ip地址和子网掩码的按位与得到网段地址)
比如子网掩码:255.255.255.0,ip地址是:192.168.13.253
子网掩码二进制:11111111 11111111 11111111 11111111
ip地址的二进制:11000000 10101000 00001101 11111101
按位与:1与1为1,1与0与为0,0与0与为0,那就是1与上任何一个数结果就是任何那个数,0与任何数结果都是0
子网掩码和ip地址按位与出来的结果:11000000 10101000 00001101 00000000 :
与出来的结果是局域网的网段:192.168.13.0
192.168.13.22是不是在局域网内和其他主机在同一网段:是的,看网络地址前3位是不是和网段(192.168.13.0)是不是匹配上的
只允许后面一位不同,192.168.13.0求出来的是网段地址,在局域网内的都是以192.168.13开头的,
以此来判断访问的ip和自己在不在同一个局域网内
比如:
已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算, 172.16.10.1:10101100.00010000.00001010.000000001 255255.255.255.0:11111111.11111111.11111111.00000000 AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0 172.16.10.2:10101100.00010000.00001010.00000010 255255.255.255.0:11111111.11111111.11111111.00000000 AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0 结果都是172.16.10.0,因此它们在同一个子网络。
IP协议的作用主要有两个:
一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络(通过子网掩码)。
端口:通过端口找到对应的程序
在计算机上每一个需要网络通信的程序都会开一个端口用来做程序与程序之间的定位
当找到ip地址的时候只是找到机器,需要找到机器上对应的端口才是找到服务
在同一实际只会有一个程序占用一个端口
不可能在同一时间在同一个计算机上 有两个程序占用 同一个端口
端口的范围是从:0-65535
端口不能瞎几把使用:多少之前都是系统使用的,开端口尽量往数字大了开,一般用8000之后的端口
ip:可以一个网络之内确定唯一的一台机器
端口:确定唯一的一个程序
ip+端口:找到唯一的一台机器上的唯一的一个程序
如果想起个服务被别的机器访问:
1:同一局域网内可以随便访问
2:起服务器的机器必须申请使用公网ip,有了公网ip就会在网络和路由上注册了,别人能通过路由器找到这个公网ip
Tcp协议:
当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。
在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。
这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止
client server
全双工 clent可以往server发信息server端接收 server也能往client端发信息client能接收
这就是全双工,双发都可以收发信息,tcp协议都是全双工的,两边都能互相收发消息的
tcp协议三次握手:一般发起连接的请求端都是client客户端
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,
等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1]
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
tcp三次握手后两台设备的应用就会建立链接,链接暂时就不会断开了一直连着,两台设备之间就有一个通道了,
设备a给设备b发送消息直接写发送就行了,每一次发送信息另外一台设备收到信息之后都要回复一下收到信息了,每一次收到信息了都要回一个收到信息了
(收发信息的过程很安全,不会丢消息)
tcp是可靠的面向连接的(连上了一直连着,在连接的基础上进行通信),且传递信息的过程中是全双工的(互相都可以收发消息)
信息传完了后需要依靠四次挥手解除连接,谁说结束连接都可以,
一般连接的时候都是client端先请求连接,因为server端一直都是处于监听等待的状态,等待client客户端连接进来
结束连接的时候client和server端都可以主动发起
下图是假设client先发起的tcp解除连接
client端想要断开连接,client端第一次请求断开,server端同意断开就会发送同意clent断开的消息给client端,这里有两次请求干了两件事,
这里先断开全双工client发送消息的这一条线路(断开一个单双工线路),现在client不能给serve tcp传递消息了,
然后server端也发起请求想要断开连接给client,client也同意server的断开马上发送ok请求给server,走了这条路另外一条线路(单双杠线路)也断开
经过4个步骤才完成着两个全双工通信之间的断开服务,这就是四次挥手
为什么三次握手,挥手需要四次:
四次挥手,假设c不想给s发消息了,那么c客户端先发结束通信,马上服务端发送ok,那么这两个步骤会端开一条双工线路,c无法向s发送消息了,
s端不主动发起结束的话,s还是能给c发送消息的,不能c不想给s发消息了强制s也不能给c发消息,只会先把c给s发消息这条双工线路关闭,
相当于clent端的耳朵还开着能收到消息,但是client的嘴巴关闭了,发起关闭只能控制自己的嘴巴不说了,不能控制别人的嘴巴不让别人说
别人不想说话得让别人自己主动发起不想说了,所以是四次挥手,s同意c断开发送给c的请求 和 s自己发送和c的断开连接的请求 这两个请求动作不能合并
但是三次握手s同意c的连接请求和s想要和c建立连接发送的请求这两步合成一步了所以才三次挥手
为什么三次握手四次挥手:总的答案是:只要tcp链接一旦建立就是一个全双工的,必然是双方通信,
所以a跟b表示通信,b跟a之间也能通信,上来就建立双向链接,中间一步确认同意跟你通信以及发起主动跟你通信的过程能合成一步
但是挥手的时候,a不跟b发信息之后断开一条链接,这是一个单独的过程,如果这时候b刚好也不想跟a发送消息了这两步可以合成一步
但是如果a已经断开个b发送消息的链接了,但是b任然想给a发送消息,所以这时候不能合成一步,断开a发消息给b这个链接需要两步,
等b发送完消息后b想跟a断开链接也需要两步,这这一定是四次挥手,不能我a跟b断开强制b就马上给a发断开消息让b也不给a发,
需要等b自己主动断开连接给a发,
c和s,c想断开连接,c发断开请求,s发同意请求,这里c往s发消息的这一条双工路线断开,
只是c和s的tcp连接断开了,而不是c不能往s发消息了,c任然可以向s发请求,只是c和s的tcp的长链接断开了
,网络通c和s一直可以发消息,
c和s如果建立一个长链接说明在链接的基础上c和s可以通信,在这个连接上建立存在很多数据,比如说现在传到第几次了
一些信号量,一些序列号都可以存在这次链接里,断开这个链接这些数据都没有了,但是c和s任然能够通信,
tcp四次挥手断开链接只是断开了tcp的链接,没有断开网络链接,c和s任然能够通信的,
只是不走tcp协议通信了,所以四次挥手最后一次挥手不走tcp协议来通信了(因为c往s发消息这条tcp链接已经同意断开了)
这就是三次握手四次挥手
tcp协议的四次挥手:
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。[1]
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2]
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。[2]
tcp长链接的机制:只要连接上了中间这个链接就一直不断,占用两台机器之间的通信线路,其他的不能用了,直到断开为止
tcp协议保证连接之上发信息不会丢失,每发一条消息过来告诉另一方我读取了------这是协议的内容
如果没有协议就类似udp直接发就行了,都不需要建立连接,
当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上,丢网络上对面接收就行了
Udp协议:
当应用程序希望通过UDP与一个应用程序通信时,传输数据之前源端和终端不建立连接
当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上
udp:不可靠的,无连接的
tcp对数据的安全更好,每次发送消息后必须对方收到一个回信有没有收到,udp只管发不管对方有没有接收到,发了就和我无关了
udp传送数据非常快,因为不需要回复,直接丢消息就行,更快而且不占用连接
tcp是打电话需要一直连着,挂了电话就没了
udp是发短信,只管发,对方不会实时收消息并且并且回复收到消息,可以回复可以不回复
tcp是长链接,udp是无链接的
tcp和udp的对比:
TCP:传输控制协议,提供的是面向连接、可靠的字节流服务。
当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。
TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
tcp占链接比udp慢,但是安全数据不会丢(会回复收到数据机制没有收到重复发送)
tcp可靠,面向连接,需要先建立连接才传送数据,相对耗时长,效率没有udp高
UDP:用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,
它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。
由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快
udp快,且不占用链接
不可靠,无连接,效率高,发的快
qq:本身走的udp协议,不可能和多个人发消息就建立多个长连接,但是qq能显示消息在这端消息没发出去,能感受到消息能重发
一般情况下不会丢包,是因为qq在基于udp协议的基础上自己实现了一个信息的检测,没有三次握手和四次挥手,没有长连接
但是在程序的代码基础上去实现我给你发消息之后如果成功了那么对方会给个回信,这个是基于自己程序完成的
比如每次发个消息给对方,对方都能回个信,如果隔了多少s没有得到回信说明消息没有发送成功,这个时候可以选择是否重发
,这个是基于程序的角度上实现的
两台机器想要通信:
1:找到对方的机器(经过交换机,路由器还是什么通过ip地址找到对方的机器)
2:找到对方机器上的程序(通过端口号)
3:程序之间传递信息使用什么协议(tcp,udp)
6:互联网协议与osi模型
所有的机器都在网络上,所有机器想要通信也得统一一种标准和规范:这种标准和规范就是互联网协议
互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层(重点了解五层就行)
物理层:两台机器相连,真正连接起来的是物理层(网线,网卡),传输电信号表示010101
我们编写程序是基于应用层来写的,写的所有的python代码都可以称为一个应用,应用层不关心底层的情况,只管传消息就行
比如应用层传输一个"hello"到对面,只关心发什么就行,底层还有很多事情要完成
1:选择什么协议
2:访问对面什么端口
3:找到对面机器需要对面的ip地址,需要对面的mac地址
这些都是下面的层来做的
传输层:选择通信传输协议,tcp或者udp协议
网络层:ip协议,需要知道访问的对方的ip是什么,带着自己的ip地址,给上一层的消息又加上了ip信息
数据链路层:mac地址,包了一层mac地址
tcp和udp属于传输层的
ip协议属于网络层的
mac地址属于数里链路层的
每层运行常见物理设备:如下
四层路由器:走传输层,传输层是tcp协议和udp协议,四层交换机它能够根据走的是什么协议然后帮忙找对应的应用去,功能能之间帮找到协议
每层运行常见的协议:如下
应用层就是我们写的代码
物理层就是010101
7:网络编程参考网址:https://www.cnblogs.com/Eva-J/articles/8244551.html
网络相关的一些基础面试题: 1:ip协议属于网络osi七层协议中的那一层:网络层 2:tcp协议和udp协议属于osi七层协议中的那一层:传输层 3:arp协议属于osi七层协议中的那一层:数据链路层
8:socket的概念
应用层就是我们写的程序
socekt:socket在应用层和传输层之间,直接和我们的程序打交道,我们一般不直接去接触这些tcp协议和udp协议
什么三次握手,四次挥手我们不需要关心,都被socket承包了
理解socket:
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,
它是一组接口(原本通信这是需要我们完成的,但是中间加了个socket,只给socket指令就行,socket帮忙完成协议里面做的事情)。
(socket就是操作网络的一个接口,我们想使用网络通信就使用socket,socket就是起到一个接口的作用)
在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,
对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议
有了socket就不需要关心下层的协议内部怎么走的,发什么请求,包了什么数据
其实站在我们的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。
所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
套接字(socket)的发展史 套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。
因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。
一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC(进程之间的通信就是IPC通信)
套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的
两个py文件之间通信,如果在同一台机器上可以基于文件通信,在不同机器上只能通过网络通信,基于文件的基本不用
基于文件类型的套接字家族:
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,
两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族:
套接字家族的名字:AF_INET(ipv4)
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,
要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,
所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,
但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
tcp协议和udp协议: TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、
传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序 UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),
一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)
socket建立网络连接发起tcp和udp请求的流程:如下
基于socket角度来看tcp和udp协议在实际使用的时候做了哪些事情:左边tcp右边udp
9:简单的基于Tcp协议的socket
# server.py import socket sk = socket.socket() # 创建一个socket对象 sk.bind(('127.0.0.1', 8898)) # 把地址绑定到套接字,绑定自己这个程序所在的地址。给server端绑定一个ip和端口 sk.listen() # 监听链接,可以传递一个参数设置最大连接数,不写就没有限制 conn, addr = sk.accept() # 接受客户端链接,获取到一个客户端的连接conn,
# 这些走的都是tcp协议,三次握手不需要我们去操作,只要拿到conn就说明已经完成了三次握手建立了一个连接了,conn就是建立的连接
# 正常情况下程序在accept这里就会阻塞,一直停这里不会结束,等待一个客户端连接进来阻塞才会结束往下走
# 拿到连接conn,对于tcp协议来说需要拿着连接conn去和别人聊天 ret = conn.recv(1024) # 接收客户端信息
# recv这里又是一个阻塞点,每一次执行一个recv一定是接收消息,如果没消息就一直等着,等到天荒地老,直到收到一个客户端发来的消息
print(ret) # 打印客户端信息 conn.send(b'hi') # 向客户端发送信息,发消息过程不阻塞,消息来了直接就发 conn.close() # 关闭客户端套接字,关闭链接,关闭连接之后说明tcp连接断开了 sk.close() # 关闭服务器套接字(可选),如果不关闭socket对象还能继续接收其他客户端的连接
# client.py import socket sk = socket.socket() # 创建客户套接字 sk.connect(('127.0.0.1', 8898)) # 尝试连接服务器 sk.send(b'hello!') ret = sk.recv(1024) # 对话(发送/接收) print(ret) sk.close() # 关闭客户套接字
# 客户端只需要一起,不需要起服务,客户端去连别人就行了,sk.connect之后直接拿到的就是一个连接
# 服务端需要起服务,等待别人连接,连接进来之后拿到一个链接,服务器的提供别人连接进来的服务需要一直在,这样别人才能连接进来
# 一般情况下先启动server服务端,然后client连接server服务端
# 下面是server端的启动代码
sk = socket.socket() # 买手机,sk是手机对象
sk.bind(('127.0.0.1', 8898)) # 绑定手机卡,接收一个元组参数,元组里两项,先是ip(同一台自己和自己通信可以使用回环地址)地址后是端口
sk.listen() # 监听,等着有人打电话
conn, addr = sk.accept() # 接收到别人的电话,拿到两个东西:conn(拿到别人打过来的连接跟别人通话)
addr:是客户端发起想要连接进来的机器的地址,别人的电话号码
拿到这个链接后对着这个链接说话,后面操作这个链接就行
ret = conn.recv(1024) # 听别人说话,写个参数听别人说多少个字,一般情况下默认写1024个字节,也可以2048,4096,
一般情况下写1024的整数倍,
print(ret) # 别人说的话打印出来
conn.send(b'hi') # 我和别人说话,网络上能传输的只有1010二进制,所以这个地方必须传一个bytes类型,
conn.close() # 挂电话
sk.close() # 手机不用了需要关机
# 上面就是一个基于tcp连接的使用socket起一个server端的过程
# 下面是client端的启动代码:起socket发消息的一个服务
sk = socket.socket() # 客户端买手机
sk.connect(('127.0.0.1', 8898)) # 拨号,不需要绑定手机号,因为不对外提供服务,不需要公布自己的手机号,只拨号过去就行了,找别人需要些别人的地址
sk.send(b'hello!') # 发送信息
ret = sk.recv(1024) # 接收信息
print(ret) # 打印信息,听到别人说的话
sk.close() # 关机
如果在重启服务的时候碰到上面的问题:address already in use 解决方法一:碰到这种情况一般情况下换个端口就行了
造成报错的原因:写程序的过程中我们关闭了链接,socket链接是在操作系统上建立的 发一个指令我建立一个网络链接,需要使用操作系统的某个端口去和别人通信 这个链接是建立在操作系统上的 程序已经下指令关闭了,任然有可能程序告诉计算机去关闭但是不是立即关闭,可能收到了关闭信息过一会才关闭,
也可能没收到关闭信息就不关闭了一直开着,只要开着这个链接端口就被占用着,再次去启动新的链接使用这个被占用的端口就会报错(不能使用同样的端口)
解决方案二:sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 就是它,在bind前加这么一行代码,完整代码如下:
#加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.setsockopt(SOL_SOCKET, SO_REUSEADDR,1) # 就是它,在bind前加,避免服务重启的时候报address already in use错误, sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv(1024) #接收客户端信息 print(ret) #打印客户端信息 conn.send(b'hi') #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选)
SO_REUSEADDR: reuseraddr(设置允许放在操作系统的一个链接被我们重用,就不会出现地址被占用的问题了)
跟操作系统允许最大链接数也有关,操作系统如果设置允许的连接数很少那么用完一个立刻就回收了
如果允许了很多连接,可能用完了也不会即时回收----就可能出现上面的报错
中文字符转成bytes数据类型的两种方式: 1:方式一:中文.encode(编码格式) print("李二狗".encode('utf8')) # b'\xe6\x9d\x8e\xe4\xba\x8c\xe7\x8b\x97' # utf8编码3个字节表示一个中文,所以李二狗需要9个字节 print(len("李二狗"), len("李二狗".encode('utf8'))) # 3 9 2:方式二:bytes(中文, encoding=编码格式) print(bytes("李二狗", encoding='utf8')) # b'\xe6\x9d\x8e\xe4\xba\x8c\xe7\x8b\x97'
小练习: # server端和client端 # 接收时间戳时间,转化成格式化时间 # client端 10秒 time.time() 把时间戳时间发给server端 # server.py import socket import time address = ("127.0.0.1", 8888) sk = socket.socket() sk.bind(address) sk.listen() conn, addr = sk.accept() print(f"欢迎{addr}连接进来") while 1: ret = conn.recv(1024).decode('utf8') print(time.ctime(float(ret))) # client.py import socket import time address = ("127.0.0.1", 8888) sk = socket.socket() sk.connect(address) while 1: sk.send(str(time.time()).encode('utf8')) time.sleep(10)
小练习:server和client循环聊天 server.py import socket address = ("127.0.0.1", 8888) sk = socket.socket() sk.bind(address) sk.listen() conn, addr = sk.accept() print(f"欢迎{addr}连接进来") while 1: ret = conn.recv(1024).decode('utf8') if ret == "q": break print(ret) info = input(">>>>") if info == "q": conn.send(b"q") break conn.send(info.encode("utf8")) conn.close() sk.close() client.py import socket address = ("127.0.0.1", 8888) sk = socket.socket() sk.connect(address) while 1: info = input(">>>") if info == "q": sk.send(b"q") break sk.send(info.encode("utf8")) ret = sk.recv(1024).decode('utf8') if ret == "q": break print(ret) sk.close()
# 假设多个client连接进server端
# server端在接受收到client1请求的时候,相当于client1和server之间建立了长连接,不会主动断开一直连接着
当client2这时候同时来连接的时候,client2想连让你连接上,只是让client2一直等着,等着client1这个链接断开了之后
服务端:sk = socket.socket(),假设后面只是conn链接的关闭,下面sk仍然能够接收其他对象连接进来,
下面还来个accept就能接收到client2的请求的连接,但是前提是必须先和client1的之间的请求断开
# 长连接的效果需要一直占据着网络,不让别人使用
# 其实服务端可以同时和多个客户端建立tcp连接同时进行聊天
server.py
import socket
from threading import Thread
sk = socket.socket()
sk.bind(("127.0.0.1", 8889))
sk.listen()
def talk(conn):
while 1:
ret = conn.recv(1024).decode("utf8")
print(ret)
if ret == "q":
break
# info = input(">>>>>")
# if info == "q":
# conn.send(b"q")
# break
# conn.send(info.encode("utf8"))
conn.send(b"hello")
while 1:
conn, addr = sk.accept()
print(f"欢迎{addr}连接进来")
t = Thread(target=talk, args=(conn,))
t.start()
client.py
import socket
address = ("127.0.0.1", 8889)
sk = socket.socket()
sk.connect(address)
while 1:
info = input(">>>")
if info == "q":
sk.send(b"q")
break
sk.send(info.encode("utf8"))
ret = sk.recv(1024).decode('utf8')
if ret == "q":
break
print(ret)
sk.close()
开启server以后多个client可以连接进server建立tcp连接和server进行聊天通话
10:简单的基于Ucp协议的socket
server.py import socket address = ("127.0.0.1", 9999) sk = socket.socket(type=socket.SOCK_DGRAM) # 拿到udp的socket对象 DGRAM:datagram数据报文的意思 sk.bind(address) # udp套接字绑定ip地址,udp无连接也需要起服务,udp不需要接收连接所以不需要listen监听 msg, addr = sk.recvfrom(1024) # udp的接收是recvfrom # udp服务这里必须等待别人先发数据进来才知道发送方的addr,所以不能先给别人发信息的 # tcp协议只要客户端主动连接进来了,那么连接后服务端主动发信息和客户端先主动发信息都可以 print(msg.decode("utf8")) sk.sendto(b"bye", addr) # udp协议中发送数据的方法sendto sk.close() client.py import socket address = ("127.0.0.1", 9999) sk = socket.socket(type=socket.SOCK_DGRAM) # 拿到udp的socket对象 sk.sendto(b'hello', address) ret, addr = sk.recvfrom(1024) # udp每一次的发送都会到这地址信息发过来,因为没有链接,只要是收发消息就需要带上地址 print(ret.decode("utf8")) sk.close()
1:udp的server不需要再进行listen监听和建立连接,而是在启动服务之后只能被动的等待客户端发送消息过来
2:客户端发送消息的同时还会自带地址信息
3:消息回复的时候,不仅需要发送消息,还需要填写对方的地址参数(sendto函数)
# udp实现一个简易的qq,一个server同时和多个client进行聊天 server.py import socket address = ("127.0.0.1", 9999) sk = socket.socket(type=socket.SOCK_DGRAM) # 建立一个udp的socket对象 sk.bind(address) # bind绑定地址后服务就起来了 while 1: msg, addr = sk.recvfrom(1024) if msg == b"q": break print(msg.decode("utf8")) info = input("server端>>>>").encode("utf8") sk.sendto(info, addr) sk.close() client.py import socket address = ("127.0.0.1", 9999) sk = socket.socket(type=socket.SOCK_DGRAM) # 拿到udp的socket对象 while 1: info = input("client1:>>>>>>") if info == "q": sk.sendto(b"q", address) break info = "来自client1的消息:" + info sk.sendto(info.encode("utf8"), address) msg, addr = sk.recvfrom(1024) # udp每一次的发送都会到这地址信息发过来,因为没有链接,只要是收发消息就需要带上地址 print(msg.decode("utf8")) sk.close() # 起一个udp的服务可以同时接收多个人的消息和多个人聊天了,
# 时间同步的udp服务器 # server端提供时间同步服务: # 1:接收时间的格式(客户端发送时间格式进来) # 2:将我的时间转化成接受到的格式发回给客户端 # server.py import socket import time sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(("127.0.0.1", 9999)) while 1: msg, addr = sk.recvfrom(1024) sk.sendto(time.strftime(msg.decode("utf8")).encode("utf8"), addr) # client.py import socket sk = socket.socket(type=socket.SOCK_DGRAM) addr = ("127.0.0.1", 9999) sk.sendto('%Y-%m-%d %a %H:%M:%S'.encode("utf8"), addr) msg, addr = sk.recvfrom(1024) print(msg.decode("utf8"))
11:粘包现象:参考文档:https://www.cnblogs.com/Eva-J/articles/8244551.html
1:起一个服务不是一台机器一般是很多台机器起的
如果想要管理这些机器应该批量的管理,
假设一台机器做管理的,其他机器是被管理的,
所有的客户端端执行server端下发的指令
将结果反馈回来,我来就收
os.popen()
和系统命令相关的都找os模块,os.popen()能够执行当前所在的操作系统的系统命名,且能拿到执行后的结果
subprocess模块 import subprocess ret = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # dir参数:接收的是命令参数 # shell参数:执行系统命令 # PIPE:管道,能往管道里放值也能往管道里取值,管道也是一直数据类型 # stdout参数:正常情况下标准输出会放到屏幕终端,现在设置stdout=subprocess.PIPE把标准输出放到管道里 # stderr参数:stdout标准错误也放到管道里 # 标准输出和错误都放到管道里了,这些数据可以直接从返回值ret里直接拿就行:如下 print(ret.stdout.read().decode("gbk")) # 拿到标准输出对象需要read读一下,读出来的内容是bytes类型,还需要decode解码 print(ret.stderr.read().decode("gbk"))
使用subprocess的好处:
如果使用os.popen()去执行的话,拿到的结果不管正确的还是错误的都在一起(标准输出和标准错误返回的内容在一起),
使用subprocess.Popen去执行一个命令的话,能分别拿到错误的信息和正确的输出信息
因为subprocess.Popen可以指定不同的信息可以放到不同的容器里面去
假设一个需求: # 基于tcp实现远程执行命令 # server端下发命令,client接收命令并执行 server.py import socket sk = socket.socket() addr = ("127.0.0.1", 9999) sk.bind(addr) sk.listen() conn, addr = sk.accept() while 1: cmd = input(">>>>>") conn.send(cmd.encode("utf8")) ret = conn.recv(1024).decode('gbk') # subprocess在windows电脑返回的是gbk格式的字节类型,这边拿到的也是gbk的字节码,所以这里需要gbk解码 print(ret) conn.close() sk.close() client.py import socket import subprocess sk = socket.socket() addr = ("127.0.0.1", 9999) sk.connect(addr) while 1: cmd = sk.recv(1024).decode("gbk") ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out = "stdout:" + ret.stdout.read().decode('gbk') std_err = "stderr:" + ret.stderr.read().decode('gbk') print(std_out) print(std_err) sk.send(std_out.encode("gbk")) # 发送的gbk的字节码 sk.send(std_err.encode("gbk")) # 发送的gbk的字节码 sk.close()
上面的代码有一个问题:
client客户端每次都会send发送两个消息,但是server端recv收到一个
所以server端每次输入一个命令只直到client端发来的一个,第二次server发送命令收到的是上一次client send发送的消息,不是本次的
后面接受到的数据已经乱了
send2次下一次再recv的时候还是能接受到send的第二次发的消息,
send很长的信息的时候,这边recv没有接收完,剩下的一部分在下一次又recv的时候又会把剩下的没有接受完的recv过来:没接收完的问题
还有send好几次过来的数据被recv这边一次性全接收过来了:接收多了的问题
上面的问题就是黏包现象:tcp才出现粘包现象
tcp:优缺点
1:出现粘包现象
2:不丢包
假设一个需求: # 基于udp实现远程执行命令 # server端下发命令,client接收命令并执行
server.py import socket sk = socket.socket(type=socket.SOCK_DGRAM) addr = ("127.0.0.1", 9999) sk.bind(addr) msg, addr = sk.recvfrom(1024) while 1: # 使用udp没有办法服务端主动发送消息给客户端,只能等待客户端先来发送一个消息进来然后才能拿到对方的信息 cmd = input(">>>>") if cmd == "q": break sk.sendto(cmd.encode("utf8"), addr) msg, addr = sk.recvfrom(1024) print(msg.decode("utf8")) sk.close() client.py import socket import subprocess sk = socket.socket(type=socket.SOCK_DGRAM) addr = ("127.0.0.1", 9999) sk.sendto(b"hello", addr) while 1: cmd, addr = sk.recvfrom(1024) ret = subprocess.Popen(cmd.decode("utf8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out = "stdout:" + ret.stdout.read().decode('gbk') std_err = "stderr:" + ret.stderr.read().decode('gbk') print(std_out) print(std_err) sk.sendto(std_out.encode("utf8"), addr) sk.sendto(std_err.encode("utf8"), addr) sk.close()
这里还是client两次sendto,server一次recvfrom,server这边一次收消息到账传入下次cmd给client的时候得到的执行结果还是sendto这边以前发送的,
但是udp这边不会把上次没有发完的数据继续recv接收过来,不会把client端两次send的数据粘在一起,udp没有发完就不发了,就丢了
udp:优缺点
1:不粘包
2:不可靠,发过来的信息不完整,udp会丢包
udp来讲先开启sever端和先开启client端都没有问题的,
qq:发送qq可能碰到发现的消息过长不让发,让以文件的形式发送过去,udp长度限制的问题,超过这个长度就不让发了
黏包成因: TCP协议中的数据传递
tcp协议的拆包机制:
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,
这样会产生很多数据包碎片,增加丢包率,降低网络速度
tcp协议自带拆包机制:看起来发的是一个数据,但是在tcp协议这一端会分开成多个包(接收方根据编号重新排序,排成一个完整的)
面向流的通信特点和Nagle算法:
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,
更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,
防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
tcp:比如两次send数据非常非常小,第一次send一个数据1,第二次send一个数据2,在我们用户角度看来是send2次
而对于tcp协议来说看为send两个小的数据包,两个数据小又挨得近所以干脆封装成一个包发送过去----这样接收方就很难分辨出来了
tcp:有一个链接存在,链接存在的基础上想怎么传就怎么传,链接没有关闭一个包打散成几个散的包然后组合起来感知不到
所以大数据网络上分段传输更快而且不依赖MTU限制,可以采用这种拆包机制,这种拆包的机制造成了面向流通信的特点,没有消息的保护边界
由于这种机制导致粘包现象的成因,
基于tcp协议特点的黏包现象成因
socket数据传输过程中的用户态与内核态说明: 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。 也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。 而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。 怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
用户态:对于socket来讲,我们写的代码就是用户态,
可能性1:
假设写了个send1,又写了个send2,两次都是在send数据,且send的数据非常非常的小,
根据优化算法会在send1这里不立即发出,等一会,
在内核角度来看,send1,在缓存区缓存一下,又发了一个send2,缓存区又缓存一下,正好连上了,这两个数据挨得很近且没什么界限
所以拿着链接直接就发了,这2个数据挨得近就封装成1个包发送过去
用户态以为发了2次,其实内核态组成1次发送,
客户端的内核态的缓存区一次就接收进来了12,socket客户端这里直接看到的就是粘包现象
可能性2:
s端send发送了一个特别大的数据,c这边只接收一部分,可能没有接受完,这一部分被用户态的socket recv接收进去了,
剩下的一部分就被悄悄的缓存在c这边的内核态缓存里了,当再一次在c这边执行recv的时候就把剩余的这部分又拿过去接收了,
这样就会可能拿一个数据拿不全,下次再拿的时候拿全了
粘包的表面现象:因为两个send离得太近,且消息太短,优化算法干得,把两个消息认为一个消息发送过去了
tcp协议才会发送粘包,udp不会发生粘包
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,
在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。
若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
UDP不会发生黏包:
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的(没有流的概念),提供高效率服务。
不会使用块的合并优化算法,发过来一条消息我这边就接收到一条消息,每发过来一条消息就当成一个数据包接收,没有合并算法,也没有接收一部分再继续接收的算法
由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)
采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),
这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,
防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),
收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
udp和tcp一次发送数据长度的限制: 用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。
用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送) 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),
这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,
如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。tcp和udp的重点知识:
1:tcp会出现粘包,而udp不会出现粘包现象 2:tcp之所以粘包是因为内部做了一些优化算法(优化算法让整个程序发送数据和接收数据没有边界了)
3:tcp的优点是会不丢包,保持跟一个人的长期通信(比如优酷上下载电影走的一般都是tcp协议,
不可能3个g的电影走udp下下来,tcp传3个g对于网络来说拆成无数的包,下载下来后拼接起来)
应用层的协议:应用协议直接就可以用的,用户就可以看到的,基于用户,http(超文本传输协议,网页相关的),ftp(文件传输),smtp(邮件相关的协议)
https(可靠安全,拦截到了数据也看不懂,传输的过程中会对内容进行加密,加密需要买证书,加解密的过程)
osi模型对应协议在哪层
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
tcp触发粘包的两种情况: 情况一 发送方的缓存机制:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包) 情况二 接收方的缓存机制:接收方不及时接收缓冲区的包,造成多个包接收 (客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
# 这里是情况二: client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 9999)) sk.send(b"hello world") server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 9999)) sk.listen() conn, addr = sk.accept() print(conn.recv(2)) # 打印:b'he' print(conn.recv(10)) # 打印:b'llo world'
# 这里粘包原因:不知道发送端发送的数据的长度,所以接收端接收的数据没有把发送端的接受完导致下次接收还能收到上次没有接受完的
如上使用的是tcp,client的一个send发送的一个数据被server这边两个recv接收,两个recv分别接收到部分数据
udp不会有这种情况:如果只发了一个包,那么这一个包只作为一个数据发到server端,发到server端之后如果conn.recvfrom(2)只接收两个数据
相当于udp接收一个数据包的时候只接收两个数据,后面的数据就丢弃了,再recvfrom一次是接收不进来的,
因为需要下一次等send发送数据进来这边才能接收 这就是udp协议的特点,因为是基于数据包的
tcp协议现在这种现象是因为:recv接收端有一个缓存机制,client端确实把长长的数据发送过来了,
在server接收这边的操作系统的内核态的缓存区是有10个数据的,
由于server这边第一次只recv2个数据,所以server这边缓存区先读2个数据出来,还有8个没有读取完还在内核态的缓存区
第二次又recv 10个数据的时候,原本要收10个,现在只有8个,把8个全部读取出来
因为tcp协议接收端这边有个缓存机制,不管这次你recv接收多少,剩下的缓存着,下次recv接收的时候还继续给你,一定不会丢数据-------假设不知道接收多少个就粘包
就是因为接收方的缓存机制,所以第一次没有全部接收,那么第二次接收会把剩下没有接受完的发给我,-----粘包问题
缓存的数据:只要这个tcp链接还在,通过这个tcp发送的缓存的数据就不会丢,顶多超过了缓存范围一定报错,一定不会不声不响的把缓存的数据丢弃 ---这就是可靠的tcp协议
所有的网络传输的本质:都是socket,开网络链接一定走socket
起一个tcp连接或者udp连接必须要开端口,端口是操作系统开了个端口给程序,看上去是程序在通信,实际上中间还架了两个机器的操作系统,
实际上通过两个操作系统之间交互最后找到程序,根据ip协议找到机器,根据端口号找到程序,数据先发给机器,
如果是s发送数据,s先把要发送的数据给自己的操作系统一份,操作系统知道要发数据,替我们把数据传给对面的操作系统
接收的时候c接收数据,先是c的操作系统先接收消息,之后程序recv才从操作系统读取出数据-----这么一个流程
缓存区域和用户态内核态:当client端准备发数据的时候做了两件事:1:把
粘包的情况一:情况二:recv两次,情况一:发送端send两次,接收端只接收一次 server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 9999)) sk.listen() conn, addr = sk.accept() print(conn.recv(1024)) # b'helloworld' client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 9999)) sk.send(b"hello") sk.send(b"world")
发送端发送的两次数据粘在一起被接收端收到了,hello 和world粘在一起发送过来了
tcp内部优化算法,所以hello 和world连在一起发送(连续的小数据包会被合并)
server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 9999)) sk.listen() conn, addr = sk.accept() print(conn.recv(1024)) # b'helloworld' print(conn.recv(1024)) # b''
print(conn.recv(1024)) # b''
print(conn.recv(1024)) # b''
client.py
import socket
sk = socket.socket()
sk.connect(("127.0.0.1", 9999))
sk.send(b"hello")
sk.send(b"world")
上面的代码正常情况s第一个recv接收完全部数据后第二次recv再接收数据应该阻塞的
windows来讲:如果client端没有结束正常的发送信息,如果client结束了他会发送有个空过去给server端
所以这里的server端第二次recv收到的是b''数据为空
s和c本来是建立tcp链接的,当这个c他关闭了之后,说明tcp链接断开了,链接断开后c给s默认发送给空消息过来,也就是b""
这就是为什么s在第二次recv收到一个空消息而没有阻塞等待消息的原因。
s后面写多少个recv没有关系,c断开tcp连接后会一直往s发空消息,所以s这边多个recv都是收到的b""空消息,
c不断开tcp连接就不会不发空消息s这边第二个recv就会阻塞,c断开了tcp连接就会一直往s发空消息,s这边多个recv都不会阻塞
多个send发送小的数据连在一起,就会发送粘包现象,tcp协议内部的优化算法造成的,
本质上:看起来是因为连续使用send引起的,如果不连续send或者两次send的间隔时间大sleep一下就不会粘包了
windows操作系统:win8以后的版本如果发送上面情况会发一个空消息,低版本的windows操作系统直接报错,和不同的操作系统有关,不是报错就是发空消息
12:tcp粘包问题的解决方案
粘包出现的两种情况 1:练习send两个小数据 2:两个recv,第一个recv特别小,紧接着剩下的消息就被缓存起来了
本质上都是一个问题,不知道到底要接收多大的数据
tcp粘包问题解决方案一: 首先发送一下这个数据到底有多大,然后再按照数据的长度接收数据, server.py import socket sk = socket.socket() addr = ("127.0.0.1", 9999) sk.bind(addr) sk.listen() conn, addr = sk.accept() while 1: cmd = input(">>>>>") if cmd == "q": conn.send(b"q") break conn.send(cmd.encode("utf8")) data_len = int(conn.recv(1024).decode("utf8")) print(data_len) # 接受到数据长度data_len,接受到数据长度后再告诉对方一个消息已经收到了数据长度了,对方收到ok确认后就会发数据过来了 # 然后我这边按照数据长度ata_len接收数据即可 conn.send(b"get num ok") ret = conn.recv(data_len).decode('gbk') # subprocess在windows电脑返回的是gbk格式的字节类型,这边拿到的也是gbk的字节码,所以这里需要gbk解码 print(ret) conn.close() sk.close() client.py import socket import subprocess sk = socket.socket() addr = ("127.0.0.1", 9999) sk.connect(addr) while 1: cmd = sk.recv(1024) if cmd == b"q": break ret = subprocess.Popen(cmd.decode("utf8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out = ret.stdout.read() # pipe管道里面的数据只能读一次,再读就没有了,就像队列 std_err = ret.stderr.read() sk.send(str(len(std_out) + len(std_err)).encode('utf8')) # 如果想要往对面发消息先把两次send数据的总长度发送出去,让那边按照这个长度统一接收 # 这样这边就send 3个数据了,3个send连续发送小的数据还是可能会粘包的, # 所以需要send长度后需要收到对方的回应接收长度ok,这样才不会三个send包粘一起 # 第一个send长度被对方接收,我这里就阻塞在recv这里需要接收到对方的确认收到长度的回信才继续send两个数据 # 然后被那边一个recv接收到我这边两个send的全部数据,不会再粘包了 msg = sk.recv(1024).decode("utf8") if msg == "get num ok": sk.send(std_out) # 发送的gbk的字节码 sk.send(std_err) # 发送的gbk的字节码 sk.close()
如上这样写避免粘包的好处 1:确定了我到底接收了多大的数据,现在能够准确收到这个信息了 可能client这边send的数据特别特别长,server这边以前接收1024不够的,接收4096也不一定够了 所以基于这样问题接收端需要知道到底需要接收多少个数据,发送端告诉数据长度来确定了到底接收多大的数据 2:要在文件中配置一个配置项,就是每一次recv的大小 buffer=4096 当我们要发送大数据的时候,recv接收数据的时候可以写任何参数比如1024,4096等,一般情况下recv的量不超过4096个字节 超过这个大小就给自己内存造成负担,一次读取4兆然后处理,假设同时处理多个人来的请求 10个人每次都拿10兆的东西40兆,100个人400兆,很多人的话recv数据量不宜过大,机器可能报废 所以可以在这个地方可以进行一个配置,配置项一般叫buffer(缓冲):一次性能接收多少个字节 buffer可以默认4096,小了可以往大调整 3:当我们发送大数据的时候,要明确的告诉接收方要发送多大的数据,以便接受方能够准确的接收到所有数据 不会发送粘包 4:多用在文件传输的过程当中,大文件传输不能把整个文件都读取出来吗,内存要gg, 一:大文件的传输一定是按照字节读,每一次读固定的字节 文件传输不能一行行读,因为视频文件都算是一行,需要每次read文件读取文件特定大小 文件传输的文件是ftp通用性的,文件视频文本都可以传输,只有文字才有一行一行的概念 图片和视频都算二进制没有一行一行----全部都按照字节读取 二: 传输端:传输的过程当中,一边读一边传(读点传点,这个过程肯定需要连续send) 接收端:一边收一边写(肯定需要连续的recv) 数据很大那么send很多次,recv很多次,肯定不能sleep来避免粘包,速度太慢 send大文件之前告诉你文件大小30000字节,发送端只管send就行,假设每次send(4096) 那么每次send之后就算 剩下没传的字节 - 4096,直到剩下没传的字节减少到0就不传了 recv这个大文件,接收到30000数据大小,再每次假设recv2048个字节, 还有多少数据没有接收 = 30000 - 2048这样一直减,减少到0表示完整被接受完了 这样就是大文件的上传和下载功能, 虽然浪费了传输数据大小和确认的一次,但是对于大文件传输来说不影响整体的速度,总比sleep好 这样才能不会发送内存的过大占用 如上这样写避免粘包的不好的地方: 1:多了一个发送数据大小的交互,必须先发送数据大小对面收到,然后对方发一个ok我这边再收到 这两次动作也需要走网络延迟 send和sendto在超过一定范围的时候都会报错,不能把一个数据全部读取到内存里然后再去进行send, 应该自己做好程序的内存管理 程序的内存管理
tcp粘包问题解决方案二:struct模块解决粘包问题
比方法一更搞级没有浪费多一次的recv和send结果,n次send和recv完美解决粘包问题 struct模块:该模块可以把一个类型,如数字,转成固定长度的bytes 固定长度的bytes,为什么转成固定长度的bytes server.py import socket import struct sk = socket.socket() addr = ("127.0.0.1", 9999) sk.bind(addr) sk.listen() conn, addr = sk.accept() while 1: cmd = input(">>>>>") if cmd == "q": conn.send(b"q") break conn.send(cmd.encode("utf8")) data_len = struct.unpack("i", conn.recv(4))[0] ret = conn.recv(data_len).decode('gbk') print(ret) conn.close() sk.close() client.py while 1: cmd = sk.recv(1024) if cmd == b"q": break ret = subprocess.Popen(cmd.decode("utf8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out = ret.stdout.read() # pipe管道里面的数据只能读一次,再读就没有了,就像队列 std_err = ret.stderr.read() num = len(std_out) + len(std_err) sk.send(struct.pack("i", num)) # 将数据长度这个数转化成一个固定4个字节长度的bytes # 第一次send:一定send4个字节,虽然不知道多大长度,send的4个字节表示了std_out数据长度和std_err数据长度加在一起 sk.send(std_out) sk.send(std_err) sk.close()
使用struct解决粘包:借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
代码的模板如上
可以自己定制更复杂的接收情况:我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,
然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
我们在网络上传输的所有数据都叫数据包
数据包里的所有数据都叫报文
报文里不止有数据,还会带上ip地址,mac地址,端口号等传输
所有的报文都有 报头
协议 报头 接收多少个字节的数据,
协议规定的:先发ip地址还是先放端口号
之前发的所有的报文里面跟ip协议相关的tcp,udp协议相关的其实已经帮我们封装到报头了,只不过帮我们封装的报头是一一对应的
从tcp这一层出去的数据包了TCP相关的报头和报文,接收端解包的时候也通过tcp层的时候解析报头,然后拿走这个数据
数据从应用层一层层往下封装传递,数据都需要加每一层的东西,数据越来越大,往外套了很多东西,接收端接收到这个数据,往上走又经过很多层
被每层解析外面加的信息,越来越端,接收端的应用层拿到的任然是发送端的元素数据-----各个层之间的报文的添加和解析(交换的过程)
我自己也可以定制报文,发的时候数据连着报文一起发过去的,解析的时候任然拿到一堆数据,自己写程序解析自己定制的报文就行
定制报文在一些复杂的应用上就会应用到,远程执行命令不算复杂,比如说传输文件复杂
传输文件带着文件的名字
文件大小
文件的类型
存储的路径
上传下载一个文件把这写东西都传给对方才行,发送文件前接收方得先拿到这些需求才行,所以定制报头,报头也是数据
head = {"fllename": xxx, "filesize": xxx, "filetype": xxxx, "filepath": xxx} 这就是需要传过去的数据
应该做两件事:
下载数据或者上传数据之前:send head报头过去,再send文件,文件可以通过多次send过去 head就是定制的报头,file文件就是报文
发送端 接收端
先发送报头长度,struct模块固定4个bytes字节发送 recv接收4个字节,拿到报头长度
send(head) 传送报头 根据4个字节获取报头长度接收完整报头
send(file) 传送文件 报头获取filesize,然后根据filesize文件大小接收文件
这一个完整的过程就是自定制了一段报头,类似自己定义的协议,是我发送文件和接收文件自定义的协议
tcp特性没有边界,发送的数据需要告诉他要发多长,多次send和多次recv想让数据完全明确没有粘包就要每一次都告诉他即将发送的数据是多少,
通过这样的形式去完成自定义协议
网络传输的过程当中处处是有协议的,协议都有报头和报文,报头等数据的拆装都算操作系统几层协议完成的,
协议就是一堆报文和报头 ----都算字节
协议的解析过程我们不需要关心
如果跳出我我们所了解的端口,ip协议,我们写的程序也需要多次发送数据或者发送多个数据,我们也可以自定制协议 ---协议本质上就是一种约定
struct+json等模块把报头做成字典,字典里包含将要发送的真实数据的详细信息, 然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
收发逻辑如下: 发送时 接收时 先发报头长度 先收报头长度,用struct取出来 再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化 最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 服务端:定制稍微复杂一点的报头 import socket,struct,json import subprocess phone=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: 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.sendall(back_msg) #在发真实的内容 conn.close() 客户端 from socket import * import struct,json ip_port=('127.0.0.1',8080) client=socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd=input('>>: ') 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_size=0 recv_data=b'' while recv_size < data_len: recv_data+=client.recv(1024) recv_size+=len(recv_data) print(recv_data.decode('utf-8')) #print(recv_data.decode('gbk')) #windows默认gbk编码
为什么会出现粘包现象:
tcp协议才会出现粘包现象,
是因为tcp协议是面向流的协议,
在发送的数据传输的过程中还有缓存机制来避免数据丢失
因此在连续send发送小数据的时候以及接收大小不符合数据的时候都容易出现粘包现象
本质还是因为因为我们在接收数据的时候不知道发送的数据的长短
一次send一次recv,且recv的值大于发送的包就不会丢数据发生粘包,只有在接收的小于发送的,且连续send发送这种情况才会出现粘包,缓存机制和优化算法导致的粘包现象
解决粘包问题:
在传输大量的数据之前先告诉接收端要发生的数据大小
如果想更漂亮的解决问题:通过struct模块定制自己的协议,接收方提前知道即将接收的数据大小,长度,接收文件的文件名等,接收方然后接收就行了
13:struct模块
struct模块:该模块可以把一个类型,如数字,转成固定长度的bytes
什么是固定长度的bytes,为什么转成固定长度的bytes
Type:type是能够转化的类型,比如int,long,float,double
主要关注:format:i 处理int数据类型的数据 对应python数据也是int 转化成固定的长度4个字节 形式是以元组的形式返回的
import struct ret = struct.pack('i', 4096) # 'i'表示int,就是即将要把一个数字转化成固定长度的bytes类型 print(ret) # b'\x00\x10\x00\x00' 打印的四个字节 ret = struct.pack('i', 4096000) print(ret, len(ret)) # b'\x00\x80>\x00' # 如果数字过大看起来拿到的不是4个字节 # 看起来拿到的是三个字节,还偷藏了一个">"不是数字大的东西,也占了一个字节,所有len打印长度还是4 # print输出机制:当数字表示不够的时候用一些符号或者字母来代替 # 如果数字太大太长了长到字符和符号都不够用的时候就报错了 # 如果没有报错:正常使用"i"模式pack拿到的结果都是4个字节, # 超过某个长度报错,报错就代表4个字节没有办法处理这么长的数据了 num = struct.unpack('i', ret) print(num, type(num)) # (4096,) <class 'tuple'> 得到一个number的元组 print(num[0]) # 4096 如果发送数据的时候发送方需要先发送长度 接收方先接收长度 以前方式一:不struct模块发送数字长度不知道这个长度比如10比如100比如1000转化成数字后多少字节 以前版本:发送方发送长度之后,需要recv一次收到接收方的ok消息 这里recv是为了防止粘包,接受到的ok消息并没有使用到,但是这步省了就粘包了 之所以粘包是因为接收方接收num数据长度的时候不知道num数据长度有多长:data_len = int(conn.recv(1024).decode("utf8")) 如果接收方知道发送方发送的数据的number的长度一定是4,那么接收方不需要send ok确认了 发送方也不需要多recv一次这个ok确认消息, 因为可以:data_len = int(conn.recv(4).decode("utf8")) 这样一次性拿到4个字节,比如4个字节就是2048这个数字,数据长度就是2048,接收方下面不需要send ok出去了 因为知道的数据长度转化成固定的4个字节,接收方先就收到4个字节 发送方:先发送4个字节数据长度 然后发送stdout和stderr 接收方:接收4个字节数据长度 然后按照数据长度接收全部的stdout和stderr 这样就不会有问题了:主要是因为struct模块能转化成固定长度的bytes
import json,struct #假设通过客户端上传1T:1073741824000的文件a.txt #为避免粘包,必须自定制报头 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 #为了该报头能传送,需要序列化并且转为bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #然后发真实内容的字节格式 #服务端开始接收 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 header=json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
关于struct的详细用法 #_*_coding:utf-8_*_ # http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html __author__ = 'Linhaifeng' import struct import binascii import ctypes values1 = (1, 'abc'.encode('utf-8'), 2.7) values2 = ('defg'.encode('utf-8'),101) s1 = struct.Struct('I3sf') s2 = struct.Struct('4sI') print(s1.size,s2.size) prebuffer=ctypes.create_string_buffer(s1.size+s2.size) print('Before : ',binascii.hexlify(prebuffer)) # t=binascii.hexlify('asdfaf'.encode('utf-8')) # print(t) s1.pack_into(prebuffer,0,*values1) s2.pack_into(prebuffer,s1.size,*values2) print('After pack',binascii.hexlify(prebuffer)) print(s1.unpack_from(prebuffer,0)) print(s2.unpack_from(prebuffer,s1.size)) s3=struct.Struct('ii') s3.pack_into(prebuffer,0,123,123) print('After pack',binascii.hexlify(prebuffer)) print(s3.unpack_from(prebuffer,0))
struct模块:
常用的方法:pack unpack
常用的模式:"i"
pack之后的长度:4个字节
unpack之后拿到的数据是一个元组:元组的第一个元组才是pack的值
14:简单练习
# 网盘 # 文件的上传 下载 # server端和client端 # client请求往server端的某个地方上传东西,请求从某个路径下载一个东西到本地 # 登录:客户端登录,登录的信息放在服务端,将用户名和密码发给服务端, 服务端确认信息之后就可以上传下载了 # 这个过程一定是tcp连接,如果是udp连接发过去登录成功了,没有然后了,一定是一个长链接,登录成功后就可以进行上传下载了
# 选择上传/下载
# server端就知道即将要上传文件了,需要告诉server端要传一个文件,读取文件之后一行行输出,输出之后再s端统一接收进来,
接收进来之后server端写在一个文件来这样实现了一个上传下载了
# 上传:选择要上传的文件路径,在server端创建一个同名的空文件,创建同名文件的时候 同名文件.cache 短暂的缓存一下,等全部上传之后再改回来
# 下载:选择要下载的文件路径,server端的文件路径,在client端创建一个同名的空文件准备下载
15:tcp+struct实现一个大文件的上传或者下载
server.py import socket import struct import json sk = socket.socket() addr = ("127.0.0.1", 9999) buffer = 4096 sk.bind(addr) sk.listen() conn, addr = sk.accept() head_len_bytes = conn.recv(4) # 1:接收4个字节的报头长度 head_len = struct.unpack("i", head_len_bytes)[0] head = json.loads(conn.recv(head_len).decode("utf8")) filepath = head["filepath"] filename = head["filename"] filesize = head["filesize"] with open(filename, "wb") as f: while filesize: if filesize >= buffer: content = conn.recv(buffer) f.write(content) filesize -= buffer else: # 如果剩余的数据不到4096了就读取剩下的数据 content = conn.recv(filesize) f.write(content) break conn.close() sk.close() client.py # 发送端 import socket import struct import json import os sk = socket.socket() addr = ("127.0.0.1", 9999) buffer = 4096 sk.connect(addr) # 发送文件:发定制报头,发报头长度,发数据的过程 # 这里传一个视频 head = {"filepath": r"C:\Users\ywt\Desktop\zzzzzzzzzzzzzzzzzzzzzzz", "filename": r"视频.mp4", "filesize": None} # 请用户输入想要下载的视频在那个位置,然后到位置上去拿对应的视频就行了 # 然后拿到文件之后计算文件的大小:os.path.getsize file_path = os.path.join(head["filepath"], head["filename"]) filesize = os.path.getsize(file_path) # 拼接路径,自己中间拼接一个\ 也可以,但是只适合windows,不能跨平台, # 使用os.path.join来拼路径可以跨平台 head["filesize"] = filesize # filesize放回字典里 # 网络上发送head这个字典使用json模块 json_head = json.dumps(head, ensure_ascii=False) # 字典转化成了字符串 bytes_head = json_head.encode("utf8") # 字符串转bytes # 计算head的长度,对面接收的时候先接收一个长度,就知道head多长了,计算bytes类型的长度,因为最终发送的是bytes类型 head_len = len(bytes_head) # 报头的长度 head_len_bytes = struct.pack("i", head_len) # 把报头的长度转化成4个字节的bytes sk.send(head_len_bytes) # 1:先发head_len_bytes报头长度 sk.send(bytes_head) # 2:再发bytes类型的报头 # 3:最后发送报文,打开文件 with open(file_path, "rb") as f: while filesize: if filesize >= buffer: # 如果大小大于4096个字节,每次就读取一个4096 content = f.read(buffer) # content就是文件中每次读取出来的内容 sk.send(content) # 读取出来的字节发送出去 filesize -= buffer else: # 否则说明剩余的未读文件不到4096个字节了,那么有多少读取多少就行 content = f.read(filesize) sk.send(content) # 读取出来的字节发送出去 filesize = 0 break # 读取到else这了,说明filesize未发的数据小于4096字节了,一次性读取剩下的发送出去说明文件发完了 # 发完之后break了,break之后整个文件就发完了 sk.close() 发送端先把数据发送到操作系统内核,操作系统把数据发送到对面的操作系统,然后接收方从操作系统拿到程序当中来
可能存在的问题: 有的机器buffer设置过大设置成4096代码运行可能出问题,机器性能造成的 发送方read和send 接收方recv和write 如果接收方不recv发送发send发的数据都在接收方操作系统缓存机制里, 文件的读操作速度快,写的速度慢 假设接收方read和send花0.01s 假设接收方recv和write花0.02s 这样可能一次性发了好几倍的数据接收方才write写了一条,接收方缓存机制就会缓存太多数据了, 数据太多可能导致本次tcp断开,所以client端发送数据就报错了 buffer换成1024可能就正常了: 1024数据量小,设置4096假设0.01s发送4096数据,现在0.01s才发1024个数据 写文件过程慢,中间有网络延迟又有代码执行的时间可能就来得及write写数据了,接收方缓存区不会爆炸 正常情况下0.01s只能写1024个字节这样的写的速度,如果传4096那么来不及写完 所以改成小的buffer可能就正常了 报错原因猜想:代码逻辑没问题,文件的读写是有速度差异的
16:验证客户端的合法性
网络上有很多恶意链接
假设访问百度输入用户名和密码(百度也有账号,登录的时候一般情况下):
假设http://www.baidu.com?username=xxx,password=xxx
这个地方传输的是一般是密文的username和password这样数据被拦截也也不会丢失
如果有人想模仿你的机器的url:也可以发一个跟你一模一样的:http://www.baidu.com?username=xxx,password=xxx,模仿你的url就能访问对方了
socket来讲客户端想要连接server端只需要端口号和ip地址,但是有一些不合法的client端连接server端
现在验证检测客户端是否合法,
检测客户端是否合法:
不依靠登录认证,你拿着我的客户端才能用,不能谁写一个客户端都来用
我写的客户端和我写的server端之间有一个检测合法性的机制 借助:hmac模块实现
hmac模块参考:https://www.liaoxuefeng.com/wiki/1016959663602400/1183198304823296 import hmac # hmac和hashlib模块类似,hashlib模块也能做合法性验证 h = hmac.new() # 参数1:secret_key:密钥 # 参数2:msg:想进行加密的bytes数据
# 参数3:digestmod:加密方式 # 拿到这两个参数可以创建一个对象,这个对象会对secret_key和msg做一个 密文的内容 = h.digest() # h.digest()拿到一个密文的内容 hmac.compare_digest() # compare:对比 对比密文和另外一个密文 当一个客户端访问的时候,有了conn之后两边建立tcp链接 这个时候server端主动发起send一个数据给client端,send一个想加密的bytes类型数据 (随便send一个数据就行了,比如send一个egg) client端收到egg这个数据,使用hmac模块,假设server端和client端有两个完全一样的secret_key假设potato secret_key:potato是客户端和服务端的秘密,别人都不知道, 可以使用hmac对发过来的数据egg以及potato进行一个加密,加密之后拿到一个密文, client把这个密文返回回来,server端也可以对数据egg以及potato进行一个相同的加密 使用compare去对比server端我自己加密的密文和client发过来的密文是否相同,如果相同说明是合法的客户端 就可以发送一些请求互相响应了,不相同的话server端可以主动的发起close,那么客户端也会被迫断开 通过hmac加密机制来实现验证客户端的合法性 其实和加盐的hashlib验证差不多的
验证客户端的合法性代码
server.py import socket import hmac import os addr = ("127.0.0.1", 9999) secret_key = b"egg" def check_conn(conn): msg = os.urandom(32) print(msg) # 每一次urandom拿到的值都不一样 conn.send(msg) # send数据过去后client端接收到了会拿这个数据进行hmac加密.我这里也进行加密 h = hmac.new(secret_key, msg, digestmod='MD5') # secret_key和msg两个参数都必须是bytes类型 print(h) digest = h.digest() # h对象调用digest这样拿到加密之后的数据digest client_digest = conn.recv(1024) return hmac.compare_digest(digest, client_digest) sk = socket.socket() buffer = 4096 sk.bind(addr) sk.listen() conn, addr = sk.accept() res = check_conn(conn) if res: print('合法的客户端') conn.close() else: print('不合法的客户端') conn.close() sk.close() client.py import socket import hmac secret_key = b"egg111" # 这个事先已经知道的,server端和client端共有的秘密, # 假设不知道这个合法的密钥那么登录进去肯定不是合法的客户端 # 还可能不知道加密方式,这里hmac加密的 # 这样是链接相对的安全,建立在只有我的客户端能和和服务端通信 # 所以能够检验客户端的合法性了 addr = ("127.0.0.1", 9999) sk = socket.socket() sk.connect(addr) msg = sk.recv(1024) # 拿到客户端发来的32位的bytes类型msg数据 h = hmac.new(secret_key, msg, digestmod='MD5') client_digest = h.digest() sk.send(client_digest) sk.close()
os.urandom(32)的用法::随机生成32位字节 import os import hmac print(os.urandom(32)) os.urandom(32):随机生成一个32位的字节,使用这个数据当我想加密的bytes,想加密的bytes发什么都行,发这个更保险,谁也不知道发的什么 # b'\x91\xa4\x9d\xb5\xe7\xfe{\x7fH\xe581\x08\xf5\xcc\x84\xeb\x1e@\x19^\x80\xaa\x11\xab\xd0\xf3h"\x03i\xfe' secret_key = b"egg" msg = os.urandom(32) h = hmac.new(secret_key, msg, digestmod='MD5') print(h.digest()) # b'\x7f\xce\x89\xc4+2\x98\x07\x0c\xbc{\x01\xbf\xaeA8'
17:socketserver模块
socket:
socket起一个tcp服务,同一时间只能和一个客户端通信
socketserver起一个服务端可以实时的多个客户端通信了
socketserver在socket的基础上进行了一层封装,python里很多这种模块
2.x版本里:SocketServer
3.x版本里:socketserver 现在全部变成小写了
socketserver模块的简单使用
server.py import socketserver class MyServer(socketserver.BaseRequestHandler): # BaseRequestHandler:处理请求的基础类,必须继承这个类 def handle(self): # 必须重构handle函数 # self.request相当于一个conn while 1:
# 原来socket写的程序里所有和conn有关的操作都挪到hande方法里,一旦客户端断开,handle方法也应该结束 msg = self.request.recv(1024).decode("utf8") if msg == "q": self.request.close() # 客户端close关闭了tcp链接,服务端自动关闭的机制 break print(msg) info = input(">>>>") self.request.send(info.encode('utf8') if __name__ == '__main__': server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), MyServer) # 参数1:socket起服务的ip和端口 # 参数2:把我们写的MyServer类传进去 # ThreadingTCPServer是一个类,这里返回一个server实例化对象 # socketserver里面一个起多线程,起tcp服务的类实例化一个对象 server.serve_forever() # 永远的起一个服务,server就需要一直运行 Thread:线程,一个程序里正常情况下运行起来一个线程,线程就是调度cpu的最小单位 每一个线程可以请求cpu给我工作的时间,只有只有线程才能占用cpu 正常情况下不可能能做到起一个socket跟很多人通信,因为占着这个线程一直使用 起多个线程去接收多个不同的请求,就能实现并发的效果。 ThreadingTCPServer就是引入了线程的概念去实现并发效果 socketserver:同时处理多个客户端的请求,socketserver在socket底层基础上做了一层封装,帮我们实现并发的效果 client端不需要做这些事情处理多个请求,没有clientserver client使用正常的socket起就行 client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 8080)) while 1: msg = input(">>>>") if msg == "q": sk.send(b"q") break sk.send(("client1:"+msg).encode("utf8")) ret = sk.recv(1024).decode("utf8") print(ret) sk.close()
socketserver源码解析: class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
ThreadingTCPServer什么都没干,只是继承了2个类:ThreadingMixIn,TCPServer
ThreadingMixIn:
实现了了3个函数
1:process_request_thread
2:process_request
3:server_close
server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), MyServer)
ThreadingTCPServer类实例化,需要调用ThreadingTCPServer的init,ThreadingTCPServer内部没有init方法
ThreadingMixIn这个类里也没有init,
TCPServer类里有init
TCPServer继承BaseServer
TCPServer类里的init方法执行之前先调用了BaseServer的init构造方法
TCPServer的init的参数:
server_address:ip+端口
RequestHandlerClass:类名
BaseServer.__init__(self, server_address, RequestHandlerClass)
调用父类的init,把ip+端口发送过来,把类传递过来,
self.socket = socket.socket(self.address_family, self.socket_type)
创建一个socket对象,
self.server_bind()
绑定ip和端口
self.server_activate()
listen监听
这里TCPServer的init执行完后拿到一个server对象,这个server对象初始化做了一堆绑定监听,起服务
接收请求的服务在:server.serve_forever() 这个函数,
windows还是linux机器上监听机制:selector.select
监听的对象(socket对象)如果有人来找我了,监听机制有反应的,有人连接有人发消息这边都能感知到接收到
监听,感知到有人连我,如果有人连我的时候我就第一时间得到通知别人连我
self._handle_request_noblock() 这个方法在BaseServer类里
self.get_request()
get_request是TcpSever类里的方法:request, client_address = self.get_request()
return serf.socket.accept() # accept接收到服务器的链接生成return返回conn,addr
所以requrst就是conn,client_address就是接收的客户端的地址
self.process_request(request, client_address)
process_request在ThreadingMixIn这个类里
t = threading.Thread(target = self.process_request_thread, args = (request, client_address))
启动多线程,线程里执行self.process_request_thread函数
self.process_request_thread:
执行:self.finish_request(request, client_address)
finish_request方法在BaseServer类里
finish_request里执行self.RequestHandlerClass(request, client_address, self)
RequestHandlerClass这个类就是我传参进来的类,如上MyServer类
传进来的类进行实例化,相当于走了自己定义的类,自己类没有init方法找BaseRequestHandler的init方法
self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() # 调用自己定义的Mysever类的handle函数 finally: self.finish()
看源码要点:
多个类之间的继承关系要先整理
每一个类中有哪些方法,要大致列出来
所有的self对象的调用要清楚了解到底是谁的对象
所有的方法调用要退回到最子类的类中开始寻找,逐级向上
18:ftp传输文件基础练习
需求分析: 1. 多用户同时登陆 socketserver 2. 用户登陆,加密认证 hashlib 3. 上传/下载文件,保证文件一致性 md5验证 4. 传输过程中现实进度条 内置函数print方法, 5. 不同用户家目录不同,且只能访问自己的家目录
c://server/upload/alex 别人上传上来的文件都存这里,alex用户登录需要有个alex这样的家目录,
alex登录上来只能看他目录下的内容,不能看别人下面的目录,每个人都有属于自己的家目录 6. 对用户进行磁盘配额、不同用户配额可不同
电脑500g硬盘,alex充了会员,买了50个g得让别人多存
liergou没钱,只有2个g空间,存满了不能让他再存了
上传文件之前就需要计算还剩下磁盘配额,上传得内容超过磁盘配额不能让他传了,让他冲会员 7. 用户登陆server后,可在家目录权限下切换子目录
c://server/upload/alex:知道了这个目录,可以再这个目录下新建一个子目录,比如c://server/upload/alex/son1/son2
从son2返回上一个son1,从son1返回上一级alex,不能从alex再返回上一级,alex就是家目录,
如果能返回alex上一层可能创造到别人得目录下,影响别人得数据,不能穿家目录,只能在家目录任意切换
8. 查看当前目录下文件,新建文件夹 9. 删除文件和空文件夹 10. 充分使用面向对象知识 11. 支持断点续传
网gg了再次上传文件,就询问你是不是接着之前的传
各种原因上传失败了就需要续传,只考虑客户端断了,一般情况下服务端不会断
ftp文件上传下载的流程图
登录:
登录的加密
ftp:ftp谁都能用
加密:server端还是client端进行加密
谁都能使用,客户端不安全的,谁都能看到源码,python不需要编译的解释型语言,客户端是不安全的,客户可以直接看到源码
客户看到源码就能看到加密规则,
密文存储密码,别人把数据库拿走,所有用户名对应的密文的密码被别人拿着,又知道加密规则,
可以通过通过撞库手段获取一部分简单密码人的信息,不安全
如果server端client端只能选择在一端进行加密,首选server端
client端也进行密文的加密,密文的加密保护用户的消息不被拦截的信息去泄露,
client md5密文 密文发送到server端 server端拿到md5的密文再做一些其他事情,可以对密文再做一层加密,让结果绝对安全,这样增加了服务器的复杂性
java语言的话,可以选择在client端做加密,java语言需要编译一次然后去执行,python语言全部是明文写的,都能看到源码看到加密手段,就没有任何隐私
client端密文——>密文传server端——>server端拿到密文——>server端再去对这个密文再次摘要(加盐)——>拿到的值和数据库的数据做匹配
所有的操作目录放在server目录切换放在server端才能做到操作系统的兼容
19:网络编程总结
互联网协议:七层:osi协议
互联网协议就是在网络当中的每一个机器都要学会的协议,每一台机器上都有这五层协议,7层协议都会
网络设备来讲也会参与一些协议
交换机:数据链路层协议和物理层协议,交换机也有两层协议
路由器:基于ip做消息转发,路由器能支持网络层,数据链路层,物理层三层协议
计算机:计算机osi7层协议都学了,网络设备来讲只学用到的协议
加用路由器/三层交换机:我们用的家用路由器一般是个三层交换机,三层交换机既有路由功能又有交换功能
四层路由器:路由器一般全部工作在下三层,四层路由器是基于协议通信的,四层路由器能够解析数据包解析tcp和udp的协议
一般请求正常是自己的机器解析数据包知道什么协议走什么端口。四层路由器直接就能解析数据包知道什么协议和端口号,
直接就能发送到对应的ip地址和端口的机器上
缩减版的五层:
应用层:我们写的python应用,应用层的协议拿过来就用,不需要关心底层发生什么,比如http协议,本质上http协议也是使用socket连接两端的,不关心是tcp还是udp协议
传输层:tcp协议和udp协议
网络层:ip协议 本层硬件:路由器,三层交换机
数据链路层:arp协议(通过ip找mac地址,交换机帮忙两台主机完成arp) 本层硬件:二层交换机
物理层:本层硬件:网卡,双绞线
交换机:广播(所有人都发,广播就需要使用arp协议),单播(只发一个机器),组播(局域网100台机器,一组机器10台,往这一组10台发)
ip协议:
ip地址:规定了ip地址(一台机器在网络内唯一的标识)格式,
子网掩码:ip地址与子网掩码做按位与运算得到的结果是网段地址
网关ip:不同局域网之间的两台机器想要通信,局域网的机器a正常是出不去的,
机器a发生请求请求一个公网地址其实通过网关ip帮助传话访问的
网关ip就是局域网内的机器访问公网ip就通过网关访问
tcp:基于长链接,面向流,可靠的,全双工通信,三次握手,四次挥手,tcp链接建立之后client端和server端都可以先发送消息,tcp粘包特点
udo:面向数据包,不可靠,udp服务起来不需要建立连接就可以通信发送消息,udp第一个消息一定是等着客户端发给服务器的,这样服务器才知道是谁跟他通信发消息的
粘包:tcp面向字节流的通信协议没有收发边界,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
情况一:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
情况二:接收方的缓存机制:接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
怎么解决粘包:
1:发送信息之前先告诉对方要发的数据有多大
2:struct模块将要发送的数据大小固定化,无论如何就发4个字节,对方固定接收4个字节就行
3:字定义协议的概念(基于应用层自己定制的)
处理并发问题:socketserver模块
1:一定继承socketserver.BaseRequestHandler
2:一定实现handle方法
3:类建立起来怎么起服务
验证客户端合法性:hmac
20:socket的更多api介绍
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据
s.sendall() 发送TCP数据
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() 创建一个与该套接字相关的文件
send和sendall方法: 官方文档对socket模块下的socket.send()和socket.sendall()解释如下: socket.send(string[, flags]) Send data to the socket. The socket must be connected to a remote socket.
The optional flags argument has the same meaning as for recv() above.
Returns the number of bytes sent. Applications are responsible for checking that all data has been sent;
if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。 socket.sendall(string[, flags]) Send data to the socket. The socket must be connected to a remote socket.
The optional flags argument has the same meaning as for recv() above. Unlike send(),
this method continues to send data from string until either all data has been sent or an error occurs.
None is returned on success. On error, an exception is raised, and there is no way to determine how much data,
if any, was successfully sent. 尝试发送string的所有数据,成功则返回None,失败则抛出异常。 故,下面两段代码是等价的: sock.sendall('Hello world\n')
sendall:一次性把所有的数据都发送过去,但是发送多了可能不成功
buffer = 'Hello world\n'while buffer: bytes = sock.send(buffer) buffer = buffer[bytes:]
send:send一次不一定把所有的数据都发送过去,可能中间发生拆包了,分次发,但是数据不会丢的,数据只要没有发完就会尝试去发
当数据足够长使用send和sendall都会报错,但是一般操作的时候不会允许这么多数据在内存之中出现,不要这么操作
更建议使用send
s.setblocking():
设置套接字的阻塞与非阻塞模式,正常情况下s.accept()会阻塞代码接受到一个链接进来,如果没链接进来就一直等,这是socket默认做的设置,包括recv也是这样
如果setblocking这个地方设置了一下,让他不阻塞,那么即使没有链接进来也会不阻塞,会从accept哪里继续往下走
import socket
sk = socket.socket()
sk.setblocking(False) # 设置成False就不阻塞
sk.bind(("127.0.0.1", 9999))
sk.listen()
try:
conn, addr = sk.accept()
except BlockingIOError:
print("[WinError 10035] 无法立即完成一个非阻止性套接字操作。")
print('------------------------')
# accept这里想获取一个链接,如果到这里没有获取到链接,那我这里也不等待,直接去做接下来要做的事情
# 设置了sk.setblocking(False)不止accept不阻塞了,recv也不会阻塞了,全部该阻塞的地方都不阻塞了