前言:
平常我们会接触到两种架构,分别是:b/s架构与c/s架构
b/s架构(brownser浏览器/server服务器):通过浏览器与服务器进行数据交互,如:购物等网页数据,我们只需要通过浏览器就可以访问了。其实B/S架构也是CS的一种,只不过客户端是浏览器
c/s架构(client客户端/server服务端):通过网络向服务器发送数据请求,然后服务端回应客户端的请求数据。如:平常所使用的软件:QQ、VX都是客户端的形式。
CS架构,例如一个游戏客户端只能与之对应的服务端进行交互。而BS架构,通过浏览器可以访问任何在上面呈现的数据,它们来自不同的服务端
我们这里所学习的Socket就是基于c/s架构,我们可以写出一个服务端与和一个客户端数据交互。
什么是Socket?
Socket也可以称为:套接字,应用程序通常使用套接字通过网络,实现与另一台计算机进行通讯。
socket套接字:集合了OSI模型七层,传输层以下的所有功能
既然是网络通信,那么我们需要了解网络上的数据传输协议
TCP协议/UDP协议
TCP协议:可靠的数据传输协议,基于字节在网络上通信。当与另一台计算机进行TCP协议传输数据,需要经过3次握手建立连接。
syn表示发起连接请求,并发送一个序列号(上面使用了x代替)。服务端接收到了以后将用户的请求放入了半链接池,当服务端有空了再去半链接拿到请求,然后ack回应那个请求,需要在客户端发送的序列号基础上加上1,以确定没有错误,且也向客户端发起syn连接请求。然后客户端回应ack表示连接成功。
所谓洪水攻击就是:大量客户端向服务端发送连接请求,被服务端放于半链接池后,待服务端有空时,拿到半链接池的请求时,回应该请求,且向这个客户端也发送syn连接请求,那么此时客户端不会回应服务端。TCP协议会在多尝试几次向客户端发送syn,再次不回应则会抛弃这个请求,但此时也耽误了一些时间。而半链接池也是有上限的,当被这些恶意的请求所占用满后,合法的用户想要向服务端建立连接就会直接失败。
当连接成功后,就可以进行数据交互了。
TCP协议传输数据,如果目标未收到,会尝试发送多次。多次未果才会放弃发送
当数据传输完毕以后,就会进行4次挥手,断开连接。
为何要4次挥手?当客户端向服务端发送完数据后,请求断开连接。此时服务端被动断开了客户端与服务端之间的连接。但是服务端向客户端的数据还没有发送完毕呢。所以待服务端发送完数据以后,才会主动向客户端发送断开连接请求。
UDP协议:也可以称为不可靠的数据传输协议,向目标发送数据不在乎目标是否收到。
优势在于:在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序。
socket()
TCP协议通信
在Python中,我们需要通过socket模块来创建套接字。
语法格式:
socket.socket([family[, type[, proto]]])
- family:套接字家族 AF_UNIX(进程间通信)、AF_INET(TCP/UDP通信)
- type:SOCK_STREAM(数据流,一般是TCP协议)、SOCK_DGRAM(数据包,udp协议)
- proto:一般不填,默认为0
实例:我们需要创建两个文件,一个服务端、一个客户端
TCP需要提前建立连接才能进行通信,故而先写服务端
服务端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# server = socket.socket() 等同于上面的写法
# 绑定ip地址和端口,127.0.0.1代表回环地址,只能当前计算机访问
server.bind(('127.0.0.1',8080))
# 建立半链接池,允许存放5个请求
server.listen(5)
# 等待建立连接请求,会返回两个值,一个是连接状态,一个是连接的客户端IP与端口
conn,ip_addr = server.accept()
# 接收客户端传递的数据,只接收1024个字节数据
res = conn.recv(1024)
# 查看一下连接客户端的IP与端口
print(ip_addr)
# 接收到的是Bytes类型,需要解码
print(res.decode('utf-8'))
# 关闭与客户端的连接
conn.close()
# 关闭套接字
server.close()
客户端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# 连接服务端
client.connect(('127.0.0.1',8080))
# 向服务端发送数据,需要转换成Bytes类型发送
client.send('Hello'.encode('utf-8'))
# 套接字关闭
client.close()
先运行服务端,再运行客户端
执行结果
# 服务端
('127.0.0.1', 51767)
'Hello'
这里我们的服务端已经可以接收到客户端发送的数据了,那么此时只能输入和接收一次,我们稍加优化,可以持续发送数据。
服务端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# 绑定ip地址和端口,127.0.0.1代表回环地址,只能当前计算机访问
server.bind(('127.0.0.1',8080))
# 建立半链接池,允许存放5个请求
server.listen(5)
# 等待建立连接请求,会返回两个值,一个是连接会话,一个是连接的客户端IP与端口
conn,ip_addr = server.accept()
while True:
# 接收客户端传递的数据,不能超过1024字节
res = conn.recv(1024)
# 将客户端的数据接收到以后,转换成大写再编码后发送给客户端
conn.send(res.decode('utf-8').upper().encode('utf-8'))
# 注意:close不能放在这里面,因为在这里面的话,只能一次后就关闭了
# 关闭与客户端的连接
conn.close()
# 关闭套接字
server.close()
客户端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# 连接服务端
client.connect(('127.0.0.1',8080))
while True:
inp = input('>>>:').strip()
# 向服务端发送数据,需要转换成Bytes类型发送
client.send(inp.encode('utf-8'))
# 接收服务端回应给客户端的数据,不能超过1024字节
res = client.recv(1024)
print(res.decode('utf-8'))
# 套接字关闭
client.close()
查看一下执行结果
到了这里,我们已经实现了持续的数据发送,且接收到了服务端的回应。但有没有发现一个问题,如果客户端直接回车,发送一个空的内容过去,会是什么效果?
原因:
服务端会卡住,因为服务端在等待我们客户端发送数据,但是发送的是一个空内容,此时服务端就会阻塞在原地持续等待。
问题解决:在客户端输入后,判断是否为空就可以了
客户端
while True:
inp = input('>>>:').strip()
# 输入内容不能为空!
if len(inp) == 0:
continue
# 向服务端发送数据,需要转换成Bytes类型发送
client.send(inp.encode('utf-8'))
# 接收服务端回应给客户端的数据,不能超过1024字节
res = client.recv(1024)
print(res.decode('utf-8'))
执行结果
但是还会遇到一个问题,就是当客户端主动断开连接时,服务端也会因为没有连接而随之关闭,Window应该会直接产生报错,而Mac | Linux 则是无限循环,且recv接收到的数据为0
。
Mac | Linux 会产生的效果
我们需要继续优化的是:当检测到客户端断开连接时,让服务端重新进入等待连接请求的状态。
服务端
while True:
# 等待建立连接请求,会返回两个值,一个是连接状态,一个是连接的客户端IP与端口
conn,ip_addr = server.accept()
while True:
# 接收客户端传递的数据
res = conn.recv(1024)
# 当res为0,说明客户端断开了连接,我们中断当前while
# 因为当前的conn保存的是断开连接客户的信息,既然它走了,就没必要保留了
if len(res) == 0:
break
# 将客户端的数据接收到以后,转换成大写编码后,再发送给客户端
conn.send(res.decode('utf-8').upper().encode('utf-8'))
# 接收一个新的连接
conn.close()
上面的优化针对的是客户端断开连接,服务端不会报错的系统
windows可以使用以下优化:
while True:
# 等待建立连接请求,会返回两个值,一个是连接状态,一个是连接的客户端IP与端口
conn,ip_addr = server.accept()
while True:
try:
# 接收客户端传递的数据
res = conn.recv(1024)
# 将客户端的数据接收到以后,转换成大写编码后,再发送给客户端
conn.send(res.decode('utf-8').upper().encode('utf-8'))
except Exception:
break
# 关闭当前的,接收一个新的连接
conn.close()
有时候我们在重启服务端时,会出现端口被占用的情况。这是因为虽然连接已经关闭,但是端口还没来得及释放。
两种解决方案:
-
更换端口。
-
bind绑定IP之前添加一个参数,来达到端口复用的目的。
import os
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定ip地址和端口,127.0.0.1代表回环地址,只能当前计算机访问
server.bind(('127.0.0.1',8080))
UDP协议通信
udp是无连接的,所以发送数据前不需要先建立连接。
我们可以使用多个客户端与服务端进行数据发送,服务端可以逐个回复
udp发送数据采用的是数据报模式,数据会一次性全部发过去,如果未接收到也不会保存,安全性没有保障,但传输速率较快。
服务端
import socket
# 此时后面数据的传输就要选择SOCK_DGRAM,代表UDP形式传输
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 绑定ip与端口
server.bind(('127.0.0.1',8000))
while True:
# 接收客户端发送的数据,以及发送者的ip以及端口(UDP不需要建立连接)
data,addr = server.recvfrom(1024)
print(f'来自{
addr}发送的一条信息:',data.decode('utf-8'))
send_data = input('回复信息:')
# 向发送者回复数据
server.sendto(send_data.encode('utf-8'),addr)
客户端
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server_ip = ('127.0.0.1',8000)
# 不需要与服务端建立连接
while True:
send_data = input('输入发送的数据:').strip()
# 不需要判断是否为空,因为udp每次发送都不只是单纯的数据,还有ip和端口信息
# 直接向写好的ip和端口发送数据
client.sendto(send_data.encode('utf-8'),server_ip)
# 接收它返回的数据
receive_data,addr = client.recvfrom(1024)
print(f'来自{
addr}的回信:{
receive_data.decode("utf-8")}')
执行结果
我们可以使用服务端回复多个客户端信息
也可以使用服务端只接收客户端信息
服务端
import socket
# 此时后面数据的传输就要选择SOCK_DGRAM,代表UDP形式传输
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8000))
print('正在接收数据中....')
while True:
# 接收客户端发送的数据,以及发送者的ip以及端口(UDP不需要建立连接)
data,addr = server.recvfrom(1024)
print(f'来自{
addr}发送的一条信息:',data.decode('utf-8'))
客户端
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server_ip = ('127.0.0.1',8000)
while True:
send_data = input('输入发送的数据:').strip()
client.sendto(send_data.encode('utf-8'),server_ip)
执行结果
配合subprocess模块实现远程执行命令
这里subprocess模块就不过多介绍了,详细可了解:Python 常用内置模块
主要学习它与套接字的搭配使用,通过客户端
执行服务端
的系统命令
下面使用的是tcp协议进行连接的,当然使用udp或许会更简单些,但这里需要解决一个tcp协议发送数据的遗留问题
服务端
import socket
import subprocess
# 创建套接字
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # SOCK_STREAM表示Tcp协议传输数据
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 重用端口
# 绑定地址
server.bind(('127.0.0.1',8082))
# 监听状态,允许半链接池存在5个连接
server.listen(5)
# 等待建立连接、有建立连接请求过来后,开始Tcp3次握手协议,建立成功
while True:
conn,ipadd = server.accept()
while True:
# 接收建立连接的用户发送数据
cmds = conn.recv(1024)
if len(cmds) == 0:
break
cmds = subprocess.Popen(cmds.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout = cmds.stdout.read() # 获取执行的正确结果
stderr = cmds.stderr.read() # 获取执行的错误结果
conn.send(stdout+stderr) # 发送给客户端
# 关闭建立的链接
conn.close()
# 关闭套接字
server.close()
客户端
import socket
# 创建套接字
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 连接服务端
client.connect(('127.0.0.1',8082))
while True:
cmds = input('>>>:').strip()
if len(cmds) <= 0:
print('输入内容不能为空!')
continue
# 向服务端发送数据
client.send(cmds.encode('utf-8'))
# 接收服务端返回数据
data = client.recv(1024)
print(data.decode('utf-8'))
# 关闭套接字
client.close()
执行结果,使用客户端输入命令:
虽然实现了效果,但这里已经出现了问题,那就是我们客户度只接收1024个字节数据,那么如果服务端发送过来的数据超过1024字节。那该怎么办?此时就需要了解一下tcp协议产生的粘包问题,以及如何解决。
粘包问题
粘包指的是:数据全部粘在一起,如果一次性未取完,下一次接着上一次未取完的数据部分接着取
tcp:recv(1024)
udp:recvfrom(1024)
粘包是由tcp产生的,由于tcp是数据流的形式接收,所有发送数据都会粘着一起,如果接收数据超过我们指定的,则会出现粘包现象,那么此时这个数据还会保存在缓冲区,这里也就是recv
内,待我们下次再次recv
时,取到的就会是上次没有取完的数据。
udp:如果接收数据超过我们指定的,则放弃那些数据。下次再次接收又是全新的数据,所以不会出现粘包现象。
tcp粘包实例:
服务端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定ip地址和端口,127.0.0.1代表回环地址,只能当前计算机访问
server.bind(('127.0.0.1',8080))
# 建立半链接池,允许存放5个请求
server.listen(5)
while True:
# 等待建立连接请求,会返回两个值,一个是连接状态,一个是连接的客户端IP与端口
conn,ip_addr = server.accept()
while True:
# 接收客户端传递的数据
res = conn.recv(1024)
if len(res) == 0:
break
# 将客户端的数据接收到以后,转换成大写编码后,再发送给客户端
conn.send(res.decode('utf-8').upper().encode('utf-8'))
# 关闭与客户端的连接
conn.close()
# 关闭套接字
server.close()
客户端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# 连接服务端
client.connect(('127.0.0.1',8080))
while True:
inp = input('>>>:').strip()
if len(inp) == 0:
continue
# 向服务端发送数据,需要转换成Bytes类型发送
client.send(inp.encode('utf-8'))
# 将接收字节数量调整为10个
res = client.recv(10)
print(res.decode('utf-8'))
# 套接字关闭
client.close()
执行结果
可以看到由于上一次未取完的数据,导致下一次取的时候会接着上一次的继续取数据。
可能会有疑问,把接收字节的长度调高一点不就行了。但忽略的是:接收字节也有上限,可能设置超过几万字节就会报错了,所以这是一种治标不治本的方法。
我们要从真正意义上的解决粘包问题。首先需要拿到整个数据的长度,再通过循环不断接收数据,直到取完本次数据为止。
那么如何拿到数据长度呢?send两次发送过去?
# 分2次发送,但结果还是存在一个缓冲区内
send('2')
send('ab')
lens = recv(1) # 需要固定知道第一次send过来的数据长度为多少
由于数据是不定长的,我们这时无法手动指定接收到数据长度。所以我们需要一种能稳定接收到数据长度的方法。
Python提供给了我们一种模块,可以很好利用在这个场景里面。那就是对数据进行封包,不管数据的内容是什么,我们只需要定义一个固定的头部长度作为标识,那么只要这个头部标识对上了,就可以把这个包解开,获取我们想要的数据
import struct
# 将整体数据分为两部分:头部,数据
# 头部作为一个标识(固定长度)
header = struct.pack('i',409632)
# 其中i就是标识,占4个字节
print(len(header))
# 打印结果就是4个字节
# 4
# 我们通过头部解header这个数据包(如果头部对上了),就可以得到里面的数据
headers = struct.unpack('i',header)
print(headers[0]) # 返回的是一个元组
# 409632
我们可以使用这种形式来把数据封装后发送,已经知道封装后的数据只占4个字节,所以解决了我们上面的问题
我们直接修改上面subprocess案例里面的代码
服务端
import socket
import subprocess
import struct
# 创建套接字
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM表示Tcp协议传输数据
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用端口
# 绑定地址
server.bind(('127.0.0.1', 8082))
# 监听状态,允许半链接池存在5个连接
server.listen(5)
# 等待建立连接、有建立连接请求过来后,开始Tcp3次握手协议,建立成功
while True:
conn, ipadd = server.accept()
while True:
# 接收建立连接的用户发送数据
cmds = conn.recv(1024)
if len(cmds) == 0:
break
cmds = subprocess.Popen(cmds.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = cmds.stdout.read() # 获取执行的正确结果
stderr = cmds.stderr.read() # 获取执行的错误结果
# 将数据使用固定长度,封装一下
header = struct.pack('i', len(stdout) + len(stderr))
conn.send(header)
conn.send(stdout + stderr) # 发送给客户端
# 关闭建立的链接
conn.close()
# 关闭套接字
server.close()
客户端
import socket
import struct
# 创建套接字
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务端
client.connect(('127.0.0.1', 8082))
while True:
cmds = input('>>>:').strip()
if len(cmds) <= 0:
print('输入内容不能为空!')
continue
# 向服务端发送数据
client.send(cmds.encode('utf-8'))
# 接收到这个固定长度的数据包
headers = client.recv(4)
# 解析这个数据包,获取里面的内容,也就是数据长度
data_len = struct.unpack('i',headers)[0]
count = 0
# 不断循环获取缓冲区里面的数据,直到获取完毕为止
while count < data_len:
data = client.recv(1024) # 每次都获取1024个字节
count += len(data)
print(data.decode('utf-8'))
# 关闭套接字
client.close()
查看一下代码效果
与上面进行对比,已经达到了我们预期的效果
来看一下使用udp的话,发送字节超过接收字节的效果是什么样的
服务端
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
while True:
data,addr = server.recvfrom(5) # 只接受5个字节的数据
server.sendto(data,addr) # 将接收到的字节发送给客户端
客户端
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server_ip_port = ('127.0.0.1',8080)
while True:
data = input('>>>>:').strip()
client.sendto(data.encode('utf-8'),server_ip_port)
print(client.recvfrom(1024)[0].decode('utf-8'))
执行效果
可以看到,当定义了只接收5个字节的时候,数据超出的将被丢弃,下一次再接收的又是全新的数据。而windows超过接收字节以后则会报错,也就是说,以上的案例用windows执行会报错。
socketserver模块
我们使用udp,开启一个服务端,多个客户端可以同时向它发送数据,至少在表面上可以实现了并发的效果。但是如果真的有成千上万的同时发送数据,那么就会明显看到效率降低。
在上面我们使用的tcp连接
一次只能与一个客户端建立连接,而这里我们了解到一种新的模块,可以同时与多个客户端进行连接,实现并发效果。
服务端
import socketserver
class MyRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
data = self.request.recv(1024) # 最大接收的字节数
if len(data) == 0:
break
self.request.send(data.upper())
except Exception:
break
self.request.close()
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandler, bind_and_activate=True)
server.serve_forever()
可以建立多个客户端
客户端
import socket
# 创建套接字对象,AF_INET基于TCP/UDP通信,SOCK_STREAM以数据流的形式传输数据,这里就可以确定是TCP了
client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
# 连接服务端
client.connect(('127.0.0.1',8080))
while True:
inp = input('>>>:').strip()
if len(inp) == 0:
continue
# 向服务端发送数据,需要转换成Bytes类型发送
client.send(inp.encode('utf-8'))
res = client.recv(1024)
print(res.decode('utf-8'))
# 套接字关闭
client.close()
# 套接字关闭
client.close()
执行结果
上面的服务端是通过多线程实现,多个客户端可以与服务端进行连接,并同时发送数据,且它们都是基于tcp协议
。
以及这个模块也提供了UDP通信
实现并发效果
服务端
import socketserver
class MyRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
data,server = self.request
server.sendto(data,self.client_address)
server = socketserver.ThreadingUDPServer(('127.0.0.1',8080),MyRequestHandler,bind_and_activate=True)
server.serve_forever()
客户端
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server_ip = ('127.0.0.1',8080)
while True:
send_data = input('输入发送的数据:').strip()
client.sendto(send_data.encode('utf-8'),server_ip)
data,addr = client.recvfrom(1024)
print(data.decode('utf-8'))
以上就是笔者所了解的socket模块相关内容,如有疑问,请评论区留言!
各位读者大大,see you next time~
技术小白记录学习过程,有错误或不解的地方请指出,如果这篇文章对你有所帮助请
点赞 收藏+关注
子夜期待您的关注,谢谢支持!
今天的文章Python socket网络编程分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/28225.html