之前简单介绍了tcp和udp的服务端和客户端,但一个完整的服务端必须至少满足三个功能
(1)绑定一个固定的ip和port
(2)一直对外提供服务,稳定运行
(3)能够支持并发
一、通信循环
对于客户端与服务端,不单单只能交流一次,正常需要交流多次,这时候需要支持通信循环,用while循环实现多次交流
服务端:
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) conn, client_addr = server.accept() # 通信循环 while True: data = conn.recv(1024) conn.send(data.upper()) conn.close() server.close()
客户端:
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) # 通信循环 while True: msg=input('>>: ').strip() client.send(msg.encode('utf-8')) data=client.recv(1024) print(data) client.close()
二、bug处理
(1)起因:当客户端非正常的断开,服务端就会报错,可预知但无法准确知道。
过程分析:客户端输入了空,服务端不会收到空数据;如果服务端收到了空数据,肯定是客户端单方面的把链接异常中断掉,而在在windows系统上服务端就会抛出异常,在Linux系统上服务端recv一直收空,无法预知异常发生的条件
解决方法:异常处理 try...except
服务端
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: data = conn.recv(1024) if len(data) == 0:break # 针对linux系统 print('-->收到客户端的消息: ',data) conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
(2)起因:当用户端输入空时候,用户端堵塞住
过程分析:当用户端输入了空数据,用户端的操作系统收不到数据,收不到数据不会传到用户端的网卡,也就不能传输数据,服务端收不到数据,所以
服务端也不会回发数据,用户端也就收不到数据最终堵塞
解决方法:添加判断,不让用户端输入空
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: msg=input('>>: ').strip() #msg='' if len(msg) == 0:continue #输入不能为空 client.send(msg.encode('utf-8')) #client.send(b'') # print('has send') data=client.recv(1024) # print('has recv') print(data) client.close()
三、链接循环
单单一个服务端和客户端交流是不够的,需要多个客户端可以与链接循环
服务端
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8081)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: data = conn.recv(1024) if len(data) == 0: break # 针对linux系统 print('-->收到客户端的消息: ', data) conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
客户端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: msg=input('>>: ').strip() #msg='' if len(msg) == 0:continue client.send(msg.encode('utf-8')) #client.send(b'') # print('has send') data=client.recv(1024) # print('has recv') print(data) client.close()
四、模拟ssh实现远程执行命令
为了执行系统命令,服务端需要导入subprocess模块
server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8081)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: cmd = conn.recv(1024) #cmd=b'dir' if len(cmd) == 0: break # 针对linux系统 obj=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() print(len(stdout) + len(stderr)) conn.send(stdout+stderr) except ConnectionResetError: break conn.close() server.close()
客户端:
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: cmd=input('>>: ').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) cmd_res=client.recv(1024000) print(cmd_res.decode('gbk')) client.close()
五、粘包问题
1、粘包问题的起因:tcp协议流式传输数据的方式 导致的,必须要全部收完
假设客户端、服务端接受的数据为1024个字节
当发送的数据量大于接受量1024的时候,就会出现每次接受只有1024个字节,剩余的字节等下次命令/输入的时候再输入过来,导致
命令错乱
2、解决思路:
(1)方案一:可以提高接受数据的上限,但是提高也有上限的,当设置的上限过高或者超过内存大小的时候会报错,
而且往往传输的文件大于内存,这个方法不适用
(2)方案二:自定义报头 ,循环多收几次收干净,一次性打印出来
步骤一:设计一个固定长度的报头数据,报头含有数据流的长度,长度是bytes才能传输,需要导入
struct模块,把整型转化为bytes,底层原理就是报头与真正的数据连在一起
ps;补充struct模块的使用
import struct # obj1=struct.pack('i',13321111111) # print(obj1,len(obj1)) 将整型转化为bytes # res1=struct.unpack('i',obj1) 将bytes反解化成整型 # print(res1[0]) obj1=struct.pack('q',1332111111111111111111111) print(obj1,len(obj1))
服务端发给客户端:
1先制作固定长度的报头
2再发送报头
3最好发送真实的数据
server.bind(('127.0.0.1', 8081)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: cmd = conn.recv(1024) #cmd=b'dir' if len(cmd) == 0: break # 针对linux系统 obj=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() # 1. 先制作固定长度的报头 header=struct.pack('i',len(stdout) + len(stderr)) # 2. 再发送报头 conn.send(header) # 3. 最后发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
步骤二:
客户端接受数据
1、先收报头,从报头里解出数据的长度
2、接受真正的数据
3、每次以1024个数据回收数据,直到收到的数据长度大于含报头的所有数据流的长度,就不收了,然后打印所有收到的数据
from socket import * import struct client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: cmd=input('>>: ').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) #1. 先收报头,从报头里解出数据的长度 header=client.recv(4) total_size=struct.unpack('i',header)[0] #2. 接收真正的数据 cmd_res=b'' recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode('gbk')) client.close()
(3)方案三(完整版):方案二还有个问题没有解决,那就是如果当报头中的文件长度很大,而struct只能把整型转化为4、8字节,当发送的数据很大的时候也会超出struct的转化长度,struct也无法转化数据的长度
解决思路:1、先制作报头,把报头做成一个字典包含比如文件名、md5值、数据长度等等
序列化字典得到字典的bytes,这时候的bytes长度就很小
这时候struct就可以又把字典的bytese的长度转化为固定4个bytes
2、先发送4个bytes(包含报头的长度)
3、在发送报头
4、最好发送真实的数据
from socket import * import subprocess import struct import json server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8081)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: cmd = conn.recv(1024) # cmd=b'dir' if len(cmd) == 0: break # 针对linux系统 obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout = obj.stdout.read() stderr = obj.stderr.read() # 1. 先制作报头 header_dic = { 'filename': 'a.txt', 'md5': 'asdfasdf123123x1', 'total_size': len(stdout) + len(stderr) } header_json = json.dumps(header_dic) header_bytes = header_json.encode('utf-8') # 2. 先发送4个bytes(包含报头的长度) conn.send(struct.pack('i', len(header_bytes))) # 3 再发送报头 conn.send(header_bytes) # 4. 最后发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
客户端接受数据
1先收4bytes,解出报头的长度
2再接受报头,拿到字典
3接受真正的数据
from socket import * import struct import json client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: cmd=input('>>: ').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) #1. 先收4bytes,解出报头的长度 header_size=struct.unpack('i',client.recv(4))[0] #2. 再接收报头,拿到header_dic header_bytes=client.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #3. 接收真正的数据 cmd_res=b'' recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode('gbk')) client.close()