Day10 Python网络编程 Socket编程

一、客户端/服务器架构

1.C/S架构,包括:

  1.硬件C/S架构(打印机)

  2.软件C/S架构(web服务)【QQ,SSH,MySQL,FTP】

2.C/S架构与socket的关系:

  我们学习socket就是为了完成C/S架构的开发

3.预备知识:

须知一个完整的计算机系统是由硬件和软件构成,软件又分为:操作系统和应用软件。

互联网之间的通信都必须遵循统一的规范,这个统一的规范就是协议,就好比全世界人通信的标准是英语,互联网协议就是计算机界的英语,所有的计算机都就可以按照统一的标准去收发信息从而完成通信了!

4.互联网世界中的两套协议:

     1.学术界:OSI七层模型

     2.工业界:TCP四层模型

两者对比:

Day10 Python网络编程 Socket编程

我们实际生产中实际上公认的标准就是使用的其实就是TCP四层模型! 

工作在上述四层的协议分别为:

Day10 Python网络编程 Socket编程

数据包的传输过程实际上是:

Day10 Python网络编程 Socket编程

普及一点知识:

TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。关于TCP/IP和HTTP协议的关系,网络有一段比较容易理解的介绍:“我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。

TCP/IP协议栈主要分为四层:应用层、传输层、网络层[网络互连层]、数据链路层[主机到网络层],每层都有相应的协议,如下图:

Day10 Python网络编程 Socket编程

在网络中,一帧以太网数据包的格式:

Day10 Python网络编程 Socket编程

二.Socket网络编程:

我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程,操作系统有0-65535个端口,每个端口都可以独立对外提供服务。。所以socket本质上就是在2台网络互通的电脑之间,架设一个通道,两台电脑通过这个通道来实现数据的互相传递,也就是说:建立一个socket必须至少有2端, 一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求, 连接建立之后,双方可以互发数据。 比如:【QQ,微信】 

能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信,从而简化我们的编程!

如下所示:我们更加形象的给大家展示一下socket抽象层!

Day10 Python网络编程 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连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

HTTP与Socket连接的区别

1.基于TCP的Socket网络编程

建立一个socket必须至少有2端, 一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求, 连接建立之后,双方可以互发数据。

Day10 Python网络编程 Socket编程

各位,我们知道对于所有的服务端和客户端架构的连接而言,都是先启动服务端,然后客户端发送请求,服务端处理客户端发送的请求,然后将结果返回给客户端,然后再继续!

所以这里我们先讲服务端和客户端的通信流程,如上图:

服务器端先初始化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()掉,否则会报出:通常每个套接字地址(协议/网络地址/端口)只允许使用一次的错误;

socket方法说明
  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 不会如书中所说的全部拒绝连接,有些会延时连接!

connect、listen、accept方法说明

演变一:一次连接,交流多次[通信循环]

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解码
且只能从管道里读一次结果

subprocess注意点

2.粘包

注意:上面有个问题,就是当输入ipconfig命令的时候显示没问题,但是一旦接着输入下一个命令的时候,那么就会出现显示的不是本条命令的结果,而是显示上一条命令的结果,这样程序就乱了,这就是粘包的现象!

 1. 什么是粘包?

须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来

首先需要掌握一个socket收发消息的原理:

Day10 Python网络编程 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的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;

2.服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区中全部取走,下次在取数据的时候可能就会出现取出的是上一个数据包中的数据的情况,造成粘包现象(确切来讲,对于基于TCP协议的应用,不应用包来描述,而应用 流来描述),个人认为服务器接收端产生的粘包应该与linux内核处理socket的方式 select轮询机制的线性扫描频度无关。
 
再说UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。
 
[TCP协议的内部优化机制]:造成粘包的第一种方式我们如果在发送的时候,不让它将小数据连续发送【等一会儿再发】,那么粘包问题就可以解决了,如下演示:
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()

服务端代码

上述代码在发送数据之前我们先把数据的长度发送过去,这样问题就解决了,但是这里的问题是,这个长度的大小是多少呢?

Day10 Python网络编程 Socket编程

如下所示:当数据变化的时候,数据的长度也是变化的,所以数据的长度是不固定的!

那有没有什么方法能把一串数字打包成一个二进制,并且长度是固定的,这个问题就解决了,有这么一个模块【struct模块】

那么python中正好提供了一个struct模块,它可以将一个数字编码成二进制,并且这串二进制的长度是固定的,这个问题就解决了!

Day10 Python网络编程 Socket编程

上述的i,表示将后面的数据打包成4个字节;

接收端在拿到数据之后,只需要解码就OK,如下所示:

Day10 Python网络编程 Socket编程

解码之后拿到的是一个元组,我们取出第一个值就是我们要接收的数据长度,如下所示:

Day10 Python网络编程 Socket编程

而且数值是整形的!

Day10 Python网络编程 Socket编程

 这时候客户端实际上也就不能直接接收数据了,它需要在接收数据之前先将数据的长度接收了,长度就是固定的4个字节:
所以客户端代码是:
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()

引入struct模块之后的客户端代码

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之后的server端代码

这样发送和接收数据就没问题了,就是在发送数据之前我们先把数据的长度发送过去!

struct模块详解:

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes

>>> struct.pack('i',1111111111111)

。。。。。。。。。

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

Day10 Python网络编程 Socket编程

当然,不用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本质就类似于键值对形式的字符串】,然后我们还需要进一步编码,但是此时服务器端的代码就需要先报头的长度给客户端,再发报头头信息给客户端,再发报文信息给客户端!

Day10 Python网络编程 Socket编程

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的字符引用。

3.基于UDP的套接字

#_*_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)

基于UDP的服务端代码

#_*_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的客户端代码

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的三次握手和四次挥手

Day10 Python网络编程 Socket编程

TCP之所以是数据安全的,是因为在TCP建立连接之后,每次都是需要进行数据确认的,但是UDP在数据传输的时候,没有数据确认这个环节,只管着发,不管对方是否能够接收到,所以说UDP是数据不安全的!

Tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头!

UDP不可靠的连接,应用场景在于QQ,TCP与UDP的区别主要是在建立连接之后,TCP在数据传输的时候是有数据确认功能的,而UDP是没有数据确认功能的!

4.用SocketServer实现高并发

上面讲的知识点都是单线程的,实现不了并发,但是我们这里又没有学习多线程,还好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() #类似与连接循环

socketserver服务端代码

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)

socketserver客户端代码

上述代码就类似于qq聊天,可以同时多个客户端去跟服务端通信!

5.作业:多用户在线的FTP程序

1.FTP是什么?FTP是文件传输协议

Day10 Python网络编程 Socket编程

2.具体细节

Day10 Python网络编程 Socket编程

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()

FTP客户端代码

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服务端代码

所以写FTP要去客户端有什么方法,服务端就有什么方法就OK!

socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

上一篇:python网络编程——socket基础篇


下一篇:Mysql --学习:大量数据快速导入导出