1.硬件C/S架构(打印机)python
2.软件C/S架构(web服务)【QQ,SSH,MySQL,FTP】linux
咱们学习socket就是为了完成C/S架构的开发web
须知一个完整的计算机系统是由硬件和软件构成,软件又分为:操做系统和应用软件。算法
互联网之间的通讯都必须遵循统一的规范,这个统一的规范就是协议,就比如全世界人通讯的标准是英语,互联网协议就是计算机界的英语,全部的计算机都就能够按照统一的标准去收发信息从而完成通讯了!shell
1.学术界:OSI七层模型编程
2.工业界:TCP四层模型json
二者对比:windows
咱们实际生产中实际上公认的标准就是使用的其实就是TCP四层模型! 缓存
工做在上述四层的协议分别为:安全
数据包的传输过程其实是:
普及一点知识:
TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。关于TCP/IP和HTTP协议的关系,网络有一段比较容易理解的介绍:“咱们在传输数据时,能够只使用(传输层)TCP/IP协议,可是那样的话,若是没有应用层,便没法识别数据内容,若是想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有不少,好比HTTP、FTP、TELNET等,也能够本身定义应用层协议。WEB使用HTTP协议做应用层协议,以封装HTTP 文本信息,而后使用TCP/IP作传输层协议将它发到网络上。
TCP/IP协议栈主要分为四层:应用层、传输层、网络层[网络互连层]、数据链路层[主机到网络层],每层都有相应的协议,以下图:
在网络中,一帧以太网数据包的格式:
咱们知道两个进程若是须要进行通信最基本的一个前提能可以惟一的标示一个进程,在本地进程通信中咱们可使用PID来惟一标示一个进程,但PID只在本地惟一,网络中的两个进程PID冲突概率很大,这时候咱们须要另辟它径了,咱们知道IP层的ip地址能够惟一标示主机,而TCP层协议和端口号能够惟一标示主机的一个进程,这样咱们能够利用ip地址+协议+端口号惟一标示网络中的一个进程,操做系统有0-65535个端口,每一个端口均可以独立对外提供服务。。因此socket本质上就是在2台网络互通的电脑之间,架设一个通道,两台电脑经过这个通道来实现数据的互相传递,也就是说:创建一个socket必须至少有2端, 一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求, 链接创建以后,双方能够互发数据。 好比:【QQ,微信】
可以惟一标示网络中的进程后,它们就能够利用socket进行通讯了,什么是socket呢?咱们常常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操做抽象为几个简单的接口供应用层调用已实现进程在网络中通讯,从而简化咱们的编程!
以下所示:咱们更加形象的给你们展现一下socket抽象层!
从上面能够知道,咱们的socket编程是基于TCP或者UDP的,基于TCP的Socket编程咱们称之为基于TCP的Socket网络编程,基于UDP的Socket编程咱们称之为基于UDP的Socket网络编程!因此,咱们无需深刻理解tcp/udp协议,socket已经为咱们封装好了,咱们只须要遵循socket的规定去编程,写出的程序天然就是遵循tcp/udp标准的。
关键点:socket通讯是两个进程之间的通信,每一个进程对应一个端口号!【区别于:线程】
HTTP与Socket链接的区别: 因为一般状况下Socket链接就是TCP链接,所以Socket链接一旦创建,通讯双方便可开始相互发送数据内容,直到双方链接断开。但在实际网络应用中,客户端到服务器之间的通讯每每须要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的链接而致使 Socket 链接断连,所以须要经过轮询告诉网络,该链接处于活跃状态。因此准确的说:Socket只算是链接,有局限性,适用于文件传输,如:FTP!不适合B/S架构,适合C/S架构。 而HTTP链接使用的是“请求—响应”的方式,不只在请求时须要先创建链接,并且须要客户端向服务器发出请求后,服务器端才能回复数据。适用于B/S架构! 不少状况下,须要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方创建的是Socket链接,服务器就能够直接将数据传送给客户端;若双方创建的是HTTP链接,则服务器须要等到客户端发送一次请求后才能将数据传回给客户端,所以,客户端定时向服务器端发送链接请求,不只能够保持在线,同时也是在“询问”服务器是否有新的数据,若是有就将数据传给客户端。
创建一个socket必须至少有2端, 一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求, 链接创建以后,双方能够互发数据。
各位,咱们知道对于全部的服务端和客户端架构的链接而言,都是先启动服务端,而后客户端发送请求,服务端处理客户端发送的请求,而后将结果返回给客户端,而后再继续!
因此这里咱们先讲服务端和客户端的通讯流程,如上图:
服务器端先初始化Socket,而后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端链接。在这时若是有个客户端初始化一个Socket,而后链接服务器(connect),若是链接成功[三次握手],这时客户端与服务器端的链接就创建了。客户端发送数据请求,服务器端接收请求并处理请求,而后把回应数据发送给客户端,客户端读取数据,最后关闭链接【四次挥手】,一次交互结束。
代码演示:
import socket #导入socket模块 server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #咱们这里编写的代码是基于网络类型的套接字家族(AF_INET),同时在这里咱们指定这是TCP链接协议,TCP协议是流式协议 server.bind(("127.0.0.1",8080)) #这里要注意:绑定IP、端口号的时候 要用 元组的形式!端口号位于:0-65535这个区间 server.listen(5) #这里咱们是写死的,其实这里能够从配置文件中读取的! conn,addr = server.accept() # #接受客户端连接,接收客户端链接(至关于TCP协议中的创建链接的过程【3次握手】),经过该方法能够返回(双方的链接信息,客户端的IP地址和端口号),注意这是元组的形式! print("tcp的链接:",conn) print("客户端的地址",addr) data = conn.recv(1024) #收消息,这个1024是指接收的字节数,获得的是data返回值是bytes二进制值! print("from client msg:%s"%data)
import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("127.0.0.1", 8080)) client.send("hello".encode("utf-8")) # 必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 data = client.recv(1024) print(data) client.close()
最后注意:运行程序时,要先运行服务器代码,再运行客户端代码代码中也必定要将server或client 给close()掉,不然会报出:一般每一个套接字地址(协议/网络地址/端口)只容许使用一次的错误;
2)connect()函数 对于客户端的 connect() 函数,该函数的功能为客户端主动链接服务器,创建链接是经过三次握手,而这个链接的过程是由内核完成, 不是这个函数完成的,这个函数的做用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手链接(三次握手详情,请看《浅谈 TCP 三次握手》), 最后把链接的结果返回给这个函数的返回值(成功链接为0, 失败为-1)。 一般的状况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的状况,这个过程很快完成)。 3)listen()函数 对于服务器,它是被动链接的。举一个生活中的例子,一般的状况下,移动的客服(至关于服务器)是等待着客户(至关于客户端)电话的到来。 而这个过程,须要调用listen()函数。listen() 函数的主要做用就是将数值传递给参数backlog,backlog 的做用是设置内核中链接队列的长度。 def listen(self, backlog=None): (可看源码) 须要注意的是:listen()函数不会阻塞,它主要作的事情为,将该套接字和套接字对应的链接队列长度告诉 Linux 内核,而后,listen()函数就结束。 这样的话,当有一个客户端主动链接(connect()),Linux 内核就自动完成TCP 3次握手,将创建好的连接自动存储到队列中,如此重复。 因此,只要 TCP 服务器调用了 listen(),客户端就能够经过 connect() 和服务器创建链接,而这个链接的过程是由内核完成。 知识点补充:【三次握手的链接队列】 这里详细的介绍一下 listen() 函数的第二个参数( backlog)的做用:告诉内核链接队列的长度。 为了更好的理解 backlog 参数,咱们必须认识到内核为任何一个给定的监听套接口维护两个队列: 1、未完成链接队列(incomplete connection queue),每一个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器, 而服务器正在等待完成相应的 TCP三次握手过程。这些套接口处于 SYN_RCVD 状态。 2、已完成链接队列(completed connection queue),每一个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态。 图解计算机的三次握手: 当来自客户的 SYN 到达时,TCP 在未完成链接队列中建立一个新项,而后响应以三次握手的第二个分节:服务器的 SYN 响应, 其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成链接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到 达或者该项超时为止(曾经源自Berkeley的实现为这些未完成链接的项设置的超时值为75秒)。 若是三次握手正常完成,该项就从未完成链接队列移到已完成链接队列的队尾。 backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,当服务器把这个完成链接队列的某个链接取走后, 这个队列的位置又空出一个,这样来回实现动态平衡,但在高并发 web 服务器中此值显然不够。 accept()函数 accept()函数功能是,从处于 established 状态的链接队列头部取出一个已经完成的链接, 若是这个队列没有已经完成的链接,accept()函数就会阻塞,直到取出队列中已完成的用户链接为止。 若是,服务器不能及时调用 accept() 取走队列中已完成的链接,队列满掉后会怎样呢? UNP(《unix网络编程》)告诉咱们,服务器的链接队列满掉后,服务器不会对再对创建新链接的syn进行应答, 因此客户端的 connect 就会返回 ETIMEDOUT。但实际上Linux的并非这样的,TCP 的链接队列满后, Linux 不会如书中所说的所有拒绝链接,有些会延时链接!
演变一:一次链接,交流屡次[通讯循环]
import socket #导入socket模块 server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #咱们这里编写的代码是基于网络类型的套接字家族(AF_INET),同时在这里咱们指定这是TCP链接协议,TCP协议是流式协议 server.bind(("127.0.0.1",8080)) #这里要注意:绑定IP、端口号的时候 要用 元组的形式!端口号位于:0-65535这个区间 server.listen(5) #这里咱们是写死的,其实这里能够从配置文件中读取的! conn,addr = server.accept() # #接受客户端连接,接收客户端链接(至关于TCP协议中的创建链接的过程【3次握手】),经过该方法能够返回(双方的链接信息,客户端的IP地址和端口号),注意这是元组的形式! print("tcp的链接:",conn) print("客户端的地址",addr) while True: #通信循环 data = conn.recv(1024) #收消息,这个1024是指接收的字节数,获得的是data返回值是bytes二进制值! print("from client msg:%s"%data) conn.send(data.upper()) #给客户端发送消息 ,由于客户端发送过来的是二进制的数据,将数据变成大写以后依旧是二进制数据! conn.close() #关闭链接 只是将tcp链接关掉 server.close() #关闭服务器,把socket套接字给关掉!
import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080)) while True: #通信循环 msg = input(">>: ") client.send(msg.encode("utf-8")) #必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 data = client.recv(1024) print(data) client.close()
演变二:屡次链接【链接循环】
import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(("127.0.0.1",8080)) server.listen(5) while True: #链接循环 conn,addr = server.accept() print("tcp的链接:",conn) print("客户端的地址",addr) while True:#//通信循环 data = conn.recv(1024) print("from client msg:%s"%data) conn.send(data.upper()) conn.close() #链接循环的时候,要记得将这个链接也关闭了! server.close()
客户端代码不变,和上面同样;
可是这里实际是有问题的,也就是说,上面的服务端代码只是形式上的屡次链接,实际上当客户端代码链接关闭以后,在服务器端的conn链接再去调用recv方法就会出异常,ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的链接。缘由:客户端1忽然关闭链接,致使服务端出现异常,从而终止了服务端程序的正常运行!
那怎么办呢?出异常能咋办,异常处理呗,以下:
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 8081)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() print("tcp的链接:", conn) print("客户端的地址", addr) while True: # //通信循环 try: data = conn.recv(1024) print("from client msg:%s" % data) conn.send(data.upper()) except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
演变三:多客户端链接
上面的代码虽然一个客户端能够开启、关闭链接,再开启、再关闭链接,可是不能同时开启多个客户端链接【并发问题】,由于服务端的代码会卡在一个链接里面,也就是说:当两个客户端同时和一个服务器通讯的时候,只有一个客户端能够得到响应,只有这个客户端关闭链接的时候,另外一个客户端才可以获得响应!固然除此以外还有一个问题,就是客户端程序啥都不输入直接回车的问题:综上所述咱们的服务端代码仍是有问题的,主要有如下两个问题:
1.不能处理并发问题
2.当客户端什么都不输入的时候,直接回车,那么服务端的conn.recv(1024)这句代码会卡住,阻塞代码执行[服务器和客户端都在等着收数据];
针对上面的第2个问题,咱们能够在客户端解决,以下所示:
import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8081)) while True: msg = input(">>: ").strip() if not msg: continue #python经常使用的判断字符串为空的方法 client.send(msg.encode("utf-8")) #必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 data = client.recv(1024) print(data) client.close()
咱们如今客户端代码是没问题的,可是此时客户端的代码若是是在MAC系统或者Linux系统上,若是咱们把客户端忽然关闭,服务器端代码会进入死循环,一直输出为空,
缘由就是:服务端代码data = conn.recv(1024) 会接收到空数据,不会报异常,一直输出空!因此这时服务器代码还须要加一个判断,以下所示:
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("192.168.222.130", 8081)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() print("tcp的链接:", conn) print("客户端的地址", addr) while True: # //通信循环 try: data = conn.recv(1024) if not data:break #针对Mac或者Linux系统上的客户端忽然断开链接的异常处理 print("from client msg:%s" % data) conn.send(data.upper()) except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
案例:写一个类ssh服务,将客户端命令在服务器上执行,并将结果返回给客户端!
import socket import subprocess server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 8081)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() print("tcp的链接:", conn) print("客户端的地址", addr) while True: # //通信循环 try: cmd = conn.recv(1024) if not cmd:break #针对Mac或者Linux系统上的客户端忽然断开链接的异常处理 print("from client msg:%s" % cmd) res = subprocess.Popen(cmd.decode("utf-8"), #注意:windows系统上运行的subprocess.Popen()方法,因此默认是以GBK编码的 shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE) error = res.stderr.read() if error: back_msg = error else: back_msg = res.stdout.read() #conn.send(len(back_msg)) conn.send(back_msg) except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8081)) while True: cmd = input(">>: ").strip() if not cmd: continue #python经常使用的判断字符串为空的方法 client.send(cmd.encode("utf-8")) #必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 res = client.recv(1024) print(res.decode("gbk")) #注意:这里必定要用gbk格式的解码 client.close()
运行代码,输入正确命令dir就会输出正确结果,若是输出的是错误命令,就会返回错误信息!
res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 的结果的编码是以当前所在的系统为准的,若是是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端须要用GBK解码 且只能从管道里读一次结果
注意:上面有个问题,就是当输入ipconfig命令的时候显示没问题,可是一旦接着输入下一个命令的时候,那么就会出现显示的不是本条命令的结果,而是显示上一条命令的结果,这样程序就乱了,这就是粘包的现象!
1. 什么是粘包?
须知:只有TCP有粘包现象,UDP永远不会粘包,为什么,且听我娓娓道来
首先须要掌握一个socket收发消息的原理:
从上面咱们客户端和服务端的进行数据传输的时候,实际上咱们从服务端发送到客户端的数据并无直接发送给客户端,而是发送到了服务端的缓存中,而后操做系统再将服务端的缓存中的数据又到了客户端的缓存中,因此在客户端接收的数据也是从客户端本身的缓存中拿到的,而不是直接从服务端获取的!那么操做系统是怎么发送服务端缓存中数据的呢?是经过TCP协议去发的,你这里不是基于TCP的Socket网络编程么,那么它就根据TCP去发,因此你会看到大家的操做系统上都有TCP/IP服务这个模块,只有存在这个服务,你才能发送TCP协议的数据!
说到底:所谓粘包问题主要仍是由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所形成的。
上面问题ipconfig命令的问题解释:
当咱们服务器端发送了ipconfig命令以后,接收方设置1024个字节的时候,这个大小是能够将整个ipconfig命令都接收过来的,而后咱们的应用程序,将在应用程序里执行ipconfig命令,并将结果写回到客户端,可是此时客户端咱们设置的是1024个字节,致使ipconfig的命令结果咱们没法在客户端所有接收,剩下的数据就保存在服务器的缓存中,这样客户端就将客户端缓存中的1024个字节所有输出了,此时计算机程序会执行下一次循环,执行输入,输入以后执行下面的程序就是从服务端的缓存中读数据,这样咱们就看到了输入的命令与输出结果不一致,输出的是上一次命令的结果,此时实际上咱们第二次命令的结果已经输入到客户端的缓存中了,可是此时咱们的程序又进入了下一次循环,先要输入才能看到数据,这就是一个恶性循环了!
小Demo演示:
import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(("127.0.0.1",8080)) server.listen(5) conn,addr = server.accept() cmd = conn.recv(1) print(cmd) data = conn.recv(10) print(data) conn.close() #链接循环的时候,要记得将这个链接也关闭了! server.close()
import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080)) client.send("hello".encode("utf-8")) client.send("world".encode("utf-8")) client.close()
这样就会看出问题了,若是咱们设置的接收字节数小于发送到缓存中的数据,那么一次接收数据的时候就接收不彻底,等下次再接收的时候就会出现粘包的问题!
2.粘包产生的两大缘由:
1.先说TCP:因为TCP协议自己的机制(面向链接的可靠地协议-三次握手机制)客户端与服务器会维持一个链接(Channel),数据在链接不断开的状况下,能够持续不断地将多个数据包发往服务器,可是若是发送的网络数据包过小,那么他自己会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)而后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就没法区分哪些数据包是客户端本身分开发送的,这样产生了粘包;
import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(("127.0.0.1",8080)) server.listen(5) conn,addr = server.accept() cmd = conn.recv(104) print(cmd) data = conn.recv(1024) print(data) conn.close() #链接循环的时候,要记得将这个链接也关闭了! server.close()
import socket import time client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8080)) client.send("hello".encode("utf-8")) time.sleep(5) client.send("world".encode("utf-8")) client.close()
这种问题固然咱们能够手动控制发送的速度,这是能够的,可是问题是若是个人程序在作交互的时候,就是程序来完成的,那么这种人为控制速度的方式就有点不适合了(固然咱们仍是能够经过在客户端程序中import time ,而后在屡次发送数据请求之间使用time.sleep(5)代码),可是若是按照这种方式咱们的高并发也就作不了了!
那还有没有别的方式呢?有的,咱们能够在客户端发送数据的时候,将发送数据的大小也发送过去,让服务器端知道咱们要发送的数据有多长就OK了,以下所示:
import socket import subprocess # subprocess最简单的用法就是调用shell命令了 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 8000)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() while True: # //通信循环 try: cmd = conn.recv(1024) if not cmd: break # 解决当recv方法接收为空,linux或者mac进入死循环问题 print("from client msg:%s" % cmd) res = subprocess.Popen(cmd.decode("utf-8"), # 注意:windows系统上运行的subprocess.Popen()方法,因此默认是以GBK编码的 shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) error = res.stderr.read() if error: back_msg = error else: back_msg = res.stdout.read() print("===",back_msg) conn.send(str(len(back_msg)).encode("utf-8")) #将数据的长度编码成utf-8发过去! conn.send(back_msg) except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
上述代码在发送数据以前咱们先把数据的长度发送过去,这样问题就解决了,可是这里的问题是,这个长度的大小是多少呢?
以下所示:当数据变化的时候,数据的长度也是变化的,因此数据的长度是不固定的!
那有没有什么方法能把一串数字打包成一个二进制,而且长度是固定的,这个问题就解决了,有这么一个模块【struct模块】
那么python中正好提供了一个struct模块,它能够将一个数字编码成二进制,而且这串二进制的长度是固定的,这个问题就解决了!
上述的i,表示将后面的数据打包成4个字节;
接收端在拿到数据以后,只须要解码就OK,以下所示:
解码以后拿到的是一个元组,咱们取出第一个值就是咱们要接收的数据长度,以下所示:
并且数值是整形的!
import socket import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8000)) while True: #客户端只须要通信循环就好 cmd = input(">>: ").strip() if not cmd: continue client.send(cmd.encode("utf-8")) #必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 data = client.recv(4) datasize=struct.unpack("i",data)[0] res = client.recv(datasize) print(res.decode("gbk")) #注意:解码的时候是gbk解码的 client.close()
import socket import struct import subprocess # subprocess最简单的用法就是调用shell命令了 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 8000)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() print("bb") print("tcp的链接:", conn) print("客户端的地址", addr) while True: # //通信循环 try: cmd = conn.recv(1024) if not cmd: break # 解决当recv方法接收为空,linux或者mac进入死循环问题 print("from client msg:%s" % cmd) res = subprocess.Popen(cmd.decode("utf-8"), # 注意:windows系统上运行的subprocess.Popen()方法,因此默认是以GBK编码的 shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) error = res.stderr.read() if error: back_msg = error else: back_msg = res.stdout.read() print(back_msg) conn.send(struct.pack("i", len(back_msg))) conn.send(back_msg) except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
这样发送和接收数据就没问题了,就是在发送数据以前咱们先把数据的长度发送过去!
struct模块详解:
为字节流加上自定义固定长度报头,报头中包含字节流长度,而后一次send到对端,对端在接收时,先从缓存中取出定长的报头,而后再取真实数据
struct模块
该模块能够把一个类型,如数字,转成固定长度的bytes
>>> struct.pack('i',1111111111111)
。。。。。。。。。
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
固然,不用struct模块将数据的长度打包成固定大小的数据发送过去,也能够采用其它方式,好比,连续多个\r\n,具体参考TCP/IP中的解决方式!
还存在什么问题呢?
若是咱们send的数据比较大,当缓存放满的时候,send的数据尚未发完,那么用send函数发送数据的时候是否是就会丢数据啊,那咱们怎么解决呢?咱们可使用在服务器端使用sendall方法,sendall方法能够循环的调用send方法,一直到数据都发送完为止,避免了在发送数据的时候遇到服务器的缓存满的问题,这是在服务器端的解决方案,那么在客户端也不能直接接收全部的数据了,因此客户端代码也须要改动,以下所示:
import socket import struct import subprocess # subprocess最简单的用法就是调用shell命令了 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 8000)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() print("bb") print("tcp的链接:", conn) print("客户端的地址", addr) while True: # //通信循环 try: cmd = conn.recv(1024) if not cmd: break # 解决当recv方法接收为空,linux或者mac进入死循环问题 print("from client msg:%s" % cmd) res = subprocess.Popen(cmd.decode("utf-8"), # 注意:windows系统上运行的subprocess.Popen()方法,因此默认是以GBK编码的 shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) error = res.stderr.read() if error: back_msg = error else: back_msg = res.stdout.read() print(back_msg) conn.send(struct.pack("i", len(back_msg))) conn.sendall(back_msg) #循环调用send方法,直到将大数据发送完毕! except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
import socket import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8000)) while True: #客户端只须要通信循环就好 cmd = input(">>: ").strip() if not cmd: continue client.send(cmd.encode("utf-8")) #必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 data = client.recv(4) datasize=struct.unpack("i",data)[0] # res = client.recv(datasize) recv_size = 0 #存放已经接收的数据大小 recv_bytes = b"" #存放接收的字节 while recv_size < datasize: res = client.recv(1024) recv_bytes += res recv_size +=len(res)#这里注意,不是每次都接收1024哦【最后一次】,因此加的是res的真实长度,而不是1024 print(recv_bytes.decode("gbk")) #注意:解码的时候是gbk解码的 client.close()
还有没有问题呢?
可是上面实际上仍是有问题的,就是服务端设置struct包的 struct.pack('i',len(back_msg))时候,咱们设置的是"i"这个格式的!这个表示的是int类型,标准大小是4个字节,也就是说,这是有大小限制的,当超过这个大小的时候就会出问题,并且咱们发送的其实是由报头+数据两部分组成的,报头中包含数据大小,文件名等信息,因此咱们在服务端的代码就变成了以下:这样报头咱们就能够设置成为字典类型(键对应的值是没有大小限制的)的就能够了,可是字典类型的数据若是要在网络中传输而且在接收端接收到字典以后还能直接使用,咱们就须要将字典序列化,因此在服务端还须要导入json,用来序列化它,转换成json格式以后【JSON本质就相似于键值对形式的字符串】,而后咱们还须要进一步编码,可是此时服务器端的代码就须要先报头的长度给客户端,再发报头头信息给客户端,再发报文信息给客户端!
import socket import struct import json import subprocess # subprocess最简单的用法就是调用shell命令了 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(("127.0.0.1", 8000)) server.listen(5) while True: # 链接循环 conn, addr = server.accept() print("bb") print("tcp的链接:", conn) print("客户端的地址", addr) while True: # //通信循环 try: cmd = conn.recv(1024) if not cmd: break # 解决当recv方法接收为空,linux或者mac进入死循环问题 print("from client msg:%s" % cmd) res = subprocess.Popen(cmd.decode("utf-8"), # 注意:windows系统上运行的subprocess.Popen()方法,因此默认是以GBK编码的 shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) error = res.stderr.read() if error: back_msg = error else: back_msg = res.stdout.read() header_dict={"datasize":len(back_msg)} header_json = json.dumps(header_dict) header_bytes = header_json.encode("utf-8"); conn.send(struct.pack("i", len(header_bytes))) conn.send(header_bytes) conn.sendall(back_msg) except Exception: break conn.close() # 链接循环的时候,要记得将这个链接也关闭了! server.close()
由于服务端是分三次发送的,客户端相应的也要作三次接收【报头长度直接取出4个字节就OK,报头数据也不是很大,因此咱们直接取出来就好,最后获取数据自己】:
import socket import struct import json client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(("127.0.0.1",8000)) while True: #客户端只须要通信循环就好 cmd = input(">>: ").strip() if not cmd: continue client.send(cmd.encode("utf-8")) #必定要注意:发送的数据要是Bytes格式的,即二进制形式的数据 #收报头长度信息 head = client.recv(4) headsize=struct.unpack("i",head)[0] #收报头信息(根据报头长度) head_bytes = client.recv(headsize) head_json = head_bytes.decode("utf-8") #反序列化 head_dict = json.loads(head_json) datasize = head_dict["datasize"] #取出真实数据的长度大小! #收真实的数据 recv_size = 0 recv_bytes = b"" while recv_size < datasize: res = client.recv(1024) recv_bytes += res recv_size +=len(res) print(recv_bytes.decode("gbk","ignore")) #注意:解码的时候是gbk解码的 client.close()
提示:若是在写代码的时候报这个错误:UnicodeDecodeError: ‘XXX' codec can't decode bytes in position 2-5: illegal multibyte sequence
错误缘由:
这是由于遇到了非法字符,例如:全角空格每每有多种不一样的实现方式,好比\xa3\xa0,或者\xa4\x57,
这些字符,看起来都是全角空格,但它们并非“合法”的全角空格
真正的全角空格是\xa1\xa1,所以在转码的过程当中出现了异常。
而以前在处理新浪微博数据时,遇到了非法空格问题致使没法正确解析数据。
解决办法:
#将获取的字符串strTxt作decode时,指明ignore,会忽略非法字符,
#固然对于gbk等编码,处理一样问题的方法是相似的
strTest = strTxt.decode('utf-8', 'ignore')
return strTest
[补充]
默认的参数就是strict,表明遇到非法字符时抛出异常;
若是设置为ignore,则会忽略非法字符;
若是设置为replace,则会用?号取代非法字符;
若是设置为xmlcharrefreplace,则使用XML的字符引用。
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_client.bind(ip_port) while True: msg,addr=udp_server_client.recvfrom(BUFSIZE) print(msg,addr) udp_server_client.sendto(msg.upper(),addr)
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() if not msg:continue udp_server_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_server_client.recvfrom(BUFSIZE) print(back_msg.decode('utf-8'),addr)
UDP和TCP的区别就是:UDP是无链接的,因此UDP虽然是有端口的,可是UDP是不须要监听的【无链接的】,也不须要accept的,并且接收和发送的方法也变成了recvfrom、sendto方法了,而且recvfrom方法的返回值再也不是链接、地址,而是接收的 数据、地址,sendto('data',IPADDR_PORT)方法里面的参数也成了数据和IP地址_端口号,
UDP不会发生粘包现象
UDP(user datagram protocol,用户数据报协议)是无链接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,因为UDP支持的是一对多的模式,因此接收端的skbuff(套接字缓冲区)采用了链式结构来记录每个到达的UDP包,在每一个UDP包中就有了消息头(消息来源地址,端口等信息)[UDP协议底层支持的],这样,对于接收端来讲,就容易进行区分处理了。 即面向消息的通讯是有消息保护边界的。
TCP的三次握手和四次挥手
TCP之因此是数据安全的,是由于在TCP创建链接以后,每次都是须要进行数据确认的,可是UDP在数据传输的时候,没有数据确认这个环节,只管着发,无论对方是否可以接收到,因此说UDP是数据不安全的!
Tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头!
UDP不可靠的链接,应用场景在于QQ,TCP与UDP的区别主要是在创建链接以后,TCP在数据传输的时候是有数据确认功能的,而UDP是没有数据确认功能的!
上面讲的知识点都是单线程的,实现不了并发,可是咱们这里又没有学习多线程,还好python给咱们提供了一个socketserver模块,该模块能够将单线程的套接字作成多线程的,实现并发,代码以下所示:
import socketserver class FtpServer(socketserver.BaseRequestHandler):#这个类不能随便定义,要继承socketserver下面的BaseRequestHandler def handle(self): #BaseRequestHandler处理通讯 print(self.request) #其实就是conn print(self.client_address) #其实addr while True:#相似于通讯循环! data = self.request.recv(1024) self.request.send(data.upper()) if __name__=="__main__": s= socketserver.ThreadingTCPServer(("127.0.0.1",8000),FtpServer) #处理链接 s.serve_forever() #相似与链接循环
from socket import * client = socket(AF_INET,SOCK_STREAM) client.connect(("127.0.0.1",8000)) while True: msg = input(">>:") client.send(msg.encode("utf-8")) data = client.recv(1024) print(data)
上述代码就相似于qq聊天,能够同时多个客户端去跟服务端通讯!
1.FTP是什么?FTP是文件传输协议
2.具体细节
import os import json import struct from socket import * class FtpClient: def __init__(self,ip,port,Family=AF_INET,Type=SOCK_STREAM): self.client=socket(AF_INET,SOCK_STREAM) self.client.connect((ip,port)) def run(self): while True: inp=input('>>: ').strip() if not cmd:continue cmd,attr=inp.split() #put /a/b/c/a.txt if hasattr(self,cmd): func=getattr(self,cmd) func(attr) def put(self,filepath): filename=os.path.basename(filepath) filesize=os.path.getsize(filepath) head_dict={ 'cmd':'put', 'filesize':filesize, 'filename':filename } head_json=json.dumps(head_dict) head_bytes=head_json.encode('utf-8') self.client.send(struct.pack('i',len(head_bytes))) self.client.send(head_bytes) with open(filepath,'rb') as f: for line in f: self.client.send(line) if __name__ == '__main__': f=FtpClient('127.0.0.1',8080) f.run()
import socket import struct import json import subprocess import os class MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 server_dir='file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn,self.client_addr=self.get_request() print('from client ',self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct:break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) except Exception: break def put(self,args): file_path=os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize=args['filesize'] recv_size=0 print('----->',file_path) with open(file_path,'wb') as f: while recv_size < filesize: recv_data=self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size+=len(recv_data) print('recvsize:%s filesize:%s' %(recv_size,filesize)) tcpserver1=MYTCPServer(('127.0.0.1',8080)) tcpserver1.run() #下列代码与本题无关 class MYUDPServer: """UDP server class.""" address_family = socket.AF_INET socket_type = socket.SOCK_DGRAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' def get_request(self): data, client_addr = self.socket.recvfrom(self.max_packet_size) return (data, self.socket), client_addr def server_activate(self): # No need to call listen() for UDP. pass def shutdown_request(self, request): # No need to shutdown anything. self.close_request(request) def close_request(self, request): # No need to close anything. pass
因此写FTP要去客户端有什么方法,服务端就有什么方法就OK!
socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在创建链接打开后,能够向本身文件写入内容供对方读取或者读取对方内容,通信结束时关闭文件。