网络编程
一 C\S架构,客户端服务端架构⑩
客户端(client) : 享受服务端提供的服务
服务端(server) : 给客户端提供服务
服务端:提供服务的
客户端:享受服务的
软件CS架构:京东,淘宝,qq,微信,暴风影音,快播
硬件CS架构:打印机
B\S 浏览器和服务端 B(browser) 谷歌,360,IE
二 网络通信的整个流程(硬件\名词)
网卡: 接收电信号,网络插口:插网线
mac地址(物理地址): 8C-EC-4B-87-99-D7 ①
16进制的6个数表示,前三位厂商编号,后三位生产流水号
网络设备的唯一标识,全球唯一的,相当于身份证
广播\单播
广播: 信息发给所有人⑨
单播: 单独发给某个人或者说某个设备
广播风暴: 不安全, 容易拥堵网络
IP地址: 划分广播域,192.168.15.113 四个点分十进制组成②
IPv4 : 4个点分十进制
IPv4 地址:192.168.15.192
IPv6 : 6个冒号分十六进制
IPv6 地址: fe80::48c3:8f81:8474:c7d0%2
集线器 : 将所有连电脑设备连通起来
交换机: 升级版集线器⑤
DHCP协议 : 自动分配IP地址③
划分广播域--> IP网段 : 192.168.15.0 - 192.168.15.255 属于同一子网
192.168.16.0 - 192.168.16.255
子网掩码 : 计算目标IP地址是否和咱们是同一网段(计算是否属于同一网段)⑥
同一网段的:广播发送
不同网段的:发送给路由器
子网掩码: 255.255.255.0,
例外一个班的同学的IP地址为:192.168.14.12
192.168.14.12
255.255.255.0
11000000.10101000.00001110.00001100
11111111.11111111.11111111.00000000
11000000.10101000.00001110.00000000
192.168.14.0
属于同一个网段的,我们成为属于同一子网
路由器(内网的作用) : 管理局域网④
找外部网路的设备:
DNS服务器:域名:www.jd.com -->ip地址⑧
域名:www.jd.com --> IP地址
DNS服务器:记录着所有的域名和他网站对应的那台服务器的IP地址的对应关系,理解为一个字典
{'www.jd.com':192.168.15.12}
网关:在路由器这儿,把关你对外网的请求⑦
NAT技术: 网络地址转换,将你的IP地址,转换为网关的IP地址
外网又称为公网 ,网关的IP地址又称为外网IP地址或者公网IP地址
路由器(外网的作用) : 转发消息
路由器:管理网络,连通外网,并且路由转发,就是转发消息
路由协议 : 计算一个最优路径,然后进行路由转发
发到京东的路由器上,京东的路由器做了端口映射(看图)
端口:标识电脑上某个应用程序,范围0-65535 0-1024 属于电脑内部程序用的,我们一般使用的都是8000以后的
端口:电脑给你创建的标识电脑上的程序用的.
ip加端口(“192.168.15.192”,1-65535)1024 之前都是电脑自己的不能用,用后面的。
通过IP地址+端口:我就能唯一确定一台电脑上的某个应用程序
三初始socket(重点)
tcp与udp下socket差异图
1 网络通信协议
osi七层 应示会偷网链物
osi七层
各层的功能简述:
【1】物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换),这一层的数据叫做比特。
【2】数据链路层:定义了如何让格式化数据以进行传输,以及如何让控制对物理介质的访问,这一层通常还提供错误检测和纠正,以确保数据的可靠传输。
【3】网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择,Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
【4】传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的), 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组,常常把这一层数据叫做段。
【5】会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路,主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
【6】表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
【7】应用层: 是最靠近用户的OSI层,这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
各层中涉及的协议的简单解释:
应用层
·DHCP(动态主机分配协议)
· DNS (域名解析)
· FTP(File Transfer Protocol)文件传输协议
· Gopher (英文原义:The Internet Gopher Protocol 中文释义:(RFC-1436)网际Gopher协议)
· HTTP (Hypertext Transfer Protocol)超文本传输协议
· IMAP4 (Internet Message Access Protocol 4) 即 Internet信息访问协议的第4版本
· IRC (Internet Relay Chat )网络聊天协议
· NNTP (Network News Transport Protocol)RFC-977)网络新闻传输协议
· XMPP 可扩展消息处理现场协议
· POP3 (Post Office Protocol 3)即邮局协议的第3个版本
· SIP 信令控制协议
· SMTP (Simple Mail Transfer Protocol)即简单邮件传输协议
· SNMP (Simple Network Management Protocol,简单网络管理协议)
· SSH (Secure Shell)安全外壳协议
· TELNET 远程登录协议
· RPC (Remote Procedure Call Protocol)(RFC-1831)远程过程调用协议
· RTCP (RTP Control Protocol)RTP 控制协议
· RTSP (Real Time Streaming Protocol)实时流传输协议
· TLS (Transport Layer Security Protocol)安全传输层协议
· SDP( Session Description Protocol)会话描述协议
· SOAP (Simple Object Access Protocol)简单对象访问协议
· GTP 通用数据传输平台
· STUN (Simple Traversal of UDP over NATs,NAT 的UDP简单穿越)是一种网络协议
· NTP (Network Time Protocol)网络校时协议
传输层
·TCP(Transmission Control Protocol)传输控制协议
· UDP (User Datagram Protocol)用户数据报协议
· DCCP (Datagram Congestion Control Protocol)数据报拥塞控制协议
· SCTP(STREAM CONTROL TRANSMISSION PROTOCOL)流控制传输协议
· RTP(Real-time Transport Protocol或简写RTP)实时传送协议
· RSVP (Resource ReSer Vation Protocol)资源预留协议
· PPTP ( Point to Point Tunneling Protocol)点对点隧道协议
网络层
IP(IPv4 · IPv6) Internet Protocol(网络之间互连的协议)
ARP : Address Resolution Protocol即地址解析协议,实现通过IP地址得知其物理地址。
RARP :Reverse Address Resolution Protocol 反向地址转换协议允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。
ICMP :(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
ICMPv6:
IGMP :Internet 组管理协议(IGMP)是因特网协议家族中的一个组播协议,用于IP 主机向任一个直接相邻的路由器报告他们的组成员情况。
RIP : 路由信息协议(RIP)是一种在网关与主机之间交换路由选择信息的标准。
OSPF : (Open Shortest Path First开放式最短路径优先).
BGP :(Border Gateway Protocol )边界网关协议,用来连接Internet上独立系统的路由选择协议
IS-IS:(Intermediate System to Intermediate System Routing Protocol)中间系统到中间系统的路由选择协议.
IPsec:“Internet 协议安全性”是一种开放标准的框架结构,通过使用加密的安全服务以确保在 Internet 协议 (IP) 网络上进行保密而安全的通讯。
数据链路层
802.11 · 802.16 · Wi-Fi · WiMAX · ATM · DTM · 令牌环 · 以太网 · FDDI · 帧中继 · GPRS · EVDO · HSPA · HDLC · PPP · L2TP · ISDN
物理层
以太网物理层 · 调制解调器 · PLC · SONET/SDH · G.709 · 光导纤维 · 同轴电缆 · 双绞线
各层功能及协议的简单解释
tcp\ip五层
arp协议:通过IP地址找到mac地址
传输层:TCP\UDP协议
tcp
三次握手
四次挥手
2.tcp和udp的区别
tcp协议:面向连接,消息可靠,相对udp来讲,传输速度慢,消息是面向流的,无消息保护边界
udp协议:面向无连接,消息不可靠,传输速度快,消息是面向包的,有消息保护边界.
tcp代码和udp代码的区别看代码
tcp协议下的socket
udp协议下的socket
tcp:属于长连接,与一个客户端进行连接了以后,其他的客户端要等待,要连接另外一个,必须优雅的断开前面这个客户端的连接.
允许地址重用:在bind IP地址和端口之前加上,# server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 允许(IP地址和端口)地址重用
缓冲区
输入缓冲区 #recv
输出缓冲区 #send
粘包(tcp的两种粘包现象)
1 连续发送小的数据,并且每次发送之间的时间间隔很短(输出缓冲区:两个消息在缓冲区黏在一起了)
原因是TCP为了传输效率,做了一个优化算法(Nagle),减少连续的小包发送(因为每个消息被包裹以后,都会有两个过程:1 组包 2拆包)
2 第一次服务端发送的数据比我客户端设置的一次接收消息的大小要大,那么接收不完,第二次再接收的时候,就会将第一次剩余的消息接收到
粘包的根本原因是因为:双方不知道对方发送消息的大小
解决方案一:
发送消息之前,先计算要发送消息的长度,然后先将消息长度发送过去,对方给你回一个确认收到长度的信息,然后根据接收到的消息长度来修改自己一次接收消息的大小
这个过程多了一次交互
1.TCP模式下地址及端口如何重用
server:
import socket sk = socket.socket() 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() #关闭服务器套接字(可选)
client:
import socket sk = socket.socket() # 创建客户套接字 sk.connect(('127.0.0.1',8898)) # 尝试连接服务器 sk.send(b'hello!') ret = sk.recv(1024) # 对话(发送/接收)print(ret) sk.close() # 关闭客户套接字
socket绑定IP和端口时可能出现下面的问题:
解决办法:
加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #在bind前加,允许地址重用
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() #关闭服务器套接字(可选)
但是如果你加上了上面的代码之后还是出现这个问题:OSError: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。那么只能换端口了,因为你的电脑不支持端口重用。
记住一点,用socket进行通信,必须是一收一发对应好。
2.TCP长连接
原因解释:
tcp属于长连接,长连接就是一直占用着这个链接,这个连接的端口被占用了,第二个客户端过来连接的时候,他是可以连接的,但是处于一个占线的状态,就只能等着去跟服务端建立连接,除非一个客户端断开了(优雅的断开可以,如果是强制断开就会报错,因为服务端的程序还在第一个循环里面),然后就可以进行和服务端的通信了。什么是优雅的断开呢?看代码。
server端代码:
1 import socket 2 from socket import SOL_SOCKET,SO_REUSEADDR 3 sk = socket.socket() 4 sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #允许地址重用,这个东西都说能解决问题,我非常不建议大家这么做,容易出问题 5 sk.bind(('127.0.0.1',8090)) 6 sk.listen() 7 # 第二步演示,再加一层while循环while True: #下面的代码全部缩进进去,也就是循环建立连接,但是不管怎么聊,只能和一个聊,也就是另外一个优雅的断了之后才能和另外一个聊 8 #它不能同时和好多人聊,还是长连接的原因,一直占用着这个端口的连接,udp是可以的,然后我们学习udp 9 conn,addr = sk.accept() #在这阻塞,等待客户端过来连接 10 while True: 11 ret = conn.recv(1024) #接收消息 在这还是要阻塞,等待收消息 12 ret = ret.decode('utf-8') #字节类型转换为字符串中文 13 print(ret) 14 if ret == 'bye': #如果接到的消息为bye,退出 15 break 16 msg = input('服务端>>') #服务端发消息 17 conn.send(msg.encode('utf-8')) 18 if msg == 'bye': 19 break 20 conn.close()
client端代码:
import socket sk = socket.socket() sk.connect(('127.0.0.1',8090)) #连接服务端 while True: msg = input('客户端>>>') #input阻塞,等待输入内容 sk.send(msg.encode('utf-8')) if msg == 'bye': break ret = sk.recv(1024) ret = ret.decode('utf-8') print(ret) if ret == 'bye': break # sk.close()
强制断开连接之后的报错信息:
3.什么是缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
这些I/O缓冲区特性可整理如下:
1.I/O缓冲区在每个TCP套接字中单独存在;
2.I/O缓冲区在创建套接字时自动生成;
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4.关闭套接字将丢失输入缓冲区中的数据。
输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
1.unsigned optVal;
2.int optLen = sizeof(int);
3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal,&optLen);
4.printf("Buffer length: %d\n", optVal);
4.引用subprocess
import subprocess
cmd = input('请输入指令>>>')
res = subprocess.Popen(
cmd, #字符串指令:'dir','ipconfig',等等
shell=True, #使用shell,就相当于使用cmd窗口
stderr=subprocess.PIPE, #标准错误输出,凡是输入错误指令,错误指令输出的报错信息就会被它拿到
stdout=subprocess.PIPE, #标准输出,正确指令的输出结果被它拿到)
print(res.stdout.read().decode('gbk'))
print(res.stderr.read().decode('gbk'))
注意:
如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码
且只能从管道里读一次结果,PIPE称为管道。
下面是subprocess和windows上cmd下的指令的对应示意图:subprocess的stdout.read()和stderr.read(),拿到的结果是bytes类型,所以需要转换为字符串打印出来看。
5.TCP下的两种粘包现象,以及两种解决方式
tcp粘包演示(一):
先从上面粘包现象中的第一种开始:接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
server:
1 from socket import * 2 3 import subprocess 4 5 ip_port = ('127.0.0.1', 8080) 6 BUFSIZE = 1024 7 8 tcp_socket_server = socket(AF_INET, SOCK_STREAM) 9 tcp_socket_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 10 tcp_socket_server.bind(ip_port) 11 tcp_socket_server.listen(5) 12 13 while True: 14 conn, addr = tcp_socket_server.accept() 15 print('客户端>>>', addr) 16 17 while True: 18 cmd = conn.recv(BUFSIZE) 19 if len(cmd) == 0: break 20 21 res = subprocess.Popen(cmd.decode('gbk'), shell=True, 22 stdout=subprocess.PIPE, 23 stdin=subprocess.PIPE, 24 stderr=subprocess.PIPE) 25 26 stderr = res.stderr.read() 27 stdout = res.stdout.read() 28 conn.send(stderr) 29 conn.send(stdout)
client:
import socket ip_port = ('127.0.0.1', 8080) size = 1024 tcp_sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = tcp_sk.connect(ip_port) while True: msg = input('>>: ').strip() if len(msg) == 0: continue if msg == 'quit': break tcp_sk.send(msg.encode('utf-8')) act_res = tcp_sk.recv(size) print('接收的返回结果长度为>', len(act_res)) print('std>>>', act_res.decode('gbk')) # windows返回的内容需要用gbk来解码,因为windows系统的默认编码为gbk
tcp粘包演示(二):
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
server端代码示例:(如果两次发送有一定的时间间隔,那么就不会出现这种粘包情况,试着在两次发送的中间加一个time.sleep(1))
1 import socket 2 3 server = socket.socket() 4 server.bind(('127.0.0.1', 8820)) 5 server.listen() 6 conn, addr = server.accept() 7 from_client_msg1 = conn.recv(1024).decode("utf-8") 8 from_client_msg2 = conn.recv(1024).decode('utf-8') 9 10 print('msg1:', from_client_msg1) 11 print('msg2:', from_client_msg2) 12 client端: 13 import socket 14 15 client = socket.socket() 16 client.connect(('127.0.0.1', 8820)) 17 client.send('hello'.encode('utf-8')) 18 client.send('sigui'.encode('utf-8')) 19 20 client.close()
示例二的结果:全部被第一个recv接收了
TCP会粘包、UDP永远不会粘包
看下面的解释原因:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
1.TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
补充两个问题:
补充问题一:为何tcp是可靠传输,udp是不可靠传输
tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。
而udp发送数据,对端是不会返回确认信息的,因此不可靠
补充问题二:send(字节流)和sendall
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失,一般的小数据就用send,因为小数据也用sendall的话有些影响代码性能,简单来讲就是还多while循环这个代码呢。
用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)
用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
再补充一个问题:
粘包的原因:主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
学到这里,我们留一个小作业(做不做是你的事情,我的事情是真心的教会你,希望你尊重自己的努力):实现一个简单的网盘功能。
粘包的解决方案:
解决方案(一):
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端发一个确认消息给发送端,然后发送端再发送过来后面的真实内容,接收端再来一个死循环接收完所有数据。
看代码示例:
1 import socket 2 import subprocess 3 4 server = socket.socket() 5 server.bind(('127.0.0.1', 8820)) 6 server.listen() 7 conn, addr = server.accept() 8 while 1: 9 # 来自客户端的指令 10 print("等待接受信息.......") 11 from_client_cmd = conn.recv(1024).decode('utf-8') 12 print(from_client_cmd) 13 sub_obj = subprocess.Popen( 14 from_client_cmd, # 客户端的指令 15 shell=True, 16 stdout=subprocess.PIPE, 17 stderr=subprocess.PIPE 18 ) 19 # 接收到的返回值信息是bytes类型的,并且windows系统的默认编码为gbk 20 server_cmd_msg = sub_obj.stdout.read() 21 cmd_msg_len = str(len(server_cmd_msg)) 22 print('指令返回的正确信息的长度>>>>', cmd_msg_len) 23 conn.send(cmd_msg_len.encode('gbk')) 24 25 from_client_ack = conn.recv(1024).decode('utf-8') 26 print('from_client_ack', from_client_ack) 27 if from_client_ack == 'ok': 28 conn.send(server_cmd_msg) 29 else: 30 continue
import socket client = socket.socket() client.connect(('127.0.0.1', 8820)) while 1: msg = input("请输入要执行的指令>>>") client.send(msg.encode('utf-8')) # 先接收服务端要发送给我发的信息的长度 from_server_msglen = int(client.recv(1024).decode('gbk')) print('.......', from_server_msglen) # 给服务端回应一个收到你的信息长度的确认信息 client.send('ok'.encode('utf-8')) # 拿到信息长度后,将信息长度作为参数给了recv,recv就按照这个长度大小来接受服务端后面要给我发送的数据 from_server_stdout = client.recv(from_server_msglen).decode('gbk') print('收到的正确信息:', from_server_stdout)
解决方案(二):
通过struck模块将需要发送的内容的长度进行打包,打包成一个4字节长度的数据发送到对端,对端只要取出前4个字节,然后对这四个字节的数据进行解包,拿到你要发送的内容的长度,然后通过这个长度来继续接收我们实际要发送的内容。不是很好理解是吧?哈哈,没关系,看下面的解释~~
为什么要说一下这个模块呢,因为解决方案(一)里面你发现,我每次要先发送一个我的内容的长度,需要接收端接收,并切需要接收端返回一个确认消息,我发送端才能发后面真实的内容,这样是为了保证数据可靠性,也就是接收双方能顺利沟通,但是多了一次发送接收的过程,为了减少这个过程,我们就要使struck来发送你需要发送的数据的长度,来解决上面我们所说的通过发送内容长度来解决粘包的问题。
关于struck的介绍:
了解c语言的人,一定会知道struct结构体在c语言中的作用,不了解C语言的同学也没关系,不影响,其实它就是定义了一种结构,里面包含不同类型的数据(int,char,bool等等),方便对某一结构对象进行处理。而在网络通信当中,大多传递的数据是以二进制流(binary data)存在的。当传递字符串时,不必担心太多的问题,而当传递诸如int、char之类的基本数据的时候,就需要有一种机制将某些特定的结构体类型打包成二进制流的字符串然后再网络传输,而接收端也应该可以通过某种机制进行解包还原出原始的结构体数据。python中的struct模块就提供了这样的机制,该模块的主要作用就是对python基本类型值与用python字符串格式表示的C struct类型间的转化(This module performs conversions between Python values and C structs represented as Python strings.)。
struck模块的使用:struct模块中最重要的两个函数是pack()打包, unpack()解包。
pack():#我在这里只介绍一下'i'这个int类型,上面的图中列举除了可以打包的所有的数据类型,并且struck除了pack和uppack两个方法之外还有好多别的方法和用法,大家以后找时间可以去研究一下,这里我就不做介绍啦,网上的教程很多~~
import struct
a=12
# 将a变为二进制
bytes=struct.pack('i',a)
-------------------------------------------------------------------------------
struct.pack('i',1111111111111) 如果int类型数据太大会报错struck.error
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
unpack():
# 注意,unpack返回的是tuple !!
a,=struct.unpack('i',bytes) #将bytes类型的数据解包后,拿到int类型数据
先发送数据的长度 + 发送数据 #自定制消息头
Pack打包的数据长度 = 4个字节
Struct
打包:pack(‘i’,int类型数据) #21亿多,4个字节
解包:unpack(‘i’,pack的数据) 返回数据是个元组 拿数据需要加上[0]
一个神奇的打包工具
import struct num = 156 # 将int类型的数据打包成4个字节的数据 num_stru = struct.pack("i", num) print(len(num_stru)) # 4 print(num_stru) # b'\x9c\x00\x00\x00' print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') # 在通过int类型解包,将前面打包的数据解包打包之前的int数据 num2 = struct.unpack('i', num_stru) # 解包出来是一个元组 print(num2) # (156,) print(num2[0]) # 156
好,到这里我们将struck这个模块将int类型的数据打包成四个字节的方法了,那么我们就来使用它解决粘包吧。
先看一段:
1 import json,struct 2 #假设通过客户端上传1T:1073741824000的文件a.txt 3 4 #为避免粘包,必须自定制报头 5 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 6 7 #为了该报头能传送,需要序列化并且转为bytes,因为bytes只能将字符串类型的数据转换为bytes类型的,所有需要先序列化一下这个字典,字典不能直接转化为bytes 8 head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 9 10 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 11 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 12 13 #客户端开始发送 14 conn.send(head_len_bytes) #先发报头的长度,4个bytes 15 conn.send(head_bytes) #再发报头的字节格式 16 conn.sendall(文件内容) #然后发真实内容的字节格式 17 18 #服务端开始接收 19 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 20 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 21 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 22 header=json.loads(json.dumps(header)) #提取报头 23 24 #最后根据报头的内容提取真实的数据,比如 25 real_data_len=s.recv(header['file_size']) 26 s.recv(real_data_len)
下面看正式的代码:
报头:就是消息的头部信息,我们要发送的真实内容为报头后面的内容。
1 import socket 2 import subprocess 3 import struct 4 5 server = socket.socket() 6 server.bind(('127.0.0.1', 8820)) 7 server.listen() 8 conn, addr = server.accept() 9 while 1: 10 # ;来自客户端的指令 11 print("等待接受信息......") 12 from_client_cmd = conn.recv(1024).decode('utf-8') 13 # 通过subprocess模块执行服务端的系统指令,并且拿到执行结果 14 sub_obj = subprocess.Popen( 15 from_client_cmd, # 客户端的指令 16 shell=True, 17 stdout=subprocess.PIPE, 18 stderr=subprocess.PIPE 19 ) 20 # 接受到的返回信息是bytes类型的,并且windows系统的默认编码为gbk 21 server_cmd_msg = sub_obj.stdout.read() 22 # 首先计算出你将要发出的数据的长度 23 cmd_msg_len = len(server_cmd_msg) 24 # 先对数据长度进行打包,打包成4个字节的数据, 25 # 目的是为了和你将要发送的数据拼接在一起,就好比我们自制了一个消息头 26 msg_len_stru = struct.pack('i', cmd_msg_len) 27 conn.send(msg_len_stru) # 首先发送打包成功后的那四个字节的数据 28 conn.sendall(server_cmd_msg) # 循环send数据,直到数据全部发送成功 29
1 import socket 2 import struct 3 4 client = socket.socket() 5 client.connect(('127.0.0.1', 8820)) 6 while 1: 7 msg = input("请输入要执行的指令>>>") 8 client.send(msg.encode('utf-8')) 9 # 先接受服务端要发送给我的信息的长度,前4个字节,固定的 10 from_server_msglen = client.recv(4) 11 unpack_len_msg = struct.unpack('i', from_server_msglen)[0] 12 # 接受数据长度统计,和服务端发给我的数据长度作比较,来确定跳出循环的条件 13 recv_msg_len = 0 14 # 统计拼接接收到的数据,注意:这个不是统计长度 15 all_msg = b'' 16 while recv_msg_len < unpack_len_msg: 17 every_recv_data = client.recv(1024) 18 # 将每次接收的数据进行拼接和统计 19 all_msg += every_recv_data 20 # 对每次接收到的数据长度进行累加 21 recv_msg_len += len(every_recv_data) 22 23 print(all_msg.decode("gbk")) 24 25
socketserver
为什么要讲socketserver?我们之前写的tcp协议的socket是不是一次只能和一个客户端通信,如果用socketserver可以实现和多个客户端通信。它是在socket的基础上进行了一层封装,也就是说底层还是调用的socket,在py2.7里面叫做SocketServer也就是大写了两个S,在py3里面就小写了。后面我们要写的FTP作业,需要用它来实现并发,也就是同时可以和多个客户端进行通信,多个人可以同时进行上传下载等。
那么我们先看socketserver怎么用呢,然后在分析,先看下面的代码
ThreadingTCPServer,多线程,简单解释:看图
通过上面的代码,我们来分析socket的源码:(大家还记得面向对象的继承吗,来,实战的时候来啦)
在整个socketserver这个模块中,其实就干了两件事情:1、一个是循环建立链接的部分,每个客户链接都可以连接成功 2、一个通讯循环的部分,就是每个客户端链接成功之后,要循环的和客户端进行通信。
看代码中的:server=socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)
还记得面向对象的继承吗?来,大家自己尝试着看看源码:
查找属性的顺序:
ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer
实例化得到server,先找ThreadMinxIn中的__init__方法,发现没有init方法,然后找类ThreadingTCPServer的__init__,在TCPServer中找到,在里面创建了socket对象,进而执行server_bind(相当于bind),server_active(点进去看执行了listen)
找server下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....
源码分析总结:
基于tcp的socketserver我们自己定义的类中的
self.server即套接字对象
self.request即一个链接
self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的
self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
self.client_address即客户端地址
伪代码:
socketserver Import socketserver Class A(socketserver.BaseRequestHandler): Def handle(self): Self.request.recv() #conn Send close if __name__ == ‘__main__’: Ip_port = (‘127.0.0.1’,8001) Server = socketserver.ThreadingTCPServer(ip_port,A) Server.serve_forever() 前提:原生的tcp连接不能连接多个用户
一个完整的sockeserver代码示例:
服务端代码示例:
server端:
1 import socketserver 2 3 # 1定义一个类 4 class MySer(socketserver.BaseRequestHandler): # 2类里面继承socketserver.BaseRequestHandler 5 # 3类里面定义一个handle方法,handle名称不能变 6 def handle(self): 7 while 1: 8 # self.requse 就是conn链接通道 9 from_clinet_data = self.request.recv(1024).decode('utf-8') 10 print(from_clinet_data) 11 server_input = input("说>>>>>>") 12 self.request.send(server_input.encode('utf-8')) 13 # self.request.close() 14 15 16 if __name__ == '__main__': 17 # 服务端的ip地址和端口 18 ip_port = ('127.0.0.1', 8820) 19 socketserver.TCPServer.allow_reuse_address = True 20 # 绑定IP地址和端口,并且启动我定义的上面这个类 21 server = socketserver.ThreadingTCPServer(ip_port, MySer) 22 # 永久的给我执行下去 23 server.serve_forever()
client端:
import socket tcp_clinet = socket.socket() server_ip_port = ('127.0.0.1', 8820) tcp_clinet.connect(server_ip_port) while 1: client_msg = input("客户说>>>>") tcp_clinet.send(client_msg.encode("utf-8")) from_server_msg = tcp_clinet.recv(1024).decode('utf-8') print(from_server_msg)