今日内容概要
- TCP与UDP协议
- socket套接字编程
- 半连接池
- TCP黏包问题及解决思路
TCP与UDP协议
1.规定了数据传输所遵循的规则;
数据传输能够遵循的协议有很多,TCP和UDP是常见的两个。
2.TCP协议:
(1)三次握手
建立双向通道
ps:洪水攻击——同时让大量的客户朝服务端发送建立TCP连接的请求。
(2)四次挥手
断开双向通道
中间的两步不能合并(需要有检查的时间)
(3)基于TCP传输数据非常的安全,因为有双向通道这句话是否正确?
答:不是因为有双向通道传输数据才安全,基于TCP传输数据,数据不容易丢失,其原因在于二次确认机制。
每次发送数据都需要返回确认消息,否则在一定的时间会反复发送。
3.UDP协议
基于UDP协议发送数据,没有任何的通道也没有任何的机制;
UDP发送数据没有TCP安全(没有二次确认机制)
socket套接字
1.套接字
基于文件类型的套接字家族。
套接字家族的名字:AF_UNIX
基于网络类型的套接字家族。
套接字家族的名字:AF_INEF
2.实例展示之简单代码
(1)服务端
import socket # 创建一个socket对象 server = socket.socket() # 括号内什么都不写,默认就是基于网络的TCP套接字 # 绑定一个固定的地址(ip\port) server.bind(('127.0.0.1', 8088)) # 127.0.0.1本地回环地址(只允许自己的机器访问) # 半连接池 server.listen(5) sock,address = server.accept() print(sock,address) # sock是双向通道,address是客户端地址 # 数据交互 sock.send(b'hello word') # 朝客户发送数据 data = sock.recv(1024) # 接收客户端发送的数据 1024bytes print(data) # 断开连接 sock.close() # 断链接 server.close() # 关机 D:\Python36\python36.exe "E:/pythonProject/Day37/01 socket模块/socket模块之服务端.py" <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 49827)> ('127.0.0.1', 49827) b'hello python' Process finished with exit code 0
(2)客户端
import socket # 产生一个socker对象 client = socket.socket() # 连接服务器(拼接服务端的ip和port) client.connect(('127.0.0.1',8088)) # 数据交互 data = client.recv(1024) # 接收服务端发送的数据 print(data) client.send(b'hello python') # 朝服务端发送数据 # 关闭 client.close() D:\Python36\python36.exe "E:/pythonProject/Day37/01 socket模块/socket模块之客户端.py" b'hello word' Process finished with exit code 0
3.实例展示之代码优化处理
(1)send与recv
客户端与服务端不能同时执行同一个
有一个接收另外一个就是发送;
有一个发送另外一个就是接收。
不能实现同时接收或者同时发送。
(2)
消息自定义
input获取用户数据即可。
(3)循环通信
给数据交互环节添加循环即可。
(4)服务端能够持续提供服务
要不会因为客户端断开而报错:
解决方法:异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待
(5)消息不可以为空
判断是否为空 如果是则重新输入(主要针对客户端)
(6)服务端频繁重启可能会报端口被占用的错(主要针对mac电脑)
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
(7)客户端异常退出会发送空消息(针对mac linux)
针对接收的消息加判断处理即可
(8)代码展示
(1)服务端import socket from socket import SOL_SOCKET, SO_REUSEADDR # 1.创建一个socket对象 server = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字 # 2.绑定一个固定的地址(ip\port) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 就是它,在bind前加 server.bind(('127.0.0.1', 8088)) # 127.0.0.1本地回环地址(只允许自己的机器访问) # 3.半连接池(暂且忽略) server.listen(5) # 4.开业 等待接客 while True: sock, address = server.accept() print(sock, address) # sock是双向通道 address是客户端地址 # 5.数据交互 while True: try: msg = input('请输入发送给客户端的消息>>>:').strip() if len(msg) == 0: continue sock.send(msg.encode('utf8')) # 朝客户端发送数据 data = sock.recv(1024) # 接收客户端发送的数据 1024bytes if len(data) == 0: # break print(data.decode('utf8')) except ConnectionResetError: sock.close() break
(2)客户端
import socket # 1.产生一个socket对象 client = socket.socket() # 2.连接服务端(拼接服务端的ip和port) client.connect(('127.0.0.1', 8088)) # 3.数据交互 while True: data = client.recv(1024) # 接收服务端发送的数据 print(data.decode('utf8')) msg = input('请输入发送给客户端的消息>>>:').strip() if len(msg) == 0: msg = '手抖了一下 暂无消息' client.send(msg.encode('utf8')) # 朝服务端发送数据
半连接池
server.listen(5)
主要是为了缓冲,避免太多的无效等待!!!
黏包问题
1.问题
在前面我们知道tcp容易产生黏包的问题,而udp不会产生黏包的问题,但是会产生丢包的问题,tcp应用的场景很多所以黏包问题必须要解决。
(1)TCP协议特性:
流程协议:所有的数据类似于水流,连接在一起;
会将数据量比较小并且时间间隔比较短的数据整合到一起发送
并且还会受制于recv括号内的数字大小。
(2)recv
我们不知道即将要接收的数据量多大 如果知道的话不会产生也不会产生黏包
2.解决方法一:
1.解决黏包问题第一种方法,我们知道黏包问题是由于tcp的优化算法将两个不太大的数据包合并了一起发送的,这种情况一般出现在连续使用几个send()出现的,所以我们如果知道要发送的数据有多大我们就可以设置接收的大小,这样就可以刚好能把所有的数据接收完。下面是具体的步骤细节见代码
server端代码
import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() while True: cmd = input('>>>') conn.send(bytes(cmd,encoding='utf-8')) num = conn.recv(1024).decode('utf-8') #接收client端计算好的数据长度 conn.send(bytes('ok',encoding='utf-8')) #发送一个确认防止发送num的时候跟后面的send内容合并了 ret = conn.recv(num) print(ret.decode('gbk')) conn.close() sk.close()
client端代码
import socket import struct import subprocess sk = socket.socket() sk.connect(('127.0.0.1',8080)) while True: cmd = sk.recv(1024).decode('utf-8') ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) std_out = ret.stdout.read() std_err = ret.stderr.read() sk.send(bytes(str(len(std_err)+len(std_out)),encoding='utf-8')) #上面计算字符串的长度发送给server端在接收的时候刚好接收那么长的数据 sk.recv(1024) #ok 这一步主要的目的是为了将num的发送跟后面的send分割开防止黏包现象 sk.send(std_out) sk.send(std_err) sk.close()
3.解决方法二:
用struct模块解决黏包现象
server端代码
#tcp黏包现象的解决 struct import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen() conn,addr = sk.accept() while True: cmd = input('>>>') conn.send(bytes(cmd,encoding='utf-8')) num = conn.recv(1024) #接收数据 num = struct.unpack('i',num)[0]#进行解包,解包的结果是一个元组类型取第一个数据 ret = conn.recv(num) print(ret.decode('gbk')) conn.close() sk.close()
client端代码
import socket import struct import subprocess sk = socket.socket() sk.connect(('127.0.0.1',8080)) while True: cmd = sk.recv(1024).decode('utf-8') ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) std_out = ret.stdout.read() std_err = ret.stderr.read() num = len(std_err) + len(std_out) num = struct.pack('i',num) #利用struct模块将一个数据转换成bytes类型 i代表int型 sk.send(num) sk.send(std_out) sk.send(std_err) sk.close()
4.思路与总结
(1)思路
服务端:
1.先将真实数据的长度制作成固定长度
2.先发送固定长度的报头
3.再发送真实数据
客户端:
1.先接收固定长度的报头
2.再根据报头解压出真实长度
3.根据真实长度接收即可
(2)解决黏包问题的终极方案
服务端
1.先构造一个数据的详细字典
2.对字典数据进行打包处理 得到一个固定长度的数据
3.将上述打包之后的数据发送给客户端
4.将字典数据发送给客户端
5.将真实数据发送给客户端
客户端
1.先接收固定长度的数据
2.根据固定长度解析出即将要接收的字典真实长度
3.接收字典数据
4.根据字典数据 获取出真实数据的长度
5.接收真实数据长度
(3)总结
struct模块无论数据长度是多少 都可以帮你打包成固定长度
然后基于该固定长度 还可以反向解析出真实长度
struct模块针对数据量特别大的数字没有办法打包!!!