失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Python之路(第三十一篇) 网络编程:简单的tcp套接字通信 粘包现象

Python之路(第三十一篇) 网络编程:简单的tcp套接字通信 粘包现象

时间:2023-02-21 21:22:19

相关推荐

Python之路(第三十一篇) 网络编程:简单的tcp套接字通信 粘包现象

一、简单的tcp套接字通信

套接字通信的一般流程

服务端

server = socket() #创建服务器套接字server.bind()#把地址绑定到套接字,网络地址加端口server.listen()#监听链接inf_loop:#服务器无限循环conn,addr = server.accept() #接受客户端链接,建立链接connconn_loop: #通讯循环conn.recv()/conn.send() #通过建立的链接conn不断的对话(接收与发送消息)conn.close() #关闭客户端套接字链接connserver.close() #关闭服务器套接字(可选)

客户端

client = socket() # 创建客户套接字client.connect() # 尝试连接服务器,用ip+portcomm_loop: # 通讯循环client.send()/client.recv() # 对话(发送/接收)消息client.close() # 关闭客户套接字

套接字通信例子

socket通信流程与打电话流程类似,我们就以打电话为例实现简单的tcp套接字通信

服务端

import socket​# 1.买手机phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 基于网络通信的 基于tcp通信的套接字​# 2.绑定手机卡(IP地址) 运行这个软件的电脑IP地址 ip和端口都应该写到配置文件中phone.bind(('127.0.0.1',8080)) # 端口0-65535 0-1024 给操作系统,127.0.0.1是本机地址即本机之间互相通信​# 3.开机phone.listen(5) # 5 代表最大挂起的链接数​# 4.等电话链接print('服务器运行啦...')# res = phone.accept() #底层 就是 tcp 三次握手# print(res)conn,client_addr = phone.accept() # conn 电话线 拿到可以收发消息的管道 conn链接​while True: #通信循环,可以不断的收发消息# 5.收发消息data = conn.recv(1024) # 1024个字节 1.单位:bytes 2.1024代表最大接收1024个bytesprint(data)​conn.send(data.upper())​# 6.挂电话conn.close()

客户端

import socket​# 1.买手机 客户端的phone 相当于服务端的 connphone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 基于网络通信的 基于tcp通信的套接字​# 2.拨号 (服务端的ip 和服务端的 端口)phone.connect(('127.0.0.1',8080)) #phone 拿到可以发收消息的管道 链接对象phone,建立了与服务端的链接​while True:# 3.发收消息 bytes型msg = input("请输入:")phone.send(msg.encode('utf-8'))data = phone.recv(1024)print(data)​# 4.关闭phone.close()

注意:这里的发消息收消息都不能为空,否则会出现错误。

这里只能接收一个链接,不能循环接收链接,即打一次电话不能再打了,只能重新开机(重新运行程序)再打,

所以这里要加上链接循环。

加上链接循环

服务端

import socket​phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#这里是重用ip和端口,防止出现地址被占用的情况,即time_wait状态phone.bind(('127.0.0.1',8080))phone.listen(5)while True: #连接循环 没有并发 但可一个一个 接收客户端的请求,一个链接结束,另外一个链接进来print('服务器开始运行啦...')conn,client_addr = phone.accept() # 现在没并发 只能一个一个print(client_addr)​while True:try: # try...except 出异常适合windows 出异常这里指客户端断开,防止服务端直接终止data = conn.recv(1024)if not data:break #linux客户端意外断开,这里接收的就是空,防止接收为空的情况print('客户端数据:',data)conn.send(data.upper())except ConnectionResetError:breakconn.close()phone.close()​# 针对客户端意外断开的两种情况#使用try ...except 是防止客户端意外断开产生# ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。# 错误,针对windows系统​# linux客户端意外断开,这里接收的就是空,防止接收为空的情况# 用if 判断接收的消息是否为空

客户端

import socketphone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)phone.connect(('127.0.0.1',8080))while True:msg = input('msg>>>:').strip() # ''if not msg:continue #防止输入为空的情况phone.send(msg.encode('utf-8')) # b''data = phone.recv(1024)print(data.decode('utf-8')) #解码​phone.close()

附:一个服务端,多个客户端,将一个客户端复制多个相同的文件,同时运行多个相同代码的客户端文件即可实现多个客户端链接服务端,但是这种链接不是同时的,只能一个客户端通信完,另外一个客户端在连接池(backlog设置的内容)里等着,等一个链接结束才能开始通信。

可能会遇到的问题

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法),即之前用的端口在系统中仍未清理

解决方法

方法1

#加入一条socket配置,重用ip和端口​phone=socket(AF_INET,SOCK_STREAM)phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8080))

方法2

在linux系统中发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,vi /etc/sysctl.conf​编辑文件,加入以下内容:net.ipv4.tcp_syncookies = 1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_fin_timeout = 30然后执行 /sbin/sysctl -p 让参数生效。net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;​net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;​net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。​net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间

二、基于tcp实现远程执行命令

模拟ssh远程执行命令 ,执行命令即Windows的命令提示行里输入命令,在linux的终端输入命令

通过tcp模拟执行命令并获得结果,这里需要用到subprocess模块

如何执行系统命令: 并拿到执行结果

import os

os.system # 只能拿到 运行结果 0 执行成功 非0 失败

一般用:

import subprocess

obj = subprocess.Popen('dir d:',shell=True) # shell 启了一个cmd

把命令结果丢到管道里面:

subprocess.Popen('dir d:',shell=True,

stdout=subprocess.PIPE)

print(obj.stdout.read().decode('gbk'))拿到命令的结果

print(obj.stderr.read().decode('gbk'))拿到产生的错误,Windows系统用'gbk'编码,linux用'utf-8'编码

#且只能从管道里读一次结果

例子

服务端

import socketimport subprocess​ip_port = ("127.0.0.1",8000)buffer_size = 1024tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_server.bind(ip_port)tcp_server.listen(5)​while True:print("服务器开始运行啦")conn,addr = tcp_server.accept()# print("conn是",conn)while True:try:# 1、收到命令cmd = conn.recv(buffer_size)print("收到客户端的命令",cmd.decode("utf-8"))# 2、执行命令,拿到结果p = subprocess.Popen(cmd.decode("utf-8"),stdout=subprocess.PIPE,stdin=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)res_cmd_err = p.stderr.read()res_cmd_out = p.stdout.read() #这里产生的结果Windows的编码是'gbk',linux是'utf-8'# print("res_cmd——out",res_cmd_out)if res_cmd_err: #出现错误res_cmd = res_cmd_errconn.send(res_cmd)else:if not res_cmd_out: #命令正常执行,但没有返回值res_cmd = "命令执行成功!"conn.send(res_cmd.encode("gbk")) #3、将结果返回给客户端,注意Windows和linux的编码不同else:conn.send(res_cmd_out)except Exception as e:print(e)breakconn.close()

客户端

import socket​ip_port = ("127.0.0.1",8000)buffer_size = 1024tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_client.connect(ip_port)while True:# 1、发命令cmd = input("请输入命令:").strip()if not cmd:continueif cmd == "quit":breaktcp_client.send(cmd.encode("utf-8"))# 2、接收命令,但是这里接收的数据量可能大于buffersize,即一次接收不完,下次通信接收的是上次未接收完的数据,就会产生粘包现象res = tcp_client.recv(buffer_size)print(res.decode("gbk")) #注意Windows和linux的编码不同tcp_client.close()

三、tcp粘包现象

须知:只有TCP有粘包现象,UDP永远不会粘包。

socket收发消息的底层原理

收发消息流程

1、发送方的应用程序将字节要发送的消息复制到自己的缓存(内存),操作系统(os)通过调用网卡将缓存的消息发送到接收方的网卡

2、接收方网卡将消息存在自己操作系统的缓存中,接收方的应用程序从自己的缓存中取出消息

总结

1、程序的内存和os(操作系统)的内存两个内存互相隔离,程序的内存是用户态 的内存,操作系统的内存是内核态的内存

2、发送消息是将用户态的内存复制给内核态的内存

3、发送方遵循tcp协议将消息通过网卡发送给接收方,接收方通知接收方的操作系统调用网卡接收数据,还要讲内存态的消息复制到用户态的内存

4、发送方消息复制给自己内核态的内存速度快时间短,接收方要通知OS收消息,还要复制消息,用时长

不管是recv还是send都不是直接接收对方的数据,而是操作自己的操作系统内存,不是一个send对应一个recv

基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法(Nagle算法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包)把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

两种情况下会发生粘包

1、发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

2、接收方不及时接收缓冲区的包,或者由于buffersize的限制,一次接收不完,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

例子

服务端

import socketimport timeserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.bind(('127.0.0.1', 9999))server.listen(5)​print('... 开始运行...')conn, addr = server.accept()​#data1 = conn.recv(1024)​data1 = conn.recv(1) # 当只取一个字符的时候,剩下的数据还在缓存池里面,下次接收时间很短的话,# 会继续把上次没接收完的一起取出来,就发生的粘包现象print('第一次', data1)​data2 = conn.recv(1024)print('第二次', data2)​conn.close()server.close()

客户端

# 两次send:数据量小,时间间隔很短,会发生粘包​import socketimport time​client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)client.connect(('127.0.0.1', 9999))​client.send('hello'.encode('utf-8'))​# time.sleep(1) #两次send直接隔一段时间,不会发生粘包现象​client.send('world'.encode('utf-8'))​client.close()

四、解决粘包问题

粘包问题产生的根源是接收方不知道一次提取多少字节的数据,那么需要发送方在发送数据前告知接收方我这次要发送多少字节的数据即可。

解决方式的简单版

先用struct 发送固定长度的消息,传递要发送消息的长度,然后按照这个长度接收消息

服务端

import socketimport subprocessimport struct​ip_port = ("127.0.0.1",9001)back_log = 5buffer_size = 1024​tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_server.bind(ip_port)tcp_server.listen(back_log)​while True:conn,addr = tcp_server.accept()print("服务器开始运行啦!")while True:try:cmd = conn.recv(buffer_size)if not cmd: breakp = subprocess.Popen(cmd.decode("utf-8"), stderr=subprocess.PIPE, stdin=subprocess.PIPE,stdout=subprocess.PIPE, shell=True)err = p.stderr.read()if err:res_cmd = errelse:res_cmd = p.stdout.read()if not res_cmd:res_cmd = "执行成功!".encode("gbk")print("命令已经执行!")​# 第一步:获取结果消息的长度length = len(res_cmd)# 第二步:将结果消息的长度封装为一个固定长度的报头header = struct.pack("i", length)# 第三步:先向接收方发送报头,使接收方知道真正接收的消息是多长,# 然后根据这个长度来重复循环接收消息conn.send(header)conn.send(res_cmd)except Exception as e:print(e)breakconn.close()

客户端

import socketimport struct​ip_port = ("127.0.0.1", 9001)buffer_size = 1024​tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_client.connect(ip_port)​while True:cmd = input("请输入命令:")if not cmd: continueif cmd == "quit": breaktcp_client.send(cmd.encode("utf-8"))# 第一步:接收一个固定长度的报头header = tcp_client.recv(4)# 第二步:解码获取报头里隐藏的真实要接收消息的长度res_length = struct.unpack("i", header)[0]# 第三步:根据消息的长度来不断的循环收取消息recv_data = b""recv_data_size = 0while recv_data_size < res_length:res_cmd = tcp_client.recv(buffer_size)recv_data = recv_data + res_cmdrecv_data_size = len(recv_data)print("收取的数据是", recv_data.decode("gbk"))​tcp_client.close()

解决方式终极版

通过自定义的报头来传递除了消息长度外更多的消息,为传递的消息做一个字典。

服务端

import socketimport subprocessimport structimport json​ip_port = ("127.0.0.1", 9000)back_log = 5buffer_size = 1024tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_server.bind(ip_port)tcp_server.listen(back_log)​while True:print("服务器开始运行啦!")conn, address = tcp_server.accept()while True:try:cmd = conn.recv(buffer_size)if not cmd: continuep = subprocess.Popen(cmd.decode("utf-8"), stderr=subprocess.PIPE, stdin=subprocess.PIPE,stdout=subprocess.PIPE, shell=True)err = p.stderr.read()# print(err)if err:res_cmd = errelse:res_cmd = p.stdout.read()# print(res_cmd)# print(res_cmd)if not res_cmd:res_cmd = "已经执行啦!".encode("gbk")res_length = len(res_cmd)# 第一步:制作自定制的字典作为报头,存储多种信息header_dict = {"filename": "a.txt","md5": "7887414147774415","size": res_length}# 第二步:将字典序列化转为json字符串,然后进行编码转成bytes,以便于直接网络发送header_bytes = json.dumps(header_dict).encode("utf-8")# 第三步:获得这个报头的长度header_length = len(header_bytes)# 第四步:将报头的长度打包成固定的长度,以便接收方先接收报头send_header = struct.pack("i", header_length)# 第五步:先发送报头的长度conn.send(send_header)# 第六步:发送报头conn.send(header_bytes)# 第七步:发送真实的消息conn.send(res_cmd)except Exception as e:print(e)breakconn.close()

客户端

import socketimport structimport jsonip_port = ("127.0.0.1", 9000)buffer_size = 1024tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_client.connect(ip_port)while True:cmd = input("请输入命令:")if not cmd: continueif cmd == "quit":breaktcp_client.send(cmd.encode("utf-8"))# 第一步:接收报头的长度信息header_length = tcp_client.recv(4)# 第二步:获取报头的长度,解码获取报头的长度header_size = struct.unpack("i", header_length)[0]# 第三步:根据报头的长度信息接收报头信息header_bytes = tcp_client.recv(header_size).decode("utf-8")# 第四步:根据接收的报头信息反序列化获得真实的报头header_dict = json.loads(header_bytes)print("客户端收到的报头字典是",header_dict)# 第五步:根据报头字典获取真实消息的长度res_size = header_dict["size"]# 第六步:根据获取的真实消息的长度不断循环获取真实消息data = b""data_size = 0while data_size < res_size:recv_data = tcp_client.recv(buffer_size)data = data + recv_datadata_size = len(data)print("接收的数据是", data.decode("gbk"))tcp_client.close()

如果觉得《Python之路(第三十一篇) 网络编程:简单的tcp套接字通信 粘包现象》对你有帮助,请点赞、收藏,并留下你的观点哦!

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