200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > TCP与UDP协议 socket套接字编程 通信相关操作(cs架构软件) TCP黏包问题及解决思路

TCP与UDP协议 socket套接字编程 通信相关操作(cs架构软件) TCP黏包问题及解决思路

时间:2020-09-04 03:55:50

相关推荐

TCP与UDP协议 socket套接字编程 通信相关操作(cs架构软件) TCP黏包问题及解决思路

OSI七层协议

传输层

1.PORT协议:前面讲过2.TCP协议与UDP协议:规定了数据传输所遵循的规则(数据传输能够遵循的协议有很多,TCP和UDP是较为常见的两个)

TCP协议

基于TCP传输数据是非常安全的,是因为数据不容易丢失,并非是因为有双向通道。不容易丢失的原因在于二次确认机制,每次发送数据都需要返回确认消息,否则在一定时间会反复发送

三次握手–建立双向通道

过程1.客户端向服务端发送请求,询问是否可以建立一个数据通道2.服务端就向客户端发送确认,允许客户端建立一个通向服务端的数据通道ps:此时数据通道是单向的,只允许客户端向服务端发送信息3.服务端向客户端发送请求,询问是否可以建立一个数据通道4.客户端就向服务端发送确认,允许服务端建立一个通向服务端的数据通道ps:此时数据通道是双向的,允许客户端、服务端互相发送信息上述有四步,可以将2、3不合并成一步,因为服务端向客户端发送确认的同时,也可以发送请求。将三步称之为三次握手

四次挥手–断开双向通道

过程1.客户端向服务端发送请求,询问是否可以关闭客户端到服务端的数据通道2.服务端就向客户端发送确认,允许关闭客户端到服务端的数据通道ps:此时数据通道由双向变为单向,只有服务端到客户端的通道了ps:这是服务端会检查还有没有数据没有发送完毕,没有发送完毕就继续发送,发送完毕就发送请求关闭通道3.服务端向客户端发送请求,询问是否可以关闭服务端到客户端的数据通道4.客户端就向服务端发送确认,允许关闭服务端到客户端的数据通道ps:此时数据通道就完全关闭了上述四个步骤称之为四次挥手,关闭双向通道,2、3步不能合并,因为需要有检查时间,看看数据是否发送完毕没有

UDP协议

基于UDP协议发送数据,没有任何的通道也没有任何的限制,UDP发送数据没有TCP安全,因为没有二次确认机制

总结

TCP类似于打电话,有来有往;UDP类似于发送短信,只要发送了,剩下的什么都不管

应用层

主要取决于程序员自己采用什么策略和协议,常见的协议有HTTP、HTTPS、FTP…

socket套接字

基于文件类型的套接字家族的名字是:AF_UNIX;基于网络类型的套接字家族的名字是:AF_INET

代码–基础

运行程序的时候,先确保服务端运行,之后才是客户端

服务端

1.创建一个socket对象

import socketserver = socket.socket()ps:socket是一个类,socket()此时就相当于实例化对象 def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):

2.绑定一个固定的地址(ip\port)

server.bind(('127.0.0.1',8080)) ps:1.bind() >>> def bind(self, address: Union[_Address, bytes]) -> None: ...>>>里面可以填元组,字符串,一般填写元组,字符串会出错在后续2. 127.0.0.1 ip地址,这里是本地回环地址,只允许自己的机器访问8080 端口号

3.半连接池

主要是为了做缓冲,避免太多的无效等待

server.listen(5)

4.开业 等待接客

sock,address = server.accept()print(sock,address)ps: 1. 类里面的函数accept()返回值是 return sock, addr2.sock是双向通道,address是客户端地址

5.数据交互

sock.send(b"hello client ,i'm server")data = sock.recv(1024)print(data)ps: 1. sock.send()是操客户端发送数据2. sock.recv()是接收客户端发送的数据 1024代表接收1024bytes

6.断开连接

sock.close() # 断连接server.close() # 关机

客户端

1.产生一个socket对象

import socketclient = socket.socket()

2.连接服务器(拼接服务端的ip和port)

client.connect(('127.0.0.1',8080))

3.数据交互

data = client.recv(1024) print(data)client.send(b"hello server ,i'm client")ps: client.recv(1024) 是接收服务端发送的数据 1024代表接收1024bytesclient.send()朝服务端发送数据

4.关闭

client.close()

代码–问题及优化

1.客户端与服务端不能同时执行同一个send或recv

意思就是如果客户端先接收(recv()),那么服务端就先发(send());如果客户端先发(send()),那么服务端就先收(recv())

2.消息自定义

用input获取用户数据(编码解码即可)

服务端msg= input("请输入你想给客户端发送的内容>>>:").strip()sock.send(msg.encode('utf8'))data = sock.recv(1024)print(data.decode('utf8'))客户端data = client.recv(1024)print(data.decode('utf8'))msg= input("请输入你想给服务端发送的内容>>>:").strip()client.send(msg.encode('utf8'))

3.循环通信

给数据交互环节添加循环即可

服务端while True:msg= input("请输入你想给客户端发送的内容>>>:").strip()sock.send(msg.encode('utf8'))data = sock.recv(1024)print(data.decode('utf8'))客户端while True:data = client.recv(1024)print(data.decode('utf8'))msg= input("请输入你想给服务端发送的内容>>>:").strip()client.send(msg.encode('utf8'))

4.服务端能够持续提供服务

也就是不会因为客户端断开连接而报错;我们就可以使用异常捕获:一旦客户断开连接,服务端就结束通信循环,跳到连接等待接客处

服务端while True:sock,address = server.accept()print(sock,address)# 数据交互while True:try:msg= input("请输入你想给客户端发送的内容>>>:").strip()sock.send(msg.encode('utf8'))data = sock.recv(1024)print(data.decode('utf8'))except ConnectionResetError :sock.close()break

5.消息不能为空

解决方式是判断待发送的消息是否为空,如果是则重新输入(主要针对客户端)

客户端while True:data = client.recv(1024)print(data.decode('utf8'))msg= input("请输入你想给服务端发送的内容>>>:").strip()if len(msg) == 0:msg = '手抖了一下'client.send(msg.encode('utf8'))

6.服务端频繁重启可能会报端口号被占用的错误(主要针对mac电脑)

在服务端写一下代码

from socket import SOL_SOCKET,SO_REUSEADDRserver.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind()前加

7.客户端异常退出就会发送空消息(针对mac linux)

针对接收的消息加判断处理即可

黏包问题

服务端接收数据,客户端发送数据,黏包问题更明显

服务端import socketserver = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5)sock, address = server.accept()print(sock.recv(1024))print(sock.recv(1024))print(sock.recv(1024))客户端import socketclient = socket.socket()client.connect(('127.0.0.1', 8080))client.send(b'jason')client.send(b'kevin')client.send(b'jerry')

产生原因

1.TCP特性流式协议:内容与内容之间没有明确的分界标志,所有的数据类似于水流连接在一起的ps:数据量小 并且时间间隔很多,那么就会自动组织到一起2.recv() 括号里规定了读取的字节数。由于我们不知道即将要接收的数据量多大,就容易产生黏包问题

struct模块

struct模块无论数据长度多少,都可以帮你打包成固定的长度;然后基于固定的长度,还可以反向解析出真实的长度

import structinfo1 = '你好 world'print(len(info1)) # 8res1 = struct.pack('i',len(info1))print(res1) #b'\x08\x00\x00\x00'print(len(res1)) # 4res2 = struct.unpack('i',res1)print(res2) # (8,)print(res2[0]) # 8import structinfo1 = '你好啊 你是我的全世界'print(len(info1)) # 11res1 = struct.pack('i',len(info1))print(res1) #b'\x0b\x00\x00\x00'print(len(res1)) # 4res2 = struct.unpack('i',res1)print(res2) # (11,)print(res2[0]) # 11

struct模块很对数据量特别大的数字没有办法打包

file_size = 11203495674265677res = struct.pack('i',file_size)print(len(res)) # struct.error: argument out of range

可以以字典的形式,解决数据量大的问题

file_dic ={'size':1223456575567567685,'name':'学习视频.mp4'}print(len(file_dic)) # 2res = struct.pack('i',len(file_dic))print(len(res)) # 4res1 = struct.unpack('i',res)print(res1[0]) # 2

struct知识点总结

1.struct.pack('i',len(info1)) 是将数据原来的长度打包,打印返回值是二进制,用len()方法打印出来是4, i是模式2.struct.unpack('i',res1) 将打包之后固定长度为4的数据拆包,打印返回值是一个元组,用索引取值取值出来的就是原来数据的长度

思路

1.学习了struct模块,对解决黏包问题有了一定的思路

1.先将真实数据的长度制作成固定的长度>>>struct.pack()2.先发送固定长度的报头 >>>sock.send()3.再发送真实的数据1.先接收固定长度的报头 >>> sock.recv(4)2.再根据报头解压出真实的长度 >>>struct.unpack()3.根据真实长度接收即可

2.问题:struct模块很对数据量特别大的数字没有办法打包

file_size = 11203495674265677res = struct.pack('i',file_size)print(len(res)) # struct.error: argument out of range

3.解决:可以以字典的形式,解决数据量大的问题

file_dic ={'size':1223456575567567685,'name':'学习视频.mp4'}print(len(file_dic)) # 2res = struct.pack('i',len(file_dic))print(len(res)) # 4res1 = struct.unpack('i',res)print(res1[0]) # 2

4.终极解决黏包方法

服务端1.先构造一个数据的详细字典2.对字典数据进行打包处理,得到一个固定长度的数据>>>struct.pack() 43.将上述打包之后的数据发送给客户端 >>>sock.send()4.将字典数据发送给客户端5.将真实数据发送给客户端客户端1.先接收固定长度的数据 >>> sock.recv(4)2.根据固定长度解析出即将要接收的字典真实长度 >>>struct.unpack()3.接收字典数据4.根据字典数据,获取出真实数据的长度5.接收真实数据长度

解决

服务端

import jsonimport osimport socketimport structserver = socket.socket()server.bind(('127.0.0.1',8080))server.listen(5)while True:sock,address = server.accept()while True:# 1.先构造数据文件的字典file_dict = {'name':'luo编程视频合集.txt','size':os.path.getsize(r'../xxx视频合集.txt'),}# 将字典打包成固定长度的数据dict_json = json.dumps(file_dict) # 将字典转换成json字符串file_bytes_dict = dict_json.encode('utf8') # 将json字符串格式转换成二进制dict_len_bytes= struct.pack('i',len(file_bytes_dict))# 发送固定长度的字典报头sock.send(dict_len_bytes)# 发送真实字典数据sock.send(dict_json.encode('utf8'))# 发送真实数据with open(r'../xxx视频合集.txt','rb') as f:for line in f:sock.send(line)break

客户端

import jsonimport socketimport structclient = socket.socket()client.connect(('127.0.0.1',8080))while True:# 先接收长度为4的报头数据header_len = client.recv(4) # 接收的是dict_len_bytes# 根据报头解包出字典的长度dict_len = struct.unpack('i',header_len)[0] # 68# 直接接收字典数据dict_data = client.recv(dict_len) # b'{"name": "luo\\u7f16\\u7a0b\\u89c6\\u9891\\u5408\\u96c6.txt", "size": 511}''# 解码并反序列化出字典real_dict = json.loads(dict_data) # {'name': 'luo编程视频合集.txt', 'size': 511}# 从数据字典中获取真实数据的各项信息total_size = real_dict.get('size')file_size = 0with open(r'%s' % real_dict.get('name'),'wb') as f:while file_size < total_size:data = client.recv(1024)f.write(data)file_size += len(data)print('文件接收完毕')break

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。