本文为了说明例子,用中文作为变量写在了程序里面,一般编程最好不要那么写
本文目录
- 概念
- 基本TCP套接字编程
- 通信循环
- 半链接池
- 链接循环
- udp协议
- 基于TCP协议实现远程执行客户端请求
概念
Socket是进程通讯的一种方式,即调用这个网络库的一些API函数实现分布在不同主机的相关进程之间的数据交换。
在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read→关闭close”模式来操作。
简单理解就是Socket就是该模式的一个实现:即socket是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
基本TCP套接字编程
流程图
基于 TCP 的套接字编程的所有客户端和服务器端都是从调用socket 开始,它返回一个套接字描述符。客户端随后调用connect 函数,服务器端则调用 bind、listen 和accept 函数。套接字通常使用标准的close 函数关闭,但是也可以使用 shutdown 函数关闭套接字。
服务端
import socket
# 1、获取通信手段
通信手段=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议→tcp协议
# 2、绑定通信手段ID
通信手段.bind(('127.0.0.1',8081)) # 0-65535, 1024以前的端口都被系统保留使用,默认端口为80端口
# 3、开机
通信手段.listen(5) # 5指的是半连接池的大小
print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080))
# 4、等待通信连接请求:拿到通信连接conn
conn,client_addr=通信手段.accept()
# print(conn)
print("通信端的ip和端口:",client_addr)
# 5、通信:接\发消息
data=conn.recv(1024) # 设置最大接收的数据量为1024Bytes,收到的是bytes类型,这里有个假设,假设所有数据量都小于1024bytes
print("通信端发来的消息:",data.decode('utf-8'))
conn.send(data.upper())
# 6、关闭通信连接conn(必须有的回收资源的操作)
conn.close()
# 7、关机(可选操作)
通信手段.close()
客户端
import socket
#1、获取通信手段
通信手段=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议→tcp协议
#2、连接服务端ID
通信手段.connect(('127.0.0.1',8081))#服务端连接客户端的,这里写服务端的端口,IP+端口联合锁定,构成唯一标识,IP重复不是问题
#3、通信
import time
time.sleep(10)
通信手段.send('这里是通信端,开始发送信息'.encode('utf-8'))
data=通信手段.recv(1024)
print(data.decode('utf-8'))
#4、关闭连接(必选的回收资源的操作)
通信手段.close()
通信循环
简单来说,就是
远程操控:客户端输入一个命令A 通过网络传输 传输给服务器,服务器后端运行此命令得到一个结果B
将结果B再通过网络传输返回给客户
在上面的代码的基础上加入通信循环
服务端
import socket
# 1、获取通信手段
通信手段=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议
# 2、绑定通信手段ID
通信手段.bind(('127.0.0.1',8083)) # 0-65535, 1024以前的都被系统保留使用
# 3、开机
通信手段.listen(5) # 5指的是半连接池的大小
print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080))
# 4、等待通信连接请求:拿到通信连接conn
conn,client_addr=通信手段.accept()
# 5、通信:收\发消息
while True:
try:
data=conn.recv(1024) # 最大接收的数据量为1024Bytes,收到的是bytes类型
if len(data) == 0:
#如果是windows,对方强行关闭连接,会抛出异常
#如果是linux,不会抛出异常,会死循环收到空的数据包
break
print("客户端发来的消息:",data.decode('utf-8'))
conn.send(data.upper())
except Exception:
# 针对windows系统
break
# 6、关闭通信连接conn(必选的回收资源的操作)
conn.close()
# 7、关机(可选操作)
通信手段.close()
客户端
import socket
#1、获取通信手段
通信手段=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议→tcp协议
#2、连接服务端ID
通信手段.connect(('127.0.0.1',8083))#服务端连接客户端的,这里写服务端的端口,IP+端口联合锁定,构成唯一标识,IP重复不是问题
#3、通信
while True:
msg=input("输入要发送的消息>>>: ").strip() #msg=''
if len(msg) == 0:continue
通信手段.send(msg.encode('utf-8'))
print('======?')
data=通信手段.recv(1024)
print(data.decode('utf-8'))
#4、关闭连接(必选的回收资源的操作)
通信手段.close()
服务端应该满足的特点:
- 一直提供服务
- 并发地提供服务
出于这样的考虑,我们就要引入链接循环与半连接池这两个概念
半链接池
概念:当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接
半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光
产生半连接的两种情况:
- 客户端无法返回ACK信息
- 服务器来不及处理客户端的连接请求
注意,限制的是同一时刻的请求数,而非连接数
链接循环
可以启动多个客户端,但是只有一个客户端是处于连接状态,其余部分在半连接池等待连接,等待的数量不能超过半连接池的最大监听数量
服务端
服务端要完成的任务:
- 循环地从板链接池中取出链接请求与其建立双向链接,获取链接对象
- 得到链接对象,对其进行通信循环
import socket
# 1、获取通信手段
通信手段=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议→tcp协议
# 2、绑定通信手段ID
通信手段.bind(('127.0.0.1',8080)) # 0-65535, 1024以前的都被系统保留使用
# 3、开机
通信手段.listen(5) # 5指的是半连接池的大小
print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080))
# 4、等待通信连接请求:拿到通信连接conn
# 加上链接循环
while True:
conn,client_addr=通信手段.accept()
# 5、通信:收\发消息
while True:
try:
data=conn.recv(1024) # 最大接收的数据量为1024Bytes,收到的是bytes类型
if len(data) == 0:
#如果是windows,对方强行关闭连接,会抛出异常
#如果是linux,不会抛出异常,会死循环收到空的数据包
break
print("客户端发来的消息:",data.decode('utf-8'))
conn.send(data.upper())
except Exception:
# 针对windows系统
break
# 6、关闭通信连接conn(必选的回收资源的操作)
conn.close()
# 7、关机(可选操作)
通信手段.close()
客户端
import socket
#1、获取通信手段
通信手段=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议→tcp协议
#2、连接服务端ID
通信手段.connect(('127.0.0.1',8083))#服务端连接客户端的,这里写服务端的端口,IP+端口联合锁定,构成唯一标识,IP重复不是问题
#3、通信
while True:
msg=input("输入要发送的消息>>>: ").strip() #msg=''
if len(msg) == 0:continue
通信手段.send(msg.encode('utf-8'))
print('======?')
data=通信手段.recv(1024)
print(data.decode('utf-8'))
#4、关闭连接(必选的回收资源的操作)
通信手段.close()
udp协议
UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,IETF RFC 768 是UDP的正式规范。UDP在IP报文的协议号是17。
UDP协议与TCP协议一样用于处理数据包,在OSI模型中,两者都位于传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用UDP协议。UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但即使在今天UDP仍然不失为一项非常实用和可行的网络传输层协议。
使用范例
服务端
import socket
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #udp协议
server.bind(('127.0.0.1',8081))
while True:
data,client_addr=server.recvfrom(1024)
server.sendto(data.upper(),client_addr)
server.close()
客户端
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #tcp协议
while True:
msg=input('>>>: ').strip()
client.sendto(msg.encode('utf-8'),('127.0.0.1',8081))
res=client.recvfrom(1024)
print(res)
client.close()
基于TCP协议实现远程执行客户端请求
粘包问题
多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
原因
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
接收方引起的粘包
是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
发送方导致的粘包
粘包并不是 TCP 协议造成的,它的出现是因为应用层协议设计者对 TCP 协议的错误理解,忽略了 TCP 协议的定义并且缺乏设计应用层协议的经验。我们将从 TCP 协议以及应用层协议出发,分析我们经常提到的 TCP 协议中的粘包是如何发生的:
TCP 协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据;
应用层协议的没有定义消息的边界导致数据的接收方无法拼接数据;
TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议,应用层交给 TCP 协议的数据并不会以消息为单位向目的主机传输,这些数据在某些情况下会被组合成一个数据段发送给目标的主机。
Nagle 算法是一种通过减少数据包的方式提高 TCP 传输性能的算法。因为网络 带宽有限,它不会将小的数据块直接发送到目的主机,而是会在本地缓冲区中等待更多待发送的数据,这种批量发送数据的策略虽然会影响实时性和网络延迟,但是能够降低网络拥堵的可能性并减少额外开销。
当应用层协议通过 TCP 协议传输数据时,实际上待发送的数据先被写入了 TCP 协议的缓冲区,如果用户开启了 Nagle 算法,那么 TCP 协议可能不会立刻发送写入的数据,它会等待缓冲区中数据超过最大数据段(MSS)或者上一个数据段被 ACK 时才会发送缓冲区中的数据
简单来说,就是
- tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
- 接收数据时没收干净,有残留,就会下一轮的数据混淆在一起
那么对应的,我们的方案自然就是:每次都接收干净,不要任何数据残留
思路:
1 拿到数据的总大小total
2 设置初始变量recv=0,循环接收,每接收一次,recv+=接收的长度
3 直到recv=total,结束循环
于是我们要把服务端这样写
import subprocess
import struct
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
server.bind(('127.0.0.1',8080))
server.listen(5)#设置链接池的请求个数
#取出链接请求与其建立双向链接,获取链接对象
while True:
conn,client_addr=server.accept()
#获取链接对象,对其进行通信循环
while True:
try:
指令=conn.recv(2048)#设置单次请求最大大小
if len(指令) == 0:break
操作的对象=subprocess.Popen(指令.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout_res=操作的对象.stdout.read()
stderr_res=操作的对象.stderr.read()
total_size=len(stdout_res)+len(stderr_res)
# 先发头部信息(固定长度的bytes):对数据描述信息
# int->固定长度的bytes
header=struct.pack('i',total_size)
conn.send(header)
# 再发真实的数据
conn.send(stdout_res)
conn.send(stderr_res)
except Exception:
break
conn.close()
客户端
import struct
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
指令=input('请键入指令>>:').strip()
if len(指令) == 0:continue
client.send(指令.encode('utf-8'))
# 解决粘包问题思路:
#先收固定长度的头:解析数据的描述信息,包括数据的总大小
header=client.recv(4)
数据总大小=struct.unpack('i',header)[0]
#根据解析出的描述信息,接收真实的数据
# 接收=0,循环接收,每接收一次,接收+=接收的长度
# 直到接收=数据总大小,结束循环
接收 = 0
while 接收 < 数据总大小:
接收_data=client.接收(1024)
接收+=len(接收_data)
print(接收_data.decode('utf-8'),end='')
else:
print()