1.定义
又称为C/S架构,S 指的是Server(服务端软件),C指的是Client(客户端软件)
本章的中点就是教大写写一个c/s架构的软件,实现服务端软件和客户端软件基于网络的通讯。
2.互联网中的c/s架构应用
腾讯做为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频)
浏览网页,浏览器是客户端软件,服务端软件在后端服务器上
3.C/S架构与socket的关系:html
咱们学习socket就是为了完成C/S架构的开发,其中socket是一个Python给咱们封装好的,方便开发不须要很是精通各类网络协议便可进行相关软件开发。python
1.前言linux
咱们从拥有一台我的计算机开始,就有意无心的接触了一台小型,因此一个完整的计算机系统:其中由硬件,计算机操做系统,应用软件等组成,git
而咱们要上网看视频、聊天等,须要使用基于操做系统之上的应用软件或者各类客户端。那这些应用软件是如何链接互联网,与互联网上的其余电脑进行通讯的呢?它基层的通讯原理是怎么样的呢?请看下面分解。算法
2.互联网协议由来shell
若是把计算机比做人,互联网协议就是计算机界的英语。全部的计算机都学会了互联网协议,那全部的计算机都就能够按照统一的标准去收发信息从而完成通讯了。编程
因此可以咱们可以链接互联网,核心就是由一堆协议组成,协议就是标准,好比全世界人通讯的标准是英语。json
人们按照分工不一样把互联网协议从逻辑上划分了层级,有国际组织就制订了统一的标准 7层的OSI 网络模型,固然还有其余分类。小程序
3.网络通讯原理详解windows
请看并了解基本网络基础知识,这个很重要。
http://www.javashuo.com/article/p-tmohapqf-p.html
4.学习socket为什么要学习网络协议呢?
1.首先:本节课程的目标就是教会你如何基于socket编程,来开发一款本身的C/S架构软件
2.其次:C/S架构的软件(软件属于应用层)是基于网络进行通讯的
3.而后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通讯的软件,就必须遵循这些标准。
4.最后:就让咱们从这些标准开始研究,开启咱们的socket编程之旅
在图1中,咱们没有看到Socket的影子,那么它到底在哪里呢?仍是用图来讲话,一目了然。
Socket是应用层与TCP/IP协议族通讯的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对 用户来讲,一组简单的接口就是所有,让Socket去组织数据,以符合指定的协议。
因此,咱们无需深刻理解tcp/udp协议,socket已经为咱们封装好了,咱们只须要遵循socket的规定去编程,写出的程序天然就是遵循tcp/udp标准的。
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序 而程序的pid是同一台机器上不一样进程或者线程的标识
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 所以,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字 被设计用在同 一台主机上多个应用程序之间的通信。这也被称进程间通信,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,能够经过访问同一个文件系统间接完成通讯
基于网络类型的套接字家族套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其余的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是不多被使用,或者是根本没有实现,全部地址家族 中,AF_INET是使用最普遍的一个,python支持不少种地址家族,可是因为咱们只关心网络编程,因此大部分时候我么只使用AF_INET)
1.套接字工做原理介绍
在生活中,你要打电话给你的GrilFriends,先拨号,gf听到电话铃声后是在考虑接电话仍是不接电话,若是接电话,这时候你和你的gf就完成创建了链接,就能够讲话了。
若是gf想要结束交流,则告知会告知你她要敷面膜洗澡睡觉了,你也回应了好的,晚安,而后彼此结束交谈。
因此下图,就是这个原理。
先从服务端提及:服务端初始化Socket,而后与端口绑定(bind()),对端口进行监听(listen()),调用accept() 阻塞,等待客户端链接。
在这个时候若是有一个客户端初始化一个Socket,而后链接服务器(connect),若是链接成功,这是客户端与服务端的链接就创建了。
客户端发送数据请求,服务端接收请求并处理请求,而后回应数据发送给客户端,客户端读取数据,最后彼此关闭链接,一次交互结束。
2.socket 模块函数用法
# @Time : 2018/9/6 16:05 # @Author : Jame import socket ''' socket.socket(socket_family,socket_type,protocal=0) socket_family 能够是AF_UNIX 或AF_INET. socket_type 能够是SOCK_STREAM 或SOCK_DGRAM. protocal 通常不填写,默认值为0 ''' #获取tcp/ip套接字 tcpSock=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #获取upd/ip套接字 udpSock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) ''' 因为socket模块中有太多的属性。 因此咱们能够破例使用 from socket import * 这样咱们就把socket模块里全部的属性都带到了咱们的命名空间中,能够缩减一些重复代码。 例如:tcpSock=socket(AF_INET,SOCK_STREAM) '''
3.服务端套接字函数、客户端套接字函数
*服务端:
server.bind() 绑定(主机ip,端口号) 到套接字
server.listen() 开始tcp监听(1...) 缓冲池数量1到n个
server.accept() 被动接受tcp 客户端的链接,(阻塞式)等待链接的到来
*客户端:
client.connect() 主动初始化tcp服务器链接
client.connect_ex() connect() 函数的扩展版本,出错时候返回错码,而不是抛出异常
4.公共用途的套接字函数
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() 关闭套接字
5.面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操做的超时时间
s.gettimeout() 获得阻塞套接字操做的超时时间
6.面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 建立一个与套接字相关的文件
# @Time : 2018/9/6 16:43 # @Author : Jame # 1:用打电话的流程快速描述socket通讯 # 2:服务端和客户端加上基于一次连接的循环通讯 # 3:客户端发送空,卡主,证实是从哪一个位置卡的 # 服务端: from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) conn,addr=phone.accept() while True: data=conn.recv(1024) print('server===>') print(data) conn.send(data.upper()) conn.close() phone.close() # 客户端: from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: msg=input('>>: ').strip() phone.send(msg.encode('utf-8')) print('client====>') data=phone.recv(1024) print(data) # 说明卡的缘由:缓冲区为空recv就卡住,引出原理图 # # # # 4.演示客户端断开连接,服务端的状况,提供解决方法 # # 5.演示服务端不能重复接受连接,而服务器都是正常运行不断来接受客户连接的 #
# 6:简单演示udp # 服务端 from socket import * phone=socket(AF_INET,SOCK_DGRAM) phone.bind(('127.0.0.1',8082)) while True: msg,addr=phone.recvfrom(1024) phone.sendto(msg.upper(),addr) # 客户端 from socket import * phone=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ') phone.sendto(msg.encode('utf-8'),('127.0.0.1',8082)) msg,addr=phone.recvfrom(1024) print(msg) # udp客户端能够并发演示 # udp客户端能够输入为空演示,说出recvfrom与recv的区别,暂且不提tcp流和udp报的概念,留到粘包去说 # # 读者勿看:socket实验推演流程
Tcp是基于双向链接的,必须先启动服务端,而后启动客户端去链接服务端,创建初始链接。
1.tcp服务端 和t cp客户端的主要步骤
ss = socket() #建立服务器套接字 ss.bind() #把地址绑定到套接字 ss.listen() #监听连接 inf_loop: #服务器无限循环 cs = ss.accept() #接受客户端连接 comm_loop: #通信循环 cs.recv()/cs.send() #对话(接收与发送) cs.close() #关闭客户端套接字 ss.close() #关闭服务器套接字(可选)
cs = socket() # 建立客户套接字 cs.connect() # 尝试链接服务器 comm_loop: # 通信循环 cs.send()/cs.recv() # 对话(发送/接收) cs.close() # 关闭客户套接字
2.基于tcp的socket实现模拟打电话
# @Time : 2018/9/6 16:55 # @Author : Jame import socket ip_port=('127.0.0.1',8080) #电话卡 BUFSIZE=1024 #收消息的大小 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机 server.bind(ip_port) #手机插卡 server.listen(2) #手机待机等待接听 conn,client_addr=server.accept() #手机接听电话 print('接到来自%s 的电话'%client_addr[0]) msg=conn.recv(BUFSIZE) #听消息 print(msg,type(msg)) conn.send(msg.upper()) #发消息 conn.close() #挂电话 server.close() #关机,防止骚扰 ''' 接到来自127.0.0.1 的电话 b'jame is boy' <class 'bytes'> '''
# @Time : 2018/9/6 16:55 # @Author : Jame import socket ip_port=('127.0.0.1',8080) #电话卡 BUFSIZE=1024 #收消息的大小 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机 client.connect_ex(ip_port) #拨打电话 client.send('jame is boy'.encode('utf-8')) #发消息,给服务端 msg=client.recv(BUFSIZE) #收消息,来自服务端回复 print(msg,type(msg)) client.close() #挂电话,不关机继续打给其余女孩,哈哈 ''' b'JAME IS BOY' <class 'bytes'> '''
3.基于tcp的链接循环和通讯循环的socket
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1买手机 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 插电话卡 server.bind(('127.0.0.1',8080)) #3.开机 server.listen(3) print('server socket start...') #4.等待电话链接中的请求,连接循环 while True: conn,client_addr=server.accept() print(conn,client_addr) #5.收/发信息,通讯循环 while True: try: data=conn.recv(1024) if len(data)==0:break #针对linux max 系统 print(data.decode('utf-8')) conn.send(data.upper()) except ConnectionResetError: #针对windows 系统 break conn.close() server.close()
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1买手机 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 拨号 client.connect(('127.0.0.1',8080)) #3 发/收 信息 while True: msg=input('>>>:').strip() client.send(msg.encode('utf-8')) #必须是bytes类型 data=client.recv(1024) print('服务端发来的消息:',data.decode('utf-8')) client.close()
4.解决服务端Address already in use
这个是因为你的服务端仍然存在四次挥手的time_wait状态在占用地址(若是不懂,请深刻研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发状况下会有大量的time_wait状态的优化方法)
#加入一条socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
发现系统存在大量TIME_WAIT状态的链接,经过调整linux内核参数解决, vi /etc/sysctl.conf 编辑文件,加入如下内容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 而后执行 /sbin/sysctl -p 让参数生效。 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少许SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。容许将TIME-WAIT sockets从新用于新的TCP链接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP链接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
UDP是不须要事先进行链接的,先启动哪一端都不会报错。
1.udp服务端和udp客户端经常使用套路
服务端:
server=socket() #建立一个服务器的套接字 server.bind() #绑定服务器套接字 inf_loop: cs=server.recvfrom() #对话(接送与发送) #或者 server.sendto() server.close() #关闭服务器套接字
客户端:
client=socket(...) #建立套接字 comm_loop: #通信循环 client.sendto() /client.recvfrom() #对话(发送、接收) client.close() #关闭客户端套接字
2.udp套接字简单实例
# @Time : 2018/9/6 17:50 # @Author : Jame import socket ip_port=('127.0.0.1',9090) BUFSIZE=1024 udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: msg,client_addr=udp_server.recvfrom(BUFSIZE) print(msg,client_addr) udp_server.sendto(msg.upper(),client_addr)
# @Time : 2018/9/6 17:50 # @Author : Jame import socket ip_port=('127.0.0.1',9090) BUFSIZE=1024 udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>:').strip() if not msg:continue udp_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_client.recvfrom(BUFSIZE) print(back_msg.decode('utf-8'),addr)
3.模拟qq聊天实例
# @Time : 2018/9/6 18:00 # @Author : Jame import socket ip_port=('127.0.0.1',9091) Buffsize=1024 udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: qq_msg,addr=udp_server.recvfrom(Buffsize) print('来自[%s:%s]的一条信息:%s'%(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回复信息:').strip() udp_server.sendto(back_msg.encode('utf-8'),addr)
# @Time : 2018/9/6 18:04 # @Author : Jame import socket Buffsize=1024 udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ 'dog':('127.0.0.1',9091), 'cat':('127.0.0.1',9091), 'pig':('127.0.0.1',9091) } while True: qq_name=input('请选择聊天对象:').strip() while True: msg=input('请输入消息,回车发送:').strip() if msg=='quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client.recvfrom(Buffsize) print('来自[%s:%s]的一条消息:%s'%(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client.close()
4.应用:ntp时间服务器
#Author http://www.cnblogs.com/Jame-mei from socket import * from time import strftime ip_port=('127.0.0.1',9000) bufsize=1024 tcp_server=socket(AF_INET,SOCK_DGRAM) tcp_server.bind(ip_port) while True: msg, addr = tcp_server.recvfrom(bufsize) print('===>', msg) if not msg or len(msg)<6: time_fmt = '%Y-%m-%d %X' else: time_fmt = msg.decode('utf-8') back_msg = strftime(time_fmt) tcp_server.sendto(back_msg.encode('utf-8'), addr) tcp_server.close()
#Author http://www.cnblogs.com/Jame-mei from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 tcp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('请输入时间格式(例%Y %m %d)>>: ').strip() tcp_client.sendto(msg.encode('utf-8'),ip_port) data=tcp_client.recv(bufsize) print(data.decode('utf-8')) tcp_client.close()
1.粘包问题发现tcp
让咱们基于tcp先制做一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig)
#Author http://www.cnblogs.com/Jame-mei import socket import subprocess #1 买手机 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) print(phone) #2 插电话卡 phone.bind(('127.0.0.1',8080)) #3 开机 phone.listen(3) print('server 端启动') #4 等待电话链接中 while True: conn,client_addr=phone.accept() print('客户端链接信息:',conn,client_addr) #5接受信息 while True: try: data=conn.recv(1024) if len(data)==0:break #针对linux mac 的异常处理,不能接受空消息,不然处于一直等待中 print('客户端发送的信息:',data) data=data.decode('utf-8') obj=subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) #服务端回信息 stdout=obj.stdout.read() #byes类型 stderr=obj.stderr.read() conn.send(stdout+stderr) except ConnectionResetError: #针对windows 系统的异常处理 break #6最后关闭链接 conn.close() #7 关机 phone.close()
#Author http://www.cnblogs.com/Jame-mei import socket #1 买手机 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 插电话卡,发信息 client.connect(('127.0.0.1',8080)) while True: msg=input('Please input>>:').strip() if len(msg)==0:continue client.send(msg.encode('utf-8')) #3 客户端收信息 data=client.recv(1024) print('服务端回复信息:',data.decode('gbk')) #4 关闭客户端链接 client.close()
注意注意注意:
subprocess.Popen 的结果的编码是以当前所在的系统为准的。
若是是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果。
注意:命令ls -l ; lllllll ; pwd 的结果是既有正确stdout结果,又有错误stderr结果
2.udp为何发生粘包问题 ?
udp不会发生粘包问题,除了超过udp接收数据的大小,会报错。
#Author http://www.cnblogs.com/Jame-mei from socket import * import subprocess ip_port=('127.0.0.1',9003) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.bind(ip_port) while True: #收消息 cmd,addr=udp_server.recvfrom(bufsize) print('用户命令----->',cmd) #逻辑处理 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() #发消息 udp_server.sendto(stderr+stdout,addr) udp_server.close()
#Author http://www.cnblogs.com/Jame-mei from socket import * ip_port=('127.0.0.1',9003) bufsize=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(bufsize) print(data.decode('gbk'),end='') ''' udp一次接收的数据量较小,于是适合dns解析,网页聊天等。 OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其余一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。 '''
1.前言
只有tcp有粘包现象,udp永远不会粘包。这是为何呢,须要了解回顾socket收发消息的原理。
2.粘包造成的原理详解
例如:基于tcp的套接字客户端往服务端上传文件,发送文件内容是按照一段一段字节流发送的,在接收方看来,根本不知道该文件的字节流何处开始,何处结束。
发送端能够是1kb 1kb 地发送数据,而接收端的应用程序能够2kb 2kb的提走数据,固然也可能一次提走3kb或者6kb数据,或者一次只提走几个字节。也就是说,应用程序所看到的数据是一 个总体,或者是一个流(stream)。一条消息有多少字节对应用程序来讲是不可见的,所以TCP协议是面向流的协议。这也就是容易粘包的缘由。
所谓粘包问题,主要仍是由于接收方不知消息之间的界限,不知道一次性提取多少字节数据形成的。
而UDP是面向消息的协议,每一个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP很不一样。怎么定义定义消息呢?能够认为对方一次性 write/send的数据为一个消息,须要明白的是当对方send一条信息时候,不管底层怎么分段分片,TCP协议层会把构成整条消息的数据段排序完成后呈现到内核的缓冲区。
此外,发送方引发的粘包是由TCP协议自己形成的,TCP为了提升效率,发送方每每收集足够多的数据后才发送一个TCP段。若连续几回都须要send的数据不多,通畅TCP会根据优化算法把这些数据 合成一个TCP段后一次发送出去,这样接收方就收到了粘在一块儿的数据流了。
*TCP(transport control protocol,传输控制协议)是面向链接的,面向流的,提供高可靠性服务。收发(client/server)两端都要有一一成对的socket,所以发送端为了将多个数据发送到接收端的包,更 有效的到对方,使用了优化算法(Nagle算法),将屡次间隔较小且数据量较小的数据,合并成一个大的数据流,而后进行封包。这样,接收端就很难分辨出从该数据包里的几段数据的始末。因此必须提 供科学的拆包机制(定制报头字典等方法) 。 因此面向流的通讯是无消息保护边界的。
*UDP(user datagram protocol,用户数据报协议) 是无链接的,面向消息的,提供高效服务。不会使用块的合并优化算法。因为UDP支持的是一对多模式,因此接收端的skbuff(套接字缓冲区)采用了链 式结构来记录每个达到UDP包,在每一个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来讲,就容易区分处理了。因此面向消息的通讯是有消息保护边界的。
总结:
tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和服务端都添加空消息处理机制,防止程序阻塞卡住。而udp时基于数据报的,即使输入的消息为空,那也不是空消息,udp会帮你 封装上消息头,详情请九.2 udp ssh模拟实例。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对惟一一个sendinto(y),收完了x字节的数据就算完成,如果y>x数据就丢失,这意味着udp根本不会粘包,可是会丢失数据,不可靠。
tcp的协议数据不会丢失,没有收完,下次会继续接收上次的,收取端收到ack才会清除缓冲区内容。数据是可靠的,可是会产生粘包问题。
3.发生粘包的两种状况
1):发送端须要等缓冲区满才发送出去,形成粘包(发送数据时间间隔短,数据量很小,汇合到一块儿,产生粘包问题)
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8080) tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(2) conn,client_addr=tcp_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('第一条----->',data1.decode('utf-8')) print('第二条---->',data2.decode('utf-8'))
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8080) BUFSIZE=1024 tcp_client=socket(AF_INET,SOCK_STREAM) res=tcp_client.connect_ex(ip_port) tcp_client.send('hello'.encode('utf-8')) tcp_client.send('jame'.encode('utf-8')) ''' 第一条-----> hellojame 第二条----> 第一条-----> hello 第二条----> jame 出现这种状况的缘由就是2条数据量小,时间间隔短 '''
2):接收方不及时收取缓冲区的包,形成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候仍是会从缓冲区拿上次遗留的数据,产生粘包)
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8081) tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(2) conn,client_addr=tcp_server.accept() data1=conn.recv(2) #第一次设置只收2个字节 不收完 data2=conn.recv(10) #下次收的时候,会先收取旧的数据,而后取新的 print('第一条----->',data1.decode('utf-8')) print('第二条---->',data2.decode('utf-8')) ''' 第一条-----> he 第二条----> llo jame #发生这种粘包问题是,收取放第一次只收取了2个字节,没有及时收取准确的大小,致使下一次收取了旧数据+新数据 ''' conn.close() tcp_server.close()
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8081) BUFSIZE=1024 tcp_client=socket(AF_INET,SOCK_STREAM) res=tcp_client.connect_ex(ip_port) tcp_client.send('hello jame'.encode('utf-8'))
拆包的状况发生:
当发送端缓冲区的长度大于网卡的MTU时候,tcp会将此次发送的数据拆成几个数据包发送出去。
4.补充tcp/udp send/recv/sendall相关问题
1):为什么tcp是可靠的,udp是不可靠的?
基于tcp的传输原理请参考:https://www.cnblogs.com/Jame-mei/p/9571728.html
由于tcp在传输的时候,发送端会把数据发送到本身的缓存中,而后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则从新发送数据,所 以tcp是可靠的。
udp发送数据的时候,对端是不会返回确认信息的,所以不可靠。
2):send(字节流)和recv(1024)及sendall
recv里指定1024意思是从缓存里一次拿出1024个字节的数据。
send的字节流是先存入已端缓存,而后由协议控制将缓存内容发送对端,若是待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。
粘包问题的根源在于,接收端不知道发送端传送的字节流长度,因此解决粘包问题的方法就是,围绕如何让发送端在发送数据的前,把本身将要发送的字节流总大小让接收端 提早知晓,而后接收端来 一个循环接收完指定长度的字节流便可。
from socket import * import subprocess import struct phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) print('服务的启动......') # 链接循环 while True: conn,client_addr=phone.accept() print(client_addr) # 通讯循环 while True: try: cmd=conn.recv(1024) if not cmd:break obj=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() # 一、先发送固定长度的报头 #目前报头里只包含数据的大小 total_size=len(stdout) + len(stderr) conn.send(struct.pack('i',total_size)) # 二、发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() phone.close()
from socket import * import struct phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: cmd=input('>>>: ').strip() if not cmd:continue phone.send(cmd.encode('utf-8')) #一、先收报头,从报头里取出对真实数据的描述信息 header=phone.recv(4) total_size=struct.unpack('i',header)[0] #二、循环接收真实的数据,直到收干净为止 recv_size=0 res=b'' while recv_size < total_size: recv_data=phone.recv(1024) res+=recv_data recv_size+=len(recv_data) print(res.decode('gbk')) phone.close()
import struct obj=struct.pack('q',53112312311231223) print(obj,len(obj)) # # res=struct.unpack('i',obj)[0] # print(res)
思路:先发送指定的报头及报头内容,其中为字节流加上自定义的报头信息,包括字节流长度,md5值,文件名等等,对端先接受固定长度的报头,从中读取真实字节流长度及其余信息,而后循环收取,直到完全收取完毕。
用到的模块:struct,能够把一个类型,如数字,转换成固定长度bytes。
经常使用的又struct('i',12345678),4个字节,l(long) 4个字节,q(longlong) 8个字节,具体请查阅帮助。
#_*_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的详细用法
具体步骤:
咱们能够把报头作成字典,字典里包含将要发送的真实数据的详细信息,而后json序列化,而后用struck将序列化后的数据长度打包成4个字节(4个本身足够用了)
发送时:
先发报头长度
再编码报头内容而后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,而后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,而后去取真实的数据内容
from socket import * import subprocess import struct import json phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) print('服务的启动......') # 链接循环 while True: conn,client_addr=phone.accept() print(client_addr) # 通讯循环 while True: try: cmd=conn.recv(1024) if not cmd:break obj=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() #制做报头 header_dic={ 'filename':'a.txt', 'total_size':len(stdout) + len(stderr), 'md5':'xxxxxsadfasdf123234e123' } header_json = json.dumps(header_dic) header_bytes=header_json.encode('utf-8') #一、先发送报头的长度 conn.send(struct.pack('i',len(header_bytes))) #二、再发送报头 conn.send(header_bytes) #三、最后发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() phone.close()
from socket import * import struct import json phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: cmd=input('>>>: ').strip() if not cmd:continue phone.send(cmd.encode('utf-8')) #一、先收报头的长度 obj=phone.recv(4) header_size=struct.unpack('i',obj)[0] #二、再接收报头 header_bytes=phone.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #三、循环接收真实的数据,直到收干净为止 recv_size=0 res=b'' while recv_size < total_size: recv_data=phone.recv(1024) res+=recv_data recv_size+=len(recv_data) print(res.decode('gbk')) phone.close()
思路:在分布式系统中,简单的实现一个客户端认证功能,不像sll那么复杂的,利用hmac+验证的方式来实现。
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os secret_key=b'jia zhuang shi secret key file are you ok?' def con_auth(conn): ''' 认证客户端链接 :param conn: :return: ''' print('开始验证新链接合法性?') msg=os.urandom(32) conn.sendall(msg) h=hmac.new(secret_key,msg) digest=h.digest() respone=conn.recv(len(digest)) return hmac.compare_digest(respone,digest) def data_handler(conn,bufsize=1024): if not con_auth(conn): print('该连接不合法,请关闭!') conn.close() return print('连接合法,开始通讯!') while True: data=conn.recv(bufsize) if not data:break conn.sendall(data.upper()) def server_handler(ip_port,bufsize,backlog=5): ''' 只处理链接 :param ip_port: :param bufsize: :param backlog: :return: ''' tcp_socket=socket(AF_INET,SOCK_STREAM) tcp_socket.bind(ip_port) tcp_socket.listen(backlog) while True: conn,addr=tcp_socket.accept() print('新链接[%s :%s]'%(addr[0],addr[1])) data_handler(conn,bufsize) if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 server_handler(ip_port,bufsize)
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os secret_key=b'jia zhuang shi secret key file are you ok?' def conn_auth(conn): ''' 验证客户端到服务器端的链接 :param conn: :return: ''' 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) tcp_client.connect(ip_port) conn_auth(tcp_client) while True: data=input('>>>>:').strip() if not data:continue if data=='quit':break tcp_client.sendall(data.encode('utf-8')) respone=tcp_client.recv(bufsize) print(respone.decode('utf-8')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize) ''' server输出: 新链接[127.0.0.1 :8761] 开始验证新链接合法性? 连接合法,开始通讯! client输出: >>>>:nihao NIHAO >>>>: '''
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os ''' 非法客户端:不知道加密方式!!! secret_key=b'jia zhuang shi secret key file are you ok?' 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) tcp_client.connect(ip_port) #conn_auth(tcp_client) while True: data=input('>>>>:').strip() if not data:continue if data=='quit':break tcp_client.sendall(data.encode('utf-8')) respone=tcp_client.recv(bufsize) print(respone.decode('utf-8')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize) ''' server输出: 新链接[127.0.0.1 :8746] 开始验证新链接合法性? 该连接不合法,请关闭! client输出: >>>>:nihao Traceback (most recent call last): File "E:/pythonwork/s14/day9/基于认证的客户端链接合法性/非法客户端.py", line 39, in <module> client_handler(ip_port,bufsize) File "E:/pythonwork/s14/day9/基于认证的客户端链接合法性/非法客户端.py", line 30, in client_handler print(respone.decode('utf-8')) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc9 in position 1: invalid continuation byte '''
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os secret_key=b'bu zhi dao secret key file????' def conn_auth(conn): ''' 验证客户端到服务器端的链接 :param conn: :return: ''' 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) tcp_client.connect(ip_port) conn_auth(tcp_client) while True: data=input('>>>>:').strip() if not data:continue if data=='quit':break tcp_client.sendall(data.encode('utf-8')) respone=tcp_client.recv(bufsize) print(respone.decode('utf-8')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize) ''' server输出: 新链接[127.0.0.1 :8795] 开始验证新链接合法性? 该连接不合法,请关闭! client输出: >>>>:nihao Traceback (most recent call last): File "E:/pythonwork/s14/day9/基于认证的客户端链接合法性/非法客户端2.py", line 41, in <module> client_handler(ip_port,bufsize) File "E:/pythonwork/s14/day9/基于认证的客户端链接合法性/非法客户端2.py", line 31, in client_handler respone=tcp_client.recv(bufsize) ConnectionAbortedError: [WinError 10053] 您的主机中的软件停止了一个已创建的链接。 '''
1):模拟并发效果:链接循环+通讯循环
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1买手机 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 插电话卡 server.bind(('127.0.0.1',8080)) #3.开机 server.listen(3) print('server socket start...') #4.等待电话链接中的请求,连接循环 while True: conn,client_addr=server.accept() print(conn,client_addr) #5.收/发信息,通讯循环 while True: try: data=conn.recv(1024) if len(data)==0:break #针对linux max 系统 print(data.decode('utf-8')) conn.send(data.upper()) except ConnectionResetError: #针对windows 系统 break conn.close() server.close()
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1买手机 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 拨号 client.connect(('127.0.0.1',8080)) #3 发/收 信息 while True: msg=input('>>>:').strip() client.send(msg.encode('utf-8')) #必须是bytes类型 data=client.recv(1024) print('服务端发来的消息:',data.decode('utf-8')) client.close()
2):socketserver模块使用
(1).socketserver原理
基于tcp的套接字,关键就是两个循环,一个连接循环,一个通讯循环
socketserver模块中分两大类:server类(解决连接问题)和request类(解决通讯问题)
*server类图:
*request类图:
*继承关系:具体请分析源码
(2).基于socketserver实例
# @Time : 2018/8/28 11:14 # @Author : Jame import socketserver import subprocess import struct import json class MyTcphandler(socketserver.BaseRequestHandler): def handle(self): #通讯循环 while True: try: cmd=self.request.recv(1024) if len(cmd)==0:break print('收到:%s:%s的命令:%s'%(self.client_address[0],self.client_address[1],cmd.decode('utf-8'))) obj = subprocess.Popen( cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() #1制做报头 header_dic={ 'total_size':len(stdout)+len(stderr), 'filename':'a.txt' } #转换成json (str类型),再将str类型转化成bytes header_json=json.dumps(header_dic) header_bytes=header_json.encode('utf-8') #2 发送报头长度,报头的数据 self.request.send(struct.pack('i',len(header_bytes))) self.request.send(header_bytes) #3 最后发送真是数据 self.request.send(stderr) self.request.send(stdout) except ConnectionResetError: break if __name__ == '__main__': server=socketserver.ThreadingTCPServer(('127.0.0.1',8088),MyTcphandler) socketserver.TCPServer.request_queue_size=3 server.serve_forever()
# @Time : 2018/8/28 11:20 # @Author : Jame import socket import struct import json #1 买手机 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式协议=>SOCK_STREAM #2 拨号 client.connect(('127.0.0.1',8088)) #3 发 收信息 while True: msg=input('>>: ').strip() if len(msg) == 0:continue client.send(msg.encode('utf-8')) # b'' #3.1 收取报头长度,报头的数据 header_size=struct.unpack('i',client.recv(4))[0] header_bytes=client.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) #3.2 读取真是返回数据 total_size=header_dic['total_size'] res = b'' recv_size =0 while recv_size < total_size: data = client.recv(1024) res += data recv_size += len(data) print(res.decode('gbk')) client.close()
一.需求 开发一个支持多用户在线的FTP程序 要求: 1.用户加密认证 2.容许同时多个用户登录 3.每一个用户有本身的家目录,且只能访问本身的家目录 4.对用户进行磁盘配额,每一个用户的可用空间不一样 5.运行用户在ftp server上随意切换目录(*) 6.运行用户查看当前目录下的文件 7.运行上传和下载文件,保证文件的一致性 8.文件传输过程当中显示进度条 9.支持文件的断电续传功能(*) 二.分析与思路 角色1:用户consumer 功能: *注册 *登录(用hashlib md5进行加密,登录用正向验证,由于md5反解难度大) *有本身的家目录,只能访问本身的家目录内的文件及文件夹 *不一样用户磁盘空间大小不一样{ 思路: 初始化的时候分配随机额度0-50m,而且用户上传的时候跟空间额度比对,会员vip充值,充值后能够提高存储空间。 } *上传 put ,下载get ,并显示进度条,保证文件一致性(md5) { 思路: 上传:能够上传本地路径存在的文件,到服务端的默认保存位置或者家目录。 下载:下载当前家目录里的某个路径的文件,到客户端本地的默认保存位置。 } *断点续传( 思路: 建立临时文件,客户端上传时,服务器检查临时文件,有就发大小发给客户端,客户端seek到文件断点处给服务器端发送数据。 ) *用户在ftp server上随意切换目录( 思路: 用户操做使用cd命令,能够切换到家目录的任意目录,再用ls查看会显示切换后的目录,下次登录会记住切换后的地址,直接登录改地址 ) 角色2:管理员 admin 功能: *查看用户列表信息 *修改用户密码 *删除用户(删除状态,而不是真正删除数据) *能够对用户的磁盘额度进行调整 *设置黑名单,禁止用于登录和使用 https://gitee.com/meijinmeng/Ftp_system_v0.1.git