开发工程师必备————【Day2】网络编程之TCP与UDP协议,黏包处理

今日内容概要

  • TCP与UDP协议
  • socket套接字编程
  • 半连接池
  • TCP黏包问题及解决思路

TCP与UDP协议

1.规定了数据传输所遵循的规则;
数据传输能够遵循的协议有很多,TCP和UDP是常见的两个。

2.TCP协议:
(1)三次握手
建立双向通道
开发工程师必备————【Day2】网络编程之TCP与UDP协议,黏包处理_第1张图片

ps:洪水攻击——同时让大量的客户朝服务端发送建立TCP连接的请求。
(2)四次挥手
断开双向通道
开发工程师必备————【Day2】网络编程之TCP与UDP协议,黏包处理_第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模块针对数据量特别大的数字没有办法打包!!!

你可能感兴趣的:(网络,udp,tcp/ip)