Python网络编程--学习记录

1. 网络通信原理:

1.1 CS和BS架构

Python网络编程--学习记录_第1张图片

客户端软件要是想将数据交给服务端,它就必须调用计算机硬件(网卡),让网卡将数据发给服务端计算机的网卡.服务端的计算机网卡,将数据交给它的操作系统,再交给服务端软件.这样就是完成了数据的传输.

这个过程是主动的,当服务端网卡收到数据的时候,会被操作系统放入内存.而服务端软件会主动向操作系统发起系统调用.问操作系统有没有我的数据.操作系统说有,服务端软件就可以拿到它要的数据了,占用的内存就可以回收了.

BS本质上也是CS架构,只是将浏览器当作客户端..BS有一个好处,就是开发的时候,只需要开发服务端.用户打开浏览器之后,只需要输入网址(对应服务端的ip地址).就可以通过浏览器和服务端进行通信了.

1.2 物理连接介质

不管是CS架构还是BS架构,都是需要网络进行通信的,而我们的电脑要想进行网络通信,一定需要物理连接介质.

网卡每台电脑都有网卡,不论是有线网卡还是无限网卡

交换机:可以连接电脑,路由器,存在主力交换机性能比较好

光猫:实现电信号和光信号的转换.

Python网络编程--学习记录_第2张图片

1.3 七层模型

1.3.1通信存在的问题

网络通信,不是简单的连接网线就可以实现通信.还存在很多问题:

多系统:我们使用的设备很多,每种设备使用的系统不同

多介质:联网的方式有很多,有的是通过网线,有的是通过WiFi,这是不同的介质

目标问题:一台电脑发送数据如何准确的发送到另一台电脑上面去.

多软件同时使用网络:如何避免在微信上面发送的消息,不会发送到QQ上面去

要想数据进行传输,网络就要进行设计,无论是光纤,网线还是WiFi,只要大家遵循相同的协议,数据就可以进行传输了.网络上有很多协议但是不论哪种协议,都要遵循osi七层模型进行设计..就是用这个七层模型作为参考来设计协议.

Python网络编程--学习记录_第3张图片

1.3.2 内容

应->表->会->传->网->数->物 

Python网络编程--学习记录_第4张图片

osi 七层模型

物理层:解决信号转换的问题

不论在信号传递的过程中使用的什么信号,光纤,网线还是WiFi,最终都是会转换成为数字信号(0101).当我们的数据往外发的时候,网卡会将数字信号转换为电信号.如果使用的光纤,电脑上面就是会存在光纤网卡,将数字信号转换为光信号.

数据链路层:mac地址,全世界唯一,身份证号,解决发给谁的问题

每一块网卡上面都存在mac地址.mac地址全世界唯一,就解决了目标问题.数据就不会发错人了.

网络层:ip地址,主要用于定位,解决发到哪里的问题

IP地址分为两种公网ip和内网ip,IP主要用于定位的,今天在安徽联网,公网就是安徽的,明天在重庆联网,公网ip就是重庆的

        公网ip 全世界唯一 类似于快递的收货地址

        内网ip 局域网唯一 类似于酒店的房间号码,在不同的内网里面就可以存在相同的内网ip

传输层:解决的是使用什么方式发 

tcp协议:可靠,速度慢,每次发送一个包,都会进行一次确认,适合长距离传输

UDP协议,不可靠,速度快, 只管发送,收没收到它 ,适合短距离传输

端口:可以让一台计算机上面的多个程序同时使用网络,当某个APP发送数据的时候可以加上端口号,接收数据的时候就可以通过端口号,知道传输的数据是哪个程序使用的了

会话层:决定什么时候开始发,什么时候开始断.

我们在正式发送数据包的时候,如果是使用TCP的话,会话层会先发送一个探测包,探测网络是否畅通.如果网络不通,后面的数据就不用再发送.如果是通畅的,就进行拆包.加上编号,拆成一个个小包一个个发送.

对于目标计算机而言,就是一个个收包.再合成起来.并且告诉对方,数据收集完成,可以断开连接了.

表示层:

用来描述文件类型,将我们发送的那部分文件前面加上文件类型

应用层:

就是我们的程序或说进程,用QQ或者微信传输文件,这些文件本质上面都是二进制,拿到这些文件知道该如何打开呢?

基于这个模型设计出来的协议非常多,使用最多的一个叫做TCP/IP协议.

Python网络编程--学习记录_第5张图片

1.4 封包和解包

TCP/IP协议是五层模型,它将后三层合并成为了应用层.

如何理解协议:?用来规定数据的组织格式的.

协议:数据+头部

封包的过程就是给数据加上头部,拆包的过程就是去掉头部,拿到真正的数据.

封包:

1)应用层产生了一段数据,会根据自己的协议套一个头部,先统称为数据

2)传输层会加一个头部叫做TCP头部,如果数据很大的话,会拆成很多个小包,就是很多的头部+数据

3)到网络层会再加上一个ip头部

4)数据链路层有一个以太网协议,再加上一个以太网头部

5)传到物理层时,将整个数据包进行信号转换,将数字信号转换为电信号,WiFi就是转为电磁波信号

通过网线将信号传给对方的计算机,对方计算机按照同样的协议反向操作

解包:

1)从物理层到数据链路层,计算机会看以太网头部,这个以太网头部里面存的就是mac地址,如果mac地址是找我的就将数据收起来,将以太网头部去掉,继续传给网络层

2)这个时候网络层就会看ip地址,ip地址是找我的,就将ip头部去掉,继续传给传输层

3)传输层就会看端口,看数据是传给哪个进程的,将tcp头部也去掉,应用层就可以拿到自己的数据了.

Python网络编程--学习记录_第6张图片

1.5 抓包

ping命令就是一个包探测器,它可以探测本机和目标计算机的网络是否通畅 

1.6 五层模型

1.6.1 物理层

一组数据称之为一个bit流

物理层是基于电信号,光信号和电磁波信号进行通信的.但是如果仅仅只是01011000这种代码是没有任何意义的.所以这种信号必须有头有尾,人们才能理解其含义.才能将01101的数据映射成为我们的数据.

1.6.2 数据链路层

Python网络编程--学习记录_第7张图片

存在一个以太网协议Ethernet,我们学习一个协议,就是看它里面规定了哪些内容.此协议规定

一组数据称之为一个数据帧,一个数据帧分为两部分(头部)+(数据:网络层所有的内容) 头部里面就是(发送者mac,接收者mac,类型),以太网里面的发送者和接收者指的就是mac地址.所以说设备要联网必须有网卡,每一块网卡都有全世界独一无二的mac地址,这就是以太网协议规定的.

Python网络编程--学习记录_第8张图片

里面的数据包含了前面所有层打包之后的结果,它的长度是1500Byte,如果长度超过了则会进行切片.这样一个数据帧既有头也有尾,就解决了前面的问题.在看似大量没有意义的二进制中读取一个个数据帧.

工作方式:广播

所以有了以太网协议之后,它使用广播的方式,理论上就可以实现全世界的计算机通信了.但是前提是全世界的计算机都是在一个小房间里面,这个小房间就相当于广播域,在广播域里面的计算机,通过广播通信,都可以接收到(暂时理解为连接到同一个交换机上的计算机).即使是有这么大的交换机也会有新的问题.

广播包的特点:

会发给广播域内所有的计算机,如果在这个广播域内所有计算机都在发送信息,这个数据量就是灾难性的,被称为广播风暴

1.6.3 网络层

1.6.3.1 广播分域

网络层存在的作用就是把全世界的计算机,放到一个个小小的广播域(局域网)里面.现在在自己的局域网里面广播,其他广播域里面的计算机是接收不到的.但是现在又带来了新的问题,如何跨局域网通信?

1.6.3.2 跨局域网通信

Python网络编程--学习记录_第9张图片

网络层规定,每个小房间都应该有一个出口(网关),网关应该有两个地址,一个是局域网地址,我们局域网的设备要找网关,就找这个地址.还有一个对外的地址,这个对外的地址是和公网联通的

A局域网数据->A局域网网关->公网->B局域网网关->B局域网电脑

1.6.3.4 协议: IP协议

一组数据称之为一个数据包,一个数据包分为两部分(头部)+(数据:传输层所有的内容) 头部里面就是(发送者ip,接收者ip,类型)

1.6.3.5 NAT技术

IP地址:ipv4,每一个ip地址都是由32位二进制组成,为了方便记忆,又将32位二进制分为四组 ,每一组中都有256种变化.全世界的ip地址一共大约是43亿个.很明显现在的对于全世界而言这些ip地址是不够用的.现在有两种解决方案1)让一部分ip地址可以重复使用->nat技术(Network Address Translation),把一部分ip地址专门给局域网使用,所有的内网计算机都使用这一部分ip地址.

当我们的数据包要发公网的时候:

局域网数据->网关(做一次nat地址转换,将内网ip转换为网关的公网ip)->公网(公网ip是全世界唯一,这样就可以找到正确的发送路径)

数据回来的时候还是通过nat转换将公网ip转为内网ip.

00000000.00000000.00000000.00000000 => 0.0.0.0 (最小的ip地址)

0.0.0.0

0.0.0.1

...

0.0.1.255

....

11111111.11111111.11111111.11111111 => 255.255.255.255 (最大的ip地址)

1.6.3.6 内网网段

Python网络编程--学习记录_第10张图片

10.0.0.0~10.255.255.255 一千六百万

172.16.0.0~172.31.255.255  104万

192.168.0.0~192.168.255.255 65536个255*255

127.0.0.0~127.255.255.255 保留地址,如果数据包是127开头的ip地址,那么这个数据包不会离开本机,一般用于测试

只要不再以上内网网段的ip都是属于公网ip

1.6.3.7 IPV6

为了解决ip地址不够用的问题,还有一种方式就是使用ipv6,分为8组,每组4个16进制数

 fe80::8c9:8240:e329:b107%en0,::表示省略,128位的ip地址的数量多到无法想象

1.6.3.8 子网掩码

子网掩码的长度和ip地址的长度一样,也是由32位二进制组成,ip地址必须和子网掩码配合使用.或者说合法的ip地址,就是和子网掩码组成的

为什么要有子网掩码?

为了解决ip地址不够用的问题,划分了很多局域网.而子网掩码配合广播域使用就是为了区分不同的广播域..单纯的ip地址并不能让我知道自己在哪个广播域里面,所以必须配合子网掩码.如果不是在同一个网段即便是物理线路联通,它们也是不可以通信的.

192.168.3.88/255.255.255.0 => 11111111.11111111.11111111.00000000

192.168.3.88/24 =>就是24个1

1.6.3.9 子网掩码区分广播域

子网掩码的前24位都是1,表示前24位都不变,这24位就是用来表示网络位的,网络位不可变.主机位是可以变的.所以子网掩码就是用来固定网络位的.只要是网络位相同的ip地址,他们就是属于同一个网段,在同一个网段下,只要是物理线路,它们就可以通信.

255.255.255.0 => 11111111.11111111.11111111.00000000

192.168.3.88 => 11000000.10101000.00000011.01011000

                             [                  网络位                    ] [主机位]

192.168.3.0 => 11000000.10101000.00000011.00000000 作为网络号使用,用来标识这个网络 第一个ip地址

192.168.3.1 => 11000000.10101000.00000011.00000001 第一个可用ip地址

......

192.168.3.254 => 11000000.10101000.00000011.11111110 最后一个可用ip地址

192.168.3.255 => 11000000.10101000.00000011.11111111  作为广播地址使用 最后一个ip地址

192.168.3.125/25和192.168.3.130/25两台主机物理线路连通之后,能否直接通信?

192.168.3.125/25 => 11000000.10101000.00000011.01111101

192.168.3.130/25 => 11000000.10101000.00000011.10000010

1.6.3.10 子网掩码计算网络位

ip地址和子网掩码按位与运算(同时为1才为1,否则为0)

192.168.3.125/25 => 11000000.10101000.00000011.01111101

                        25 =>  11111111.11111111.11111111.100000000

                                    11000000.10101000.00000011.00000000

ip计算器

Python网络编程--学习记录_第11张图片

1.6.3.11 ARP 协议 
1.6.3.11.1 引入ARP协议的原因

局域网通信,使用二层交换机(将数据解析到第二层,数据链路层,数据层的头是mac地址,所以二层交换机拿到我们的数据之后,它可以看到我们的mac地址,交换机看到mac地址,就知道这个数据该发给谁了.二层交换机解析不到第三层,也就是说它解析不到ip地址,所以只是知道ip地址,是没有办法将数据发给对方的)交换机需要知道对方的mac地址,才知道下一步将数据发给谁,否则只能广播给每个人都转发一份.这个时候就需要用到ARP协议(地址解析协议),他工作在二层和三层之间,就是数据链路层和网络层之间

它的作用就是将ip地址解析成为mac地址

1.6.3.11.2  ARP广播流程

只是知道对方的ip地址不知道对方的mac地址的时候,需要先发送一条arp广播,,这个广播的内容大概是"谁是192.168.3.6?把你的mac地址给192.168.3.5",这个广播会被交换机转发给局域网内所有的计算机.192.168.3.6的计算机收到这条广播之后,会给192.168.3.5回一条数据"我是192.168.3.6,我的mac地址是xxxxxx"这时候192.168.3.5拿到这个mac地址之后,会做一个表格记录.记录这个ip地址对应的mac地址,这个表格叫做arp缓存表,这个缓存不是永久的.不同系统的缓存时间不同.也被成为arp表的老化时间.到时间就会重新发送广播,多次广播没有回应的话,这一项就会从arp表中删除.

Python网络编程--学习记录_第12张图片

1.6.3.12 SNAT

Python网络编程--学习记录_第13张图片

Python网络编程--学习记录_第14张图片

两台计算机要想通信,就必须知道对方的ip地址.在发送数据之前,我们会通过ip地址和子网掩码进行计算,判断对方是否是和我在同一个子网里面.如果在一个局域网里面,那就通过arp协议拿到对方的mac地址,然后将数据通过交换机传给它.如果两者不再同一个子网内,我们发送数据还是通过arp协议,这次拿的就是网关的mac地址,网关的ip地址我们的计算机里面是有的.所以只要有网关的ip地址,通过arp协议,就一定能拿到网关的mac地址.

Python网络编程--学习记录_第15张图片

路由器上面的lan口和wan口接入的就是不同的子网,所以我们从局域网访问公网,就是跨子网通信.

SNAT(源地址转换)是NAT的一种,简单的说就是将我们源ip(192.168.3.88)转换为公网ip(182.151.22.141)作为源ip,路由器接收到数据之后.会执行反向SNAT,会将目标ip转换为192.168.3.88.

Python网络编程--学习记录_第16张图片

1.6.3.13 DNAT

Python网络编程--学习记录_第17张图片

目标地址转换.如果我们的计算机要对外提供服务,公网发过来的请求是不能直接到达内网计算机的

....

1.6.4 传输层

1.6.4.1 传输层引入

mac地址仅限于局域网通信,跨局域网通信只需要ip地址,是不需要mac地址的.跨局域网的时候,mac地址仅仅是让我们找到网关.或者是让网关找到局域网的主机.

局域网通信:  (源mac,目标mac) (源ip,目标ip)数据

跨局域网通信:  (源mac,网关mac) (源ip,目标ip)数据

学习网络编程的目的就是写一个客户端软件,再写一个服务端软件,让他们进行网络通信.通信的时候就要定位到它在全世界的哪一个位置

ip+mac地址就可以定位到全世界任意一台计算机了,arp协议的作用是将ip地址解析为mac地址,所以只需要ip地址就可以定位到世界任意一台计算机了.但是现在定位的是运行在计算机上的某一款软件.这时候就需要用到第四层,传输层里面的tcp/udp协议,它给我定义了一个端口的概念,端口不仅仅是nat转换的时候起到作用,还能够帮我们定位到计算机上的某一款应用程序.

ip+mac地址+端口 => 定位到全世界内运行的唯一一款基于网络通信的应用程序.

1.6.4.2 TCP标识

tcp/udp

tcp头部+数据/udp头部+数据.所以不管tcp头部还是udp头部里面都是包含源端口和目标端口.

在传输层一组数据称之为一个数据段,每一段数据都有一个编号->数据段序列号

tcp协议:

源端口,目标端口,数据段序列号

前面说过tcp协议是可靠的,因为每一个数据段都是需要对方确认的,如果数据段发送之后,没有回应.就会默认这个数据段丢失,就会重新发送.但是tcp不是一开始就发送数据端的.在发送数据之前会发送一个探测包,探测网络是否通畅,不通畅就不发数据了.

tcp的探测过程就叫做三次握手

这里的客户端和服务端,并不是指客户端软件和服务端软件,而是谁先发起请求,谁就是客户端.这里存在TCP6种数据包标识

握手包标识:SYN

挥手包标识:FIN,断开连接之前就是发送挥手包

数据包标识:PSH

握手弯完成之后就可以发送数据,就可以发送数据包了,对方每收到一个数据,都会回一个确认包.

确认包标识:ACK

重发包:RST

紧急包:URG

1.6.4.3 三次握手

Python网络编程--学习记录_第18张图片

1) 客户端->服务端, 连接的第一个包就是SYN,再加上序列号seq,这个序列号是随即生成的,值比较长(seq=x),还有一个序列号ack(确认上一次服务端给我们发的数据包的序列号,第一次建立连接的时候,这个值是没有的)

2) 服务端->客户端,回一个ACK确认包,加上序列号seq=y,确认序列号 ack=x+1,这个确认序列号就是之前的随机序列号+1(表示之前发送给我的序列号为x的数据包我已经收到了,同时告诉客户端下一次给我发的序列号就是x+1)

这样一来一回之后,客户端就知道了它跟服务端的线路是通畅的.但是服务端并不知道和客户端的线路是不是通畅的

3)服务端->客户端,发送SYN握手包,随机序列号还是seq=y,确认序列号 ack=x+1

4)客户端->服务端,发送ACK确认包,数据段序列号seq=x+1,再加上确认序列号y+1(告诉服务端有这个数据包已经收到了,下次发y+1就行了)当客户端发完这个确认包之后,它们就会进入连接状态.

发了四个包,却叫做三次握手的原因:

2)和 3)除了包的标识不一样,其他都是一样的所以可以合并为ACK,SYN,seq=y,ack=x+1;

Python网络编程--学习记录_第19张图片

1.6.4.4 四次挥手

Python网络编程--学习记录_第20张图片

四次挥手,就是数据传输完成之后,断开连接的过程

1) 客户端->服务端 先发起挥手包 FIN,ACK,seq=x,ack=y

2) 服务端->客户端 发起确认包 ACK,seq=y,ack=x+1

现在是客户端给服务端的数据已经发完,但是服务端给客户端的数据可能还没有传完.所以等服务端数据发完.等服务端数据发完之后

3) 服务端->客户端 发起挥手包 FIN,ACK seq=y,ack=x+1

4) 客户端-> 服务端 发送确认包 ACk,seq=x+1,ack=y+1

1.6.4.5 SYN洪水攻击

客户端在建立连接和断开连接的时候会有几种状态.

Python网络编程--学习记录_第21张图片

如果我们的服务端长时间处于SYN_RCVD这个状态,就很有可能遇到SYN洪水攻击(攻击者模拟大量的假客户端对我们的服务端发起请求,让服务端的资源被占满了,导致正常的客户端进不来),攻击者用大量的假ip发送请求,发完请求它就没有了,然后切换ip继续发,也有人将这种请求称之为半连接请求,因为请求发了一半就没有了.

服务端还以为是正常请求还在回SYN,ACK对方根本收不到,也不会收.所以服务端一致处于SYN_RCVD状态当服务端一直没有收到客户端确认包时,就会重新发一次SYN,ACK直到超时,才会删除连接

1.6.4.5 TCP连接状态

Python网络编程--学习记录_第22张图片

除了SYN洪水攻击,正常的连接请求也有可能是洪水效果.就是高并发的时候,就是并发量已经超过了服务端的承受极限了,大量的客户在发送SYN,服务端在回SYN ACK的时候资源被占用太多,这时候它发送SYN,ACK可能就比较慢.客户端的ACK可能回来的比较慢.服务端的状态也就可能一致处于SYN_RCVD状态

1.6.4.6 传输层头部

Python网络编程--学习记录_第23张图片

Python网络编程--学习记录_第24张图片

1.6.5 应用层

传输层以下的操作都很复杂且固定,有人已经帮组我们封装好了,封装成为一个个模块,应用程序要发数据的时候,就调用提供的接口即可.所以在传输层和应用层=之间还有一个socket抽象层,这一层不属于互联网通信协议,它只是对传输层及以下做了个封装.所以说我们以后写应用程序,只需要和套接字打交道就可以了.数据涉及到网络传输才会和套接字打交道.简单的说套接字就是我们用来通过网络收发数据的

1.7 dhcp

动态主机配置协议

当我们电脑插上网线,手机连上WiFi,并没有让我们配置ip地址,为什么也可以上网?因为dhcp协议的存在它会自动帮助我们配置ip地址,子网掩码,网关地址等.

1.8 DNS

1.8.1 IP通讯录

ip地址太多,我们也需要一个通讯录,只需要记住名字,不需要记住复杂的ip地址.有人写了一个程序,浏览器访问网站的时候,就会自动读取这个文件,就可以拿到对应的ip地址了.但是我们每次上网只需要一个网站的ip地址,不需要下载所有网站的通讯录,这样太过浪费资源.所以使用文件记录ip地址的方式,慢慢也被淘汰了.

Python网络编程--学习记录_第25张图片

1.8.2 DNS的由来

搭建了一个dns服务端,将原来通讯录里面的ip地址都放在了dns服务端里面.而每台电脑上面也安装了dns客户端,当上网的时候,dns客户端会直接向dns服务端发送一个请求.

dns服务端口 53

web服务端口 80/443

1.8.3 常用dns

阿里: 223.5.5.5 223.6.6.6

腾讯: 119.29.29.29

百度:180.76.76.76

谷歌: 8.8.8.8

1.8.4 dns解析流程

Python网络编程--学习记录_第26张图片

1.8.5 url

mac地址+ip+端口+url => 定位到唯一的一篇文章了

一个完整的url包含3部分:应用层协议+ip,端口,路径下面的文件

百度安全验证icon-default.png?t=N7T8https://www.baidu.com/s?ie=UTF-8&wd=csdn

2. 套接字

2.1 socket对象-服务端

基于网络通信: AF_INET,

所使用的协议:

socket.SOCK_STREAM 流式协议,tcp协议

socket.SOCK_DGRAM 数据报协议,udp协议

服务端:

1.创建socket对象
获得一个基于网络通信和基于tcp协议通信的套接字对象sk
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 流式协议(tcp协议)

2.绑定地址

sk.bind(("127.0.0.1", 5000))

服务端要绑定地址,并且这个地址是固定的.要定位到全世界唯一一个基于网络通信的应用程序,需要ip+mac+端口,mac地址可以省略,所以绑定地址的时候只需要绑定ip+端口即可.(ip,端口).
注意服务端绑定的ip是服务端所在计算机的ip地址

我们现在电脑的ip都是私网ip,也就是说绑定这个私网ip的话,只有我们这个局域网里面的主机客户端才可以访问.如果我们的服务端想要被跨局域网的客户端访问,就必须绑定一个公网ip.但是家庭宽带没有固定的公网ip,运营商也不支持端口外放,所以暂时只能在局域网里面玩.

端口除了自己的ip,还可以使用127.0.0.1,这是本地的一个回环地址,如果写了这个地址,就只能在本机测试,局域网内的其他主机也是访问不了的

也可以写一个全0地址,全0地址是一个特殊的ip地址,代表本机的所有ip地址.

端口的范围是0~65535,1024之前的端口都是系统保留的.所以最好不要使用1024之前的端口.

3.监听连接请求 (开始营业)

服务端跑起来之后,就会进入一个listen状态,等待客户端向我们发起连接,这个5就是半连接池的大小.半连接就是客户端想服务端发起了连接请求,服务端还没有向客户端发起连接请求.

sk.listen(5)
print("服务端启动成功,在5000端口等待连接")

4.取出连接请求,开始服务

如果半连接池里面没有请求进来,代码会阻塞在这里,不会执行此行代码..只有半连接池里面有请求了,才会从半连接池里面取出一个连接,然后回复SYN进行第二次握手,客户端进来是第一次握手.

我在这里调用accept()取连接的时候,它的底层就会自动回复syn,进行三次握手,并把连接建立好.

conn是连接对象,addr是客户端的ip和端口

当然我们只能一个个服务,如果同时来两个请求,只能一个先开始服务,另外一个呆在半连接池里面..前面的服务完成之后,才从连接池里面取出来进行服务.如果同时来的请求超过5个,第6个请求就会被拒绝.

conn,addr = sk.accept()

print("连接对象",conn)

print("客户端ip+端口",addr)

5.数据传输

接收客户端发过来的数据,1024就是一次最大接收的数据量单位是字节Bytes.不过收到的数据是Bytes类型,在python里面二进制数据就是bytes类型.不管是文本文件还是图片文件通过b模式读取之后,都是bytes类型.所以要进行解码.

conn.recv(1024) 

给客户端发送数据

conn.send()

data = conn.recv()

data = data.decode("utf-8")

print(f"客户端发送过来的数据{data}")

data.send(data.upper())

6.结束服务

调用conn.close()就可以将连接关掉,连接关掉之后,就可以继续待用accept服务下一个用户.当然不关闭也可以继续从accept服务下一个用户.accept一次就可以从半连接池里面取出一个连接对象.套接字以下的都是操作系统的资源,所以我们应该程序结束之前,将操作系统的资源全部回收掉.调用这个close()就类似与在做4次挥手,结束此次连接.

conn.close()

7.关闭服务端

这一步可做可不做,但是一般都不做,理想的状态就是在服务端运行开始的那一刻,运行到天荒地老.

sk.close()

# _*_ coding utf-8 _*_
# george
# time: 2024/1/26上午10:14
# name: server.py
# comment:
import socket

# 1.创建socket对象
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 流式协议(tcp协议)

# 2.绑定地址
sk.bind(("127.0.0.1", 5001))

# 3.监听连接请求
sk.listen(5)
print("服务端启动成功,在5000端口等待连接")

# 4.取出连接,进行服务
conn, addr = sk.accept()
print("连接对象", conn)
print("客户端ip+端口", addr)

# 5.数据传输
data = conn.recv(1024)
data = data.decode("utf-8")
print(f"客户端发送过来的数据:{data}")
conn.send(data.upper().encode("utf-8"))

# 6.结束服务
conn.close()

# 7. 关闭server(可选)
# sk.close()

2.2 socket对象-客户端

 1.创建socket对象
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

2.建连接

客户端也是需要ip和端口,但是我们不会绑定ip,因为自己所在的位置不一样,ip可能就会发生改变.端口也不能固定,避免自己电脑上面的端口冲突.客户端要给服务端发送数据第一件事,就是发三次握手建连接.当然这个连接不需要我们自己去建,调用sk.connect()之后,它首先会发送一个syn握手包,服务端的底层会自动回应三次握手并建立连接,同时将连接对象放入帮半连接池.这个连接请求就会去到服务端的半连接池..

sk.connect(("127.0.0.1",5000))

3.传输数据
sk.send('hello'.encode("utf-8"))

data = sk.recv(1024)
print("接收服务端发送过来的数据",data.decode('utf-8'))

 4.关闭连接

客户端的close()必须写,它对应的是服务端的conn
sk.close()

# _*_ coding utf-8 _*_
# george
# time: 2024/1/27下午6:41
# name: client.py
# comment:
import socket

# 1.创建socket对象
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2.建连接
sk.connect(("127.0.0.1", 5000))

# 3.传输数据
sk.send('hello'.encode("utf-8"))

data = sk.recv(1024)
print("接收服务端发送过来的数据", data.decode('utf-8'))

# 4.关闭连接
sk.close()

Python网络编程--学习记录_第27张图片Python网络编程--学习记录_第28张图片

2.3 解决程序中存在的问题

2.3.1 通信循环

将发送数据的两部分放入while循环里面,即可实现通信循环. 

# 5.数据传输 --> 服务端
while True:
    try:  # 解决windows电脑客户端异常断开
        data = conn.recv(1024)
    except:
        break
    if not data:  # 程序收到数据为空,表示客户端异常断开了
        break
    data = data.decode("utf-8")
    print(f"客户端发送过来的数据:{data}")
    conn.send(data.upper().encode("utf-8"))
while True: #数据传输 --> 客户端
    img = input(">>>")
    sk.send(img.encode("utf-8"))
    sk.send("hello".encode("utf-8"))

    data = sk.recv(1024)
    print("接收服务端发送过来的数据:", data.decode('utf-8'))

 2.3.2 针对客户端异常断开的情况

对于mac和linux系统而言数据会不断的收空数据,对于windows系统而言,服务端会直接崩溃,报错.对于windows系统的报错直接使用try,except进行处理

while True:
    try:  # 解决windows电脑客户端异常断开
        data = conn.recv(1024)
    except:
        break
    if not data:  # 程序收到数据为空,表示客户端异常断开了
        break
    data = data.decode("utf-8")
    print(f"客户端发送过来的数据:{data}")
    conn.send(data.upper().encode("utf-8"))

2.3.3 发空问题

发送是可以发送为空的,但是接收数据是收不到空的,如果接收为空就表示客户端异常断开了.收不到就要在原地等着收

实际上客户端的send和服务端的recv不是一一对应的,客户端可以send 100次,但是服务端只是recv一次.应用程序是运行在操作系统之上,我们应用程序想要发数据出去,就是要通过网卡.但是应用程序不能直接操作硬件,必须向操作系统发起请求,让操作系统调用网卡帮我们把数据发出去.

我们在使用send发送数据的时候,不是send给了服务端,而是在向操作系统发请求,让操作系统帮我们做一系列的封包操作,最后调用网卡帮我们把数据发送出去.

我们使用send发送的数据,其实是把数据放到了缓存里面,操作系统再从缓存里面一点点把数据帮我们发送出去.这个数据到对方网卡的时候,也会被操作系统放到缓存里面.recv收数据的时候其实是从缓存里面将数据取出来.

简单的说send就是将数据放入了自己的缓存里面,recv就是从自己的缓存里面取数据.send和recv多少次都是和对方没有关系的

send空的时候,确实发起了一次系统调用,但是操作系统一看缓存是空的,操作系统就不会帮我们发送数据出去,服务端也就收不到数据

解决此问题,就是发数据之前先判断一下,为空就直接跳过不发.

# 3.传输数据 -- >客户端
while True:
    img = input(">>>")
    if not img: #解决发空问题
        continue
    sk.send(img.encode("utf-8"))
    sk.send("hello".encode("utf-8"))

2.3.4 连接循环

现在的服务端一次只能服务一个人,服务完之后就会挂掉.但是需要的是服务完成一个人之后,继续服务另外一个人.理想状况是持续的提供服务的同时,并发的提供服务.但是现在不考虑并发,只是让服务端持续的提供服务.

将获取连接的代码放入while循环,conn.close()关闭一个连接之后,再调用accept()取出一个连接.

while True: # 服务端 --> 连接循环
    # 4.取出连接,进行服务
    conn, addr = sk.accept()
    print("连接对象", conn)
    print("客户端ip+端口", addr)

    # 5.数据传输
    while True:
        try:  # 解决windows电脑客户端异常断开
            data = conn.recv(1024)
        except:
            break
        if not data:  # 程序收到数据为空,表示客户端异常断开了
            break
        data = data.decode("utf-8")
        if data == "q":
            break
        print(f"客户端发送过来的数据:{data}")
        conn.send(data.upper().encode("utf-8"))

    # 6.结束服务
    conn.close()

2.3.5 半连接池

前面说过半连接池的大小不用设置的很大,设置的太大没有意义.学完并发之后,可以做到服务端同时服务很多客户端.也就是说accept可以一直从半连接池里面拿连接对象.给用户的感觉就是延迟很低,基本上只要你访问它,就可以马上获得结果.所以将半连接池设置的很大也没有意义.

如果客户的访问量很大,大的已经超出了服务端的并发能力.半连接池设置的很大的话,客户就会呆在半连接池里面,没法享受到即时服务.半连接池的大小改变不了用户体验,访问不了还是访问不了.真正要提高的是服务端的并发能力.

一个python文件运行多次的方法:

Python网络编程--学习记录_第29张图片

运行两个客户端:

前面一个客户端还在服务中,后面的一个客户端只能在半连接池里面等着.这个时候,客户端和服务端的三次握手已经完成,就等着accept把连接对象从半连接池里面拿出去.然后服务端才能够回复客户端.

所有的半连接池的请求都会占用服务端的计算机资源,而服务端又不能及时处理这些请求,改大半连接池既不能改变客户端的体验,又会过多占用服务端的计算机资源

当请求占满半连接池,再次发送请求时,会在 sk.connect(("127.0.0.1", 5001))这一步直接阻塞,出现TimeoutError.因为半连接池已经满了,客户端在发握手包的时候,服务端不会发送握手包.客户端一直重发握手包直到超时

# _*_ coding utf-8 _*_
# george
# time: 2024/1/29下午2:09
# name: cmd_server.py
# comment: 远程执行终端命令
import socket

# 1. 创建对象
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2.绑定地址
sk.bind(("0.0.0.0", 5005))

# 3.监听连接请求
sk.listen(5)
print("服务端启动成功,在5000端口等待连接")

while True:  # 服务端 --> 连接循环
    # 4. 取出连接,进行服务
    conn, addr = sk.accept()  # windows客户端异常断开连接
    print(f"连接对象是:{conn}")
    print(f"ip和端口为:{addr}")
    # 5. 数据传输
    while True:
        try:
            data = conn.recv(1200)
        except:
            break
        if not data:  # mac客户端异常断开连接
            break
        data = data.decode("utf-8")
        print(f"客户端发送过来的数据为{data}")
        if data == "q":
            break
        conn.send(data.upper().encode("utf-8"))
    # 6. 结束服务
    conn.close()

# 7. 关闭server(可选)
# sk.close()
# _*_ coding utf-8 _*_
# george
# time: 2024/1/29下午2:17
# name: cmd_client.py
# comment: 远程执行终端指令
import socket

# 1. 创建对象
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2.建连接
sk.connect(("0.0.0.0", 5005))

# 3.传输数据
while True:
    meg = input(">>>").strip()
    if not meg:  # 避免发空问题
        continue
    if meg == "q":
        break
    sk.send(meg.encode("utf-8"))
    data = sk.recv(1024)
    print(f"接收服务端发送过来的数据:{data.decode('utf-8')}")

# 4.关闭连接
sk.close()

 2.4 UDP套接字

UDP是可以发空的,因为tcp是流式协议,是要有水流进入缓存才会被发送出去.而UDP是数据报协议,我们只要send一次,他就会组织一个数据报发送出去.

udp不需要连接,不需要指定半连接池的大小,accept等都是不需要的

# _*_ coding utf-8 _*_
# george
# time: 2024/1/29上午11:27
# name: udp-server.py
# comment:
import socket

# 1.创建socket对象
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 流式协议(tcp协议)

# 2.绑定地址
sk.bind(("127.0.0.1", 5003))

# 5.数据传输
while True:
    data, addr = sk.recvfrom(1024)
    print(f"客户端发送过来的数据:{data}")
    if data.decode("utf-8") == "q":
        break
    sk.sendto(data.upper(), addr)

# 7. 关闭server(可选)
# sk.close()
# _*_ coding utf-8 _*_
# george
# time: 2024/1/29上午11:34
# name: udp-client.py
# comment:
import socket
import os

# 1.创建socket对象
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 3.传输数据
while True:
    img = input(">>>")
    sk.sendto(img.encode("utf-8"),("127.0.0.1",5003))
    if img == "q":
        break
    data,addr = sk.recvfrom(1024)
    print("接收服务端发送过来的数据:", data.decode('utf-8'))

# 4.关闭连接
sk.close()

2.5 远程执行终端命令

不可以使用udp来写这个远程执行终端命令的程序,udp传输数据是不可靠的.我们可能需要通过一条命令的结果,判断下一步执行什么操作.使用udp,可能命令执行成功了,但是我们没有收到结果.

将img接收的内容作为终端命令,服务端拿到data之后,将data作为终端命令执行,再将执行之后的结果发给客户端.

Python网络编程--学习记录_第30张图片

# _*_ coding utf-8 _*_
# george
# time: 2024/1/29下午2:09
# name: cmd_server.py
# comment: 远程执行终端命令
import socket
import subprocess

# 1. 创建对象
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2.绑定地址
sk.bind(("0.0.0.0", 5001))

# 3.监听连接请求
sk.listen(5)
print("服务端启动成功,在5000端口等待连接")

while True:  # 服务端 --> 连接循环
    # 4. 取出连接,进行服务
    conn, addr = sk.accept()  # windows客户端异常断开连接
    print(f"连接对象是:{conn}")
    print(f"ip和端口为:{addr}")
    # 5. 数据传输
    while True:
        try:
            cmd = conn.recv(1200)
        except:
            break
        if not cmd:  # mac客户端异常断开连接
            break
        data = cmd.decode("utf-8")
        print(f"客户端发送过来的数据为:{data}")
        if data == "q":
            break

        # 执行终端命令
        obj = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)

        # out_res和out_err都是Bytes类型
        out_res = obj.stdout.read()
        out_err = obj.stderr.read()
        print("管道输出:",out_res)
        print("管道输出ERROR:",out_err)
        conn.send(out_res)
        conn.send(out_err)
    # 6. 结束服务
    conn.close()

# 7. 关闭server(可选)
# sk.close()
# _*_ coding utf-8 _*_
# george
# time: 2024/1/29下午2:17
# name: cmd_client.py
# comment: 远程执行终端指令
import socket

# 1. 创建对象
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2.建连接
sk.connect(("0.0.0.0", 5001))

# 3.传输数据
while True:
    cmd = input("请输入终端命令>>>").strip()
    if not cmd:  # 避免发空问题
        continue
    if cmd == "q":
        break
    sk.send(cmd.encode("utf-8"))
    data = sk.recv(1024)
    print(f"接收服务端发送过来的数据:{data.decode('utf-8')}")

# 4.关闭连接
sk.close()

Python网络编程--学习记录_第31张图片

Python网络编程--学习记录_第32张图片

2.5.1 粘包问题引入 

指令
mac windows 备注
ls dir
pwd chdir
ps aux tasklist 查看计算机上的所有进程

看起来没有问题但是还是隐藏巨大的坑使用ps aux指令时发现,服务端给我们传输了 68554 bytes数据,但是客户端只是收到了1024 bytes数据.

之前说过服务端的数据是直接send到了客户端的缓存,而客户端使用recv取数据时,也是从缓存里面取数据.也就是说现在客户端只是从缓存里面取了1024 bytes数据,剩余的数据还在缓存里面.如果再次输入新的指令ls /users,新的指令的结果就会流入缓存,导致两次命令的结果粘在一起

我们调用send不是直接发送数据的,只是将数据放到了缓存,还没有开始封包.缓存里面的数据都是粘在一起的,全都是应用层的数据.

数据到了客户端的计算机也会被层层解包,最后只是剩下应用层面的数据,然后放到客户端的缓存,客户端的缓存可能还有上次的数据没有收完.这样还是会造成两次的命令粘在一起.这就是TCP粘包问题.

虽然数据是粘在一起的,但是数据的顺序不会变,先来的数据在前面,后来的数据在后面.

Python网络编程--学习记录_第33张图片

Python网络编程--学习记录_第34张图片Python网络编程--学习记录_第35张图片

2.5.2 分析粘包产生的原因

 有粘包产生的原因来解决问题:我们每敲一次命令,先将缓存里面的数据先收干净.然后再执行下一条命令.因为ps指令的结果只有6万多个字节,所以将客户端的recv改为10万.但是其实即使将缓存里面的数据收干净还是会存在问题,服务端->6万多,客户端->3万多

Python网络编程--学习记录_第36张图片

Python网络编程--学习记录_第37张图片

可能存在两个原因:

1)收的太快,有可能这6万多个字节流过来的速度比较慢,服务端的数据还没有完全流入客户端的缓存,所以造成我们只是收了一部分.

2) 缓存(内存空间)大小有限,缓存只是程序运行内存的一部分.ps命令的结果已经超过了缓存的大小,我们收数据的时候缓存已经占满了.我们将缓存里面的数据收完.服务端的数据才能流进来.

你可能感兴趣的:(网络,学习,服务器)