一.客户端/服务器架构
1.C/S架构:
(1)硬件C/S架构(打印机)
(2)软件C/S架构(web服务)
2.生活中的C/S架构:
饭店是S端,所有食客是C端
3.C/S架构与socket的关系:
socke就是为了完成C/S架构的开发
二.互联网协议osi七层
1.一个完整的计算机系统由硬件,操作系统,应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了,如果要跟别人一起玩,就需要上网,互联网的核心就是由一堆协议组成,协议就是标准,全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都可以按照统一的标准去收发信息从而完成通信了。人们按照分工不同把互联网协议从逻辑上划分了层级。
第一层物理层:目的是传输的连接介质(光缆,双绞线,无线电波),物理层就可以发送电信号010101
第二层数据链路层:负责把发送的电信号010101分组后电信号就有意义了,以太网协议,包含报头(原mac和目标mac)和数据部分,有了原mac和目标mac就可以基于广播的方式发送(mac标识在这个局域网的哪一个位置)
第三层网络层:IP协议标识一个网络的(IP在标识不同的子网在哪里),发送方式首先通过IP地址跟子网掩码地址计算得到一个自己的网络地址和目标网络地址,如果在一个子网里面可直接通信,如果不在一个子网里这个包就会送给网关,网关在转发到一样的子网里
第四层传输层:基于TCP和UDP协议端口方式
TCP协议三次握手和四次挥手
(1)三次握手:是你得让对方知道你已经知道他知道,确定我和对方都准备好了再传输数据
发起一次握手:客户端与服务器发生连接首先要发syn包请求到达服务器端(syn包:原IP,目标IP,原端口,目标端口)
收到一次握手:当服务器端服务器收到syn之后,他的状态就会转变成SYN_RECV(指的是某一个连接) 服务器会创建一个socket_buff
发起二次握手:服务器会回一个包syn+ack
收到二次握手:客户端收到包之后,客户端已经相应了,客户端回第三个包ack发起三次握手
收到三次握手:当服务器收到第三个包之建立好3次握手状态就会变成establi
(2)传输上层数据:
当两端只有建立establi之后,客户端才能传输上层数据
session建立TCP/IP会话
发起一个请求要index.html文件----有数据服务器返回数据
(3)四次断开:四次断开时客户端和服务器都可以发起断开,因为会牵扯到数据传输所以要四次断开(谁先发完包谁就主动发起断开连接)
发起一次断开:客户端发起了一个fin请求给服务端,客户端会进入fin_wait_1状态(主动断开连接请求)
发起二,三次断开:服务器收到fin他会把自己的状态设置成CLOSE_WAIT,他给客户端回一个ack,代表收到fin这个事(被动断开一端会出现CLOSE_WAIT)
发起四次断开:客户端收到后会把状态设置成FIN_WAIT2,等待服务器最后发送的FIN,等收到最后的FIN,状态会变成TIME_WAIT(主动断开的一端会出现TIME_WAIT),当客户收到fin回一个ack(TIME_WAIT默认停留一分钟 等ack数据包)到此整个通讯结束
第五层会话层:解除或建立与其他接点的联系
第六层表示层:数据格式化,代码转换,数据加密
第七层应用层:本机开启一个软件会监听这个端口(跑TCP或UDP协议),端口会跟IP地址还有mac信息相绑定标识了哪一个子网当中一个程序
三.socket
1.socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口的后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议,所以我们无需深入理解TCP/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的
2.socket的五个协议:组成一个逻辑上的文件
(1)原端口-sport
(2)原IP-sip
(3)目标端口-dport:对端主机交给哪个进程的
(4)目标IP-dip
(5)协议-tcp/ip
只要其中任意一个发生改变就不是同一个socket
四.套接字发展及分类
1.一开始,套接字被设计用在同一台主机上多个应用程序之间通讯,这也被称进程间通讯或IPC。套接字有俩种(或者称为有俩个种族),分别是基于文件型的和基于网络型的
2.基于文件类型的套接字家族:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,俩个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
3.基于网络类型的套接字家族:AF_INET
AF_INET是一种广泛的一个,python支持多种地址家族,但是由于无名指关心网络编程,所以大部分时候我们只使用AF_INET
五.TCP/IP 套接字
1.结合现实生活的工作流程
TCP服务端 TCP客户端
socket()--买手机 socket()
bind() --绑定一个手机卡
lister()--开机
accept()--等电话,拿到一个电话链接 connect()拨电话把请求给服务端accept()
read() --收消息 write()发消息给服务端read进行一系列处理
write() --发消息 服务端回应数据给客户端read(),再执行write
close() --断开电话链接
close() --关机 客户端关闭链接close()
2.socket()模块函数用法
import socket
socket.socket(socket_family,socket_type,protocal=0) #socket_family可以是AF_UNIX或AF_INET。socket_type可以是SOCK_STREAM或SOCK_DGRAM。protocol一般不填,默认值为0
#获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
(1)服务端套接字函数:
s.bind():绑定(主机,端口号)到套接字
s.listen():开始TCP监听
saccept():被动接受TCP客户端的连接,(阻塞式)等待连接的到来
(2)客户端套接字函数
s.connect():主动初始化TCP服务器连接
s.connect_ex():connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
(3)公共用途的套接字函数
s.recv():接收TCP数据
s.send():发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall():发送完整的TCP数据(本质就是循环调用send,sendall在待发数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom():接收UDP数据
s.sendto():发送UDP数据
s.getpeername():连接到当前套接字的远端的地址
s.getsockname():当前套接字的地址
s.getsockopt():返回指定套接字的参数
s.setsockopt():设置指定套接字的参数
s.close():关闭套接字
(4)面向锁的套接字方法
s.setblocking()设置套接字的阻塞与非阻塞模式
s.settimeout()设置阻塞套接字操作的超时时间
s.gettimeout()得到阻塞套接字操作的超时时间
(5)面向文件的套接字的函数
s.fileno():套接字的文件描述符
s.makefile():创建一个与该套接字相关的文件
3.用socket方式创建一个服务端和一个客户端使两者进行通信
服务端:
import socket #导入socket模块
#建立三次握手
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #第一步:socket.socket产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给phone创建套接字
phone.bind(('127.0.0.1',8000)) #第二步:把IP地址和访问和端绑定到套接字
phone.listen(5) #第三步:监听链接listen(5)最多可以有五个建立好三次握手后的backlog(半连接池)等着,后面的需要排队等着
conn,addr=phone.accept() #第四步:phone.accept()相当于拿到了TCP三次握手的结果是个元祖解压给给conn(三次握手的连接)和addr
#数据传输,基于TCP三次握手建立好的双向连接才能完成数据传输,收发消息基于网络方式二进制方式
msg=conn.recv(1024) #第八步:收消息
print('客户端发来的消息是: ',msg)
conn.send(msg.upper()) #第九步:服务端给客户端回一个大写的msg
#四次挥手
conn.close() #第十一步:关闭三次握手连接,触发的是四次挥手
phone.close() #第十二步:关闭socket,把这个程序关掉
客户端:
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #第五步:socket.socket产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给phone
phone.connect(('127.0.0.1',8000)) #第六步:找到服务端的IP和端口
phone.send('hello'.encode('utf-8')) #第七步:客户端发要把消息hello进行二进制编码给服务端的msg
data=phone.recv(1024) #第十步:客户端接收服务端消息打印
print('客户端收到服务端的发来的消息:',data)
服务端打印:
客户端发来的消息是: b'hello'
客户端打印:
客户端收到服务端的发来的消息: b'HELLO'
4.基于TCP套接字的通讯流程:创建服务端循环链接请求俩台客户度收发发消息
服务端:
#import socket
from socket import * #由于socket模块中有太多的属性。所以使用from module import *语句。就可以把socket模块里的所有属性都带到我们的命名空间里,这样能大幅减少我们的代码
#把变量提取出来
ip_port=('127.0.0.1',8080)
back_log=5 #半连接池最多可以有5个建立好三次握手后的连接等待
buffer_size=1024 #1024代表接收字节
tcp_server=socket(AF_INET,SOCK_STREAM) #第一步:产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给tcp_server创建服务器套接字
tcp_server.bind(ip_port) #第二步:把IP地址和访问和端口号绑定到套接字
tcp_server.listen(back_log) #第三步:监听链接,listen(5)最多可以有五个建立好三次握手后的backlog(半连接池)等着,后面的需要排队等着
while True: #第四步:服务端做连接循环的接,可以做到接收多个人发的连接
print('服务端开始运行了')
conn,addr=tcp_server.accept() #第五步:tcp_server.accept()相当于拿到了TCP三次握手的结果是个元祖解压给给conn(三次握手的连接)和addr服务端阻塞
print('双向链接是',conn) #打印conn:
print('客户端地址',addr) #打印addr:
while True: #第六步:给收消息和发消息加上通讯循环就可以多次收发消息
try: #做一个异常处理
data=conn.recv(buffer_size) #第七步:服务端收客户端消息,recv是用户态应用程序发起的(网卡来接收消息交给操作系统到内核态内存,应用程序从里面取,如果是空会卡住)
print('客户端发来的消息是',data.decode('utf-8'))
conn.send(data.upper()) #第八步:服务端发送data字节格式通过upper()转大写回给客户端
except Exception: #当客户端断掉不要影响程序的中断
break #跳出当前客户端收发消息这个循环,跳到服务端循环接消息的位置
conn.close() #第九步:关闭三次握手连接,触发的是四次挥手,关闭客户端套接字
tcp_server.close() #第十步:关闭socket,把这个程序关掉,关闭服务器套接字
客户端1:
#import socket
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM) #第一步:客户端产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给tcp_client创建客户端套接字
tcp_client.connect(ip_port) #第二步:客户端连接服务器端的IP和端口
while True: #第三步:给发消息和收消息加上循环可以循环发收消息
msg=input('>>: ').strip() #第四步:客户端让用户输入方式发消息
if not msg:continue #第五步:客户端做判断如果输入为空从新输入
tcp_client.send(msg.encode('utf-8')) #第六步:客户端把用户输入的消息进行二进制编码给服务端的msg(socket发消息会从用户态内存send给内核态内存,发到内核态的内存由操作系统接收,操作系统操作网卡发送出去)
print('客户端已经发送消息')
data=tcp_client.recv(buffer_size) #第七步:客户端接收服务端字节格式
print('收到服务端发来的消息',data.decode('utf-8')) #通过解码看服务端发送的消息
tcp_client.close() #第八步:关闭客户端套接字
客户端2:
#import socket
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM) #第一步:客户端产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议,流式套接字)给tcp_client创建客户端套接字
tcp_client.connect(ip_port) #第二步:客户端连接服务器端的IP和端口
while True: #第三步:给发消息和收消息加上循环可以循环发收消息
msg=input('>>: ').strip() #第四步:客户端让用户输入方式发消息
if not msg:continue #第五步:客户端做判断如果输入为空从新输入
tcp_client.send(msg.encode('utf-8')) #第六步:客户端把用户输入的消息进行二进制编码给服务端的msg(socket发消息会从用户态内存send给内核态内存,发到内核态的内存由操作系统接收,操作系统操作网卡发送出去)
print('客户端已经发送消息')
data=tcp_client.recv(buffer_size) #第七步:客户端接收服务端字节格式
print('收到服务端发来的消息',data.decode('utf-8')) #通过解码看服务端发送的消息
tcp_client.close() #第八步:关闭客户端套接字
当客户端1输入>>: xiaoxi
客户端1返回:
客户端已经发送消息
收到服务端发来的消息 XIAOXI
服务端返回:
服务端开始运行了
双向链接是 <socket.socket fd=372, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 60931)>
客户端地址 ('127.0.0.1', 60931)
客户端发来的消息是 xiaoxi
当客户端1断开连接且客户端2输入:>>: daxi
客户端2返回:
客户端已经发送消息
收到服务端发来的消息 DAXI
服务端返回:
服务端开始运行了
双向链接是 <socket.socket fd=368, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 60932)>
客户端地址 ('127.0.0.1', 60932)
客户端发来的消息是 daxi
六.基于UDP的套接字通讯流程(由于udp无连接,所以可以同时多个客户端去跟服务端通讯)
服务端:
from socket import * #导入模块
ip_port=('127.0.0.1',8080)
buffer_size=1024
udp_server=socket(AF_INET,SOCK_DGRAM) #第一步:服务端产生套接字对象传俩个参数(AF_INET基于网络,SOCK_DGRAM数据报套接字类型)
udp_server.bind(ip_port) #第二步:绑定服务器套接字IP和端口
while True: #第三步:服务器通讯循环
data,addr=udp_server.recvfrom(buffer_size) #第四步:服务端收客户端消息,addr是给我发消息的客户端IP和端口,recvfrom收的时候缓冲区如果没有的话拿到空
print('接收客户端发来的IP和端口',addr) #打印客发来消息的客户端的端口和IP
print('接收客户端发来的消息内容',data) #打印客户端收来的消息
udp_server.sendto(data.upper(),addr) #第五步:服务端发送data字节格式通过upper()转大写回给客户端ddr的IP和端口
udp_server.close() #第六步:关闭服务器套接字
客户端1:
from socket import *
ip_port=('127.0.0.1',8080)
buffer_size=1024
udp_client=socket(AF_INET,SOCK_DGRAM) #第一步:创建客户端产生套接字对象传俩个参数(AF_INET基于网络,SOCK_DGRAM数据报套接字类型)
while True: #第二步:通讯循环
msg=input('>>: ').strip() #客户端让用户输入方式发消息
udp_client.sendto(msg.encode('utf-8'),ip_port) #第三步:客户端把用户输入的消息进行二进制编码给服务端的,udp协议发包没有连接,只能每次发包指定sendto要发给服务端的地址跟端口
data,addr=udp_client.recvfrom(buffer_size) #第四步:客户端接收服务端字节格式
print('客户端接收到服务端返回数据',data)
客户端2:
from socket import *
ip_port=('127.0.0.1',8080)
buffer_size=1024
udp_client=socket(AF_INET,SOCK_DGRAM) #第一步:创建客户端产生套接字对象传俩个参数(AF_INET基于网络,SOCK_DGRAM数据报套接字类型)
while True: #第二步:通讯循环
msg=input('>>: ').strip() #客户端让用户输入方式发消息
udp_client.sendto(msg.encode('utf-8'),ip_port) #第三步:客户端把用户输入的消息进行二进制编码给服务端的,udp协议发包没有连接,只能每次发包指定sendto要发给服务端的地址跟端口
data,addr=udp_client.recvfrom(buffer_size) #第四步:客户端接收服务端字节格式
print('客户端接收到服务端返回数据', data)
客户端1输入:>>: xiaoxi
返回:
客户端接收到服务端返回数据 b'XIAOXI'
客户端2输入:>>: daxi
返回:
客户端接收到服务端返回数据 b'DAXI'
服务端返回:
接收客户端发来的IP和端口 ('127.0.0.1', 61907)
接收客户端发来的消息内容 b'xiaoxi'
接收客户端发来的IP和端口 ('127.0.0.1', 61908)
接收客户端发来的消息内容 b'daxi'
基于udp实现ntp服务
服务端:
from socket import *
import time
ip_port=('127.0.0.1',8080)
buffer_size=1024
udp_server=socket(AF_INET,SOCK_DGRAM) #
udp_server.bind(ip_port)
while True:
data,addr=udp_server.recvfrom(buffer_size)
print(data)
if not data: #判断客户端发来的的空的情况下
fmt='%Y-%m-%d %X' #返回默认时间
else:
fmt=data.decode('utf-8') #如果客户端输入格式就把传的值发来
back_time=time.strftime(fmt)
udp_server.sendto(back_time.encode('utf-8'),addr) #把服务端的时间以encode字符串形式返回给客户端
客户端:
from socket import *
ip_port=('127.0.0.1',8080)
buffer_size=1024
udp_client=socket(AF_INET,SOCK_DGRAM)
while True:
msg=input('>>: ').strip()
udp_client.sendto(msg.encode('utf-8'),ip_port) #客户端发给服务端一个消息
data,addr=udp_client.recvfrom(buffer_size) #服务端返回的时间
print('ntp服务器的标准时间是',data.decode('utf-8'))
客户端输入:>>: %Y
客户端返回:
ntp服务器的标准时间是 2018
服务端返回:
b'%Y'
客户端输入空>>:
客户端返回:
ntp服务器的标准时间是 2018-11-07 13:39:37
服务端返回:
b''
b''
七.recv与recvfrom的区别
tcp:send发消息,recv收消息
udp:sendto发消息,recvfrom收消息是元祖的形式
发消息二者类似,收消息确实有区别
1.tcp协议:
(1)如果收消息缓冲区里的数据为空,那么recv就会堵塞
(2)tcp基于链接通信,如果一端端口链接,那另外一端的链接也跟着完蛋recv将会堵塞,收到的是空
2.udp协议:
(1)如果收消息缓冲区里的数据为空,recvfrom不会堵塞
(2)recvfrom收的数据小于sendinto发送的数据时,数据丢失
(3)只有sendinto发送数据没有recvfrom收数据,数据丢失
注意:
(1)单独运行udp的客户端,并不会报错,相反tcp会报错,因为udp协议只负责把包发出去,对方收不收,根本不管,而tcp基于链接的,必须有一个服务端先运行着,客户端去跟服务端建立链接然后依托于链接才能传递消息,任何一方试图把链接摧毁都会导致对方程序崩溃
(2) udp程序,注释任何一条客户端的sendinto,服务端都会卡住,因为服务端有几个recvfrom就要对应几个sendinto,哪怕是sendinto(b")那也要有
3.总结
(1)udp的sendinto不用管是否是一个正在运行的服务端,可以己端一个劲的发消息
(2)udp的recvfrom是阻塞的,一个recvfrom(x)必须对一个一个sendinto(y),收完了x个字节的数据就算完成,若y>x数据就丢失,这意味udp根本不会粘包,但是会丢数据,不可靠
(3)tcp的协议数据不会丢,己端总是在收到ack时才会清楚缓冲区内容。数据是可靠的,但是会粘包
八.粘包
1.什么是粘包:由于收发消息都是在操作自己的缓冲区,自己的缓冲区根本不归你的应用程序管,而是由操作系统控制的,基于TCP协议工作的话,收消息会收一堆放在自己缓冲区,客户端第一次收到的少了,第二次收还会从缓冲区里接收上一次没收完的,这就是粘包,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够的数据后才发送一个TCP段,若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
2.TCP(transport control protocol,传输控制协议):是面向连接的,面向流的,提高可靠性服务。收发俩端(客户端和服务端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
3.UDP(user datagram protocol,用户数据报协议):是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。即面向消息的通信是有消息保护边界的。
4.tcp是基于数据流的,于是收发消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
5.俩种情况下会发生粘包
(1)发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合到一起,产生粘包)
模拟客户端一次发送三条很小量的数据产生粘包
客户端:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
tcp_client.send('wang'.encode('utf-8'))
tcp_client.send('xixi'.encode('utf-8'))
tcp_client.send('good'.encode('utf-8'))
服务端:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
conn,addr=tcp_server.accept()
data1=conn.recv(buffer_size)
print('第1次数据',data1)
data2=conn.recv(buffer_size)
print('第2次数据',data2)
data3=conn.recv(buffer_size)
print('第3次数据',data3)
客户端运行后服务端接收信息:
第1次数据 b'wangxixigood'
第2次数据 b''
第3次数据 b'
(2)接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
模拟客户端一次发送一条很大量的数据,接收端接收很小产生粘包
客户端:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
tcp_client.send('wangxixigood'.encode('utf-8'))
服务端:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
conn,addr=tcp_server.accept()
data1=conn.recv(2) #一次收俩字节
print('第1次数据',data1)
data2=conn.recv(2) #一次收俩个字节
print('第2次数据',data2)
客户端运行后服务端接收信息:
第1次数据 b'wa'
第2次数据 b'ng'
6.解决粘包的处理方法
(1)粘包问题根源在于,接收端不知道发送端将要传送的字节流长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
(2)为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。
通过解决粘包问题基于tcp实现客户端远程向服务端执行命令
客户端:
from socket import *
import struct #解决粘包问题
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
###启动客户端连接服务端
tcp_client=socket(AF_INET,SOCK_STREAM) #客户端产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给tcp_server
tcp_client.connect(ip_port) #客户端连接服务器端的IP和端口
###客户端给服务端发消息
while True: #第一步:给发消息和收消息加上循环可以循环发收消息
cmd=input('>>: ').strip() #第二步:用户输入的命令赋值给cmd
if not cmd:continue #客户端不可以发空
if cmd == 'quit':break #给客户端加上退出功能
tcp_client.send(cmd.encode('utf-8')) #第三步:客户端把用户输入的命令send发给服务端
#第二步:struct解决粘包
length_data = tcp_client.recv(4) #客户端就收recv4个字节包含服务端发来的长度的数据是byte类型
length = struct.unpack('i', length_data)[0] #用struct.unpack解码收到的byte类型是元祖的形式,元祖的第一个元素就是数据的长度,加上'i'代表解的是整型赋值给length收到的长度
recv_size = 0 #定一个接收的尺寸默认值0
recv_msg=b'' #最后得到的结果recv_msg
while recv_size < length: #有了数据的长度客户端循环从自己的缓冲区一直接收直到把数据完整接收完
recv_msg += tcp_client.recv(buffer_size) #客户端接收recv_msg尺寸加上tcp_client.recv(buffer_size)内容
recv_size=len(recv_msg) #recv_size收了多少真实数据等于len(recv_msg)真实长度=1024字节
#recv_msg = ''.join(iter(partial(tcp_client.recv, buffer_size), b'')) #partial把buffer_size传给tcp_client.recv函数的第一个参数,iter无穷执行(partial(tcp_client.recv, buffer_size)直达运行结果遇到缓冲区为空时候停掉,通过.join转换成字符串形式
print('命令的执行结果是 ',recv_msg.decode('gbk'))
tcp_client.close()
服务端:
from socket import *
import subprocess #subprocess模块执行命令
import struct #解决粘包问题
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
####启动服务端后
tcp_server=socket(AF_INET,SOCK_STREAM) #第一步:产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给tcp_server
tcp_server.bind(ip_port) #第二步:绑定IP地址和访问和端口号
tcp_server.listen(back_log) #第三步:listen(5)最多可以有五个建立好三次握手后的backlog(半连接池)等着,后面的需要排队等着
####客户端连接服务端后
while True: #做连接循环
conn,addr=tcp_server.accept() #第一步:tcp_server.accept()拿到了TCP三次握手的结果是个元祖解压给给conn(三次握手的连接)和addr服务端阻塞
print('打印出接收过来的客户端链接',addr)
while True: #第二步:服务端做通信循环的接,可以做到接收多个人发的连接
###开始收第一个客户端发来的消息
try: #第一步:做异常处理防止客户端非正常断开
cmd=conn.recv(buffer_size) #第二步:服务端收客户端消息,recv是用户态应用程序发起的
if not cmd:break #第三步:如果收到的cmd为空的话跳出通讯循环(解决客户端tcp_client.close断开服务端造成死循环问题)
print('打印出客户端所发出的命令',cmd)
#执行命令,得到命令的运行结果cmd_res
res=subprocess.Popen(cmd.decode('utf-8'),shell=True, #第四步:把接过来的字节命令转码decode('utf-8')
stderr=subprocess.PIPE, #subprocess把stderr标准错误输出的结果交给管道PIPE,res拿到的是subprocess.Popen的对象
stdout=subprocess.PIPE, #stdout标准输出
stdin=subprocess.PIPE) #stdin标准输入
#有了subprocess.Popen的对象,就可以通过stdout.read读取管道的内容获取cmd的运行结果,读取后管道PIPE里的内容就空了
err=res.stderr.read() #从错误的管道里读信息赋值给err
if err: #如果读取err有值代表出错
cmd_res=err #cmd_res读取错误信息
else:
cmd_res=res.stdout.read() #如果err没有值,cmd_res读取管道里的正确的值
###服务端发消息回给客户端
if not cmd_res: #第一步:当命令正常执行且cmd_res没有返回值的情况下防止接收到空
cmd_res='执行成功'.encode('gbk') #赋值一个返回值
#第二步:解决粘包: 第一步发数据长度,第二步发数据内容,把数据长度封装在固定的大小范围内后返回给客户端
length = len(cmd_res) #计算cmd_res的长度赋值给length
data_length = struct.pack('i', length) #struct.pack获取的长度length值直接打成整型byte形式,'i'是固定长度是4个字节的赋值给data_length
#相当于给cmd_res这个数据流封装了一个报文头叫data_length,因为cmd_res基于TCP的字节流,只要是字节流代表没有消息的边界,没有边界定一个边界data_length数据头(cmd_res的长度)
conn.send(data_length) #服务端send,把data_length数据长度发给客户端
conn.send(cmd_res) #服务端send,把得到的运行结果cmd_res通过conn.send发给客户端
except Exception as e: #收到异常处理错误
print(e)
break #断开通讯循环
九.socketserver模块实现TCP和UDP的并发
socketserver有俩大类:
1.第一个类:server类基于基本的socket帮你处理连接的
(1)BaseServer类:祖宗类
(2)TCPServer类继承BaseServer类:处理TCP连接
UnixStremServer类继承TCPServer:处理TCP连接用在Unix系统上
(3)UDPServer类继承TCPServer:处理UDP连接
UnixDatagramServer类继承UDPServer:处理UDP连接用在Unix系统上
2.第二个类:request类帮你处理通信的
(1)BaseRequestHandler类:数据通信,每一个请求来都会呼叫handle()方法,继承一个类定义handle()方法
(2)StreamRequestHandler类继承BaseRequestHandler类:数据流
(3)DatagramRequestHand类继承BaseRequestHandler类:数据报
3.进程并发
(1)ForkingUDPServer类进程优先继承ForkingMixIn类没有继承UDPServer类
(2)ForkinTCPServer类进程优先继承ForkingMixIn类没有继承TCPServer类
4.线程并发
(1)ThreadingUDPServer类线程优先继承ThreadingMixIn类没有继承UDPServer类
(2)ThreadingTCPServer类线程优先继承ThreadingMixIn类没有继承TCPServer类
5.利用socketserver模块实现TCP多客户端连接并发
服务端代码:
import socketserver
#客户端连接到服务端会进入通讯循环,一进入通讯循环就是调MyServer里的__init__函数里的handle方法接收conn和addr
'''
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
'''
class MyServer(socketserver.BaseRequestHandler): #定一个类MyServer继承socketserver下面的BaseRequestHandler类(客户端每来一个新的连接用MyServer类实例化得到一个实例,然后跟你进行通信)
def handle(self): #定义handle方法收发消息,handle属于MyServer类的函数属性,一次连接的self是实例(包含俩个信息request和client_address)
print('conn is: ',self.request) #conn:接收的连接
print('addr is: ',self.client_address) #addr:是给我发消息的客户端IP和端口
while True: #给发消息和收消息加上通信循环可以循环收发消息
try: #在通信循环做异常处理防止客户端非正常断开
###收消息
data=self.request.recv(1024) #self.request相当于conn.recv
if not data:break #解决不断的收
print('收到客户端的消息是',data,self.client_address) #client_address是到底那个客户端发的
###发消息
self.request.sendall(data.upper()) #self.request相当于conn.sendall
except Exception as e: #收到异常处理错误
print(e)
break #断开通讯循环
if __name__ == '__main__':
s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) #调socketserver模块下面的ThreadingTCPServer(多线程的TCP服务端)类处理连接,这个类传俩个参数IP端口元祖形式,第二个参数是MyServer类,ThreadingTCPServer类加上括号实例化得到结果赋值给s
# s=socketserver.ForkingTCPServer(('127.0.0.1',8080),MyServer) #多进程TCP服务端(linux系统实现)
#s里包含了以下信息
print(s.server_address)
print(s.RequestHandlerClass)
print(MyServer)
print(s.socket)
print(s.server_address)
#
s.serve_forever() #socketserver.ThreadingTCPServer内置的s.serve_forevery方法完成连接循环,连接循环里面套一个通信循环,MyServer这个类进行实例化得到的实例跟客户端进行通讯
客户端1代码:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
msg=input('>>: ').strip()
if not msg:continue
if msg == 'quit':break
tcp_client.send(msg.encode('utf-8'))
data=tcp_client.recv(buffer_size)
print('收到服务端发来的消息:',data.decode('utf-8'))
tcp_client.close()
客户端2代码:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024
tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
msg=input('>>: ').strip()
if not msg:continue
if msg == 'quit':break
tcp_client.send(msg.encode('utf-8'))
data=tcp_client.recv(buffer_size)
print('收到服务端发来的消息:',data.decode('utf-8'))
tcp_client.close()
服务端客户端启动后:
服务端返回:
('127.0.0.1', 8080)
<class '__main__.MyServer'>
<class '__main__.MyServer'>
<socket.socket fd=368, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>
('127.0.0.1', 8080)
conn is: <socket.socket fd=392, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 50129)>
addr is: ('127.0.0.1', 50129)
conn is: <socket.socket fd=424, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 50130)>
addr is: ('127.0.0.1', 50130)
客户端1执行>>: xixi
客户端1返回:
收到服务端发来的消息: XIXI
客户端2执行:>>: yaoyao
客户端2返回:
收到服务端发来的消息: YAOYAO
服务端返回:
收到客户端的消息是 b'xixi' ('127.0.0.1', 50129)
收到客户端的消息是 b'yaoyao' ('127.0.0.1', 50130)
6.利用hmac+加盐的方式来实现认证客户端的链接合法性
客户端:
from socket import *
import struct #解决粘包问题
import hmac,os
secret_key=b'wang xi xi'
def conn_auth(conn):
msg=conn.recv(32)
h=hmac.new(secret_key,msg)
digest=h.digest()
conn.sendall(digest)
def client_handler(ip_port,bufsize=1024):
###启动客户端连接服务端
tcp_client=socket(AF_INET,SOCK_STREAM) #客户端产生一个对象传俩个参数(socket.AF_INET基于网络通讯,socket.SOCK_STREAM表TCP协议)给tcp_server
tcp_client.connect(ip_port) #客户端连接服务器端的IP和端口
conn_auth(tcp_client)
###客户端给服务端发消息
while True: #第一步:给发消息和收消息加上循环可以循环发收消息
cmd=input('>>: ').strip() #第二步:用户输入的命令赋值给cmd
if not cmd:continue #客户端不可以发空
if cmd == 'quit':break #给客户端加上退出功能
tcp_client.send(cmd.encode('utf-8')) #第三步:客户端把用户输入的命令send发给服务端
#第二步:struct解决粘包
length_data = tcp_client.recv(4) #客户端就收recv4个字节包含服务端发来的长度的数据是byte类型
length = struct.unpack('i', length_data)[0] #用struct.unpack解码收到的byte类型是元祖的形式,元祖的第一个元素就是数据的长度,加上'i'代表解的是整型赋值给length收到的长度
recv_size = 0 #定一个接收的尺寸默认值0
recv_msg=b'' #最后得到的结果recv_msg
while recv_size < length: #有了数据的长度客户端循环从自己的缓冲区一直接收直到把数据完整接收完
recv_msg += tcp_client.recv(bufsize) #客户端接收recv_msg尺寸加上tcp_client.recv(buffer_size)内容
recv_size=len(recv_msg) #recv_size收了多少真实数据等于len(recv_msg)真实长度=1024字节
#recv_msg = ''.join(iter(partial(tcp_client.recv, buffer_size), b'')) #partial把buffer_size传给tcp_client.recv函数的第一个参数,iter无穷执行(partial(tcp_client.recv, buffer_size)直达运行结果遇到缓冲区为空时候停掉,通过.join转换成字符串形式
print('命令的执行结果是 ',recv_msg.decode('gbk'))
tcp_client.close()
if __name__ == '__main__':
ip_port=('127.0.0.1',9999)
bufsize=1024
client_handler(ip_port,bufsize)
服务端:
#_*_coding:utf-8_*_
from socket import *
import subprocess #subprocess模块执行命令
import struct #解决粘包问题
import hmac,os
import socketserver
#第一步:客户端验证
secret_key=b'wang xi xi'
def conn_auth(conn): #定义认证客户端链接函数
print('开始验证新链接的合法性')
msg=os.urandom(32) #产生位32字节的随机数
conn.sendall(msg) #发送给客户端
h=hmac.new(secret_key,msg) #把secret_key自定义的盐和msg产生的32位随机数添加到hmac里得到的对象是h
digest=h.digest() #拿到对象h用digest()得到数字形式赋值(32位随机字符串和加盐得到值)给digest
respone=conn.recv(len(digest)) #服务端conn会收跟客户端发过来跟respone长度一样的字节
return hmac.compare_digest(respone,digest) #hmac.compare_digest比较respone和digest这俩个数字是不是一样结果产生布尔值交给通讯里的if判断
#第三步:处理通讯
def data_handler(conn,bufsize=1024):
if not conn_auth(conn): #判断链接是否合法把conn(收发消息)链接传给定义的认证函数
print('该链接不合法,关闭')
conn.close()
return
print('链接合法,开始通信')
while True: #收发消息做通讯循环
try:
cmd = conn.recv(bufsize) # 第二步:服务端收客户端消息,recv是用户态应用程序发起的
if not cmd:break #如果收到的cmd为空的话跳出通讯循环(解决客户端tcp_client.close断开服务端造成死循环问题)
print('打印出客户端所发出的命令', cmd)
# 执行命令,得到命令的运行结果cmd_res
res = subprocess.Popen(cmd.decode('utf-8'), shell=True, # 第四步:把接过来的字节命令转码decode('utf-8')
stderr=subprocess.PIPE,
# subprocess把stderr标准错误输出的结果交给管道PIPE,res拿到的是subprocess.Popen的对象
stdout=subprocess.PIPE, # stdout标准输出
stdin=subprocess.PIPE) # stdin标准输入
# 有了subprocess.Popen的对象,就可以通过stdout.read读取管道的内容获取cmd的运行结果,读取后管道PIPE里的内容就空了
err = res.stderr.read() # 从错误的管道里读信息赋值给err
if err: # 如果读取err有值代表出错
cmd_res = err # cmd_res读取错误信息
else:
cmd_res = res.stdout.read() # 如果err没有值,cmd_res读取管道里的正确的值
###服务端发消息回给客户端
if not cmd_res: # 第一步:当命令正常执行且cmd_res没有返回值的情况下防止接收到空
cmd_res = '执行成功'.encode('gbk') # 赋值一个返回值
# 第二步:解决粘包: 第一步发数据长度,第二步发数据内容,把数据长度封装在固定的大小范围内后返回给客户端
length = len(cmd_res) # 计算cmd_res的长度赋值给length
data_length = struct.pack('i', length) # struct.pack获取的长度length值直接打成整型byte形式,'i'是固定长度是4个字节的赋值给data_length
# 相当于给cmd_res这个数据流封装了一个报文头叫data_length,因为cmd_res基于TCP的字节流,只要是字节流代表没有消息的边界,没有边界定一个边界data_length数据头(cmd_res的长度)
conn.send(data_length) # 服务端send,把data_length数据长度发给客户端
conn.send(cmd_res) # 服务端send,把得到的运行结果cmd_res通过conn.send发给客户端
except Exception as e: #收到异常处理错误
print(e)
break #断开通讯循环
#第二步:处理链接
def server_handler(ip_port,bufsize,backlog=5):
tcp_socket_server=socket(AF_INET,SOCK_STREAM) #得到socket对象
tcp_socket_server.bind(ip_port) #绑定
tcp_socket_server.listen(backlog)
while True: #做链接循环
conn,addr=tcp_socket_server.accept()
print('新连接[%s:%s]' %(addr[0],addr[1]))
data_handler(conn,bufsize) #调用data_handler通讯循环把conn和bufsize传进去
if __name__ == '__main__':
ip_port=('127.0.0.1',9999)
bufsize=1024
server_handler(ip_port,bufsize)