当我们想要编写一个C/S架构的软件,实现数据交互,是需要编写代码操作OSI七层的,相当的复杂,由于操作OSI七层是 所有C/S架构的程序都需要经历的过程,所以有固定的模块,就是socket模块。 socket套接字是一种技术,socket是一个模块,socket模块提供了快捷方式,不需要自己处理每一层,这里我们只是简 单了解一下,因为socket是最底层的原理,很多框架都被封装了。
'''C/S架构的软件无论是在编写还是运行,都应该先考虑服务端''' # 服务端 import socket server = socket.socket() # 产生一个socket对象 server.bind(('127.0.0.1', 8888)) # 产生服务端地址 '''通过查看源码得知,括号内不写参数默认就是基于网络的遵循TCP协议的套接字, 服务端应该具备固定地址这个特征 127.0.0.1是计算机的本地回环地址,只有当前计算机本身可以访问''' server.listen(5) # 半连接池 sock,addr = server.accept() # 等待并接收客户端,没有客户端就会原地等待 '''listen和accept对应TCP三次握手服务端的两个状态''' print(addr) # 打印客户端地址 data = sock.recv(1024) # 接受客户端的消息 print(data.decode('utf8')) # 接受客户端的消息进行解码 sock.send('你好'.encode('utf8')) # 给客户端发送消息 '''recv和send接受和发送的都是bytes类型的数据''' sock.close() # 关闭与客户端的链接 server.close() # 关闭服务端 # 客户端 import socket client = socket.socket() # 产生一个socket对象 client.connect(('127.0.0.1', 8888)) # 根据服务端的地址链接 client.send(b'i am fine') # 给服务端发信息 data = client.recv(1024) # 接受服务端回复的消息 print(data.decode('utf8')) # 接受服务端回复的消息进行解码 client.close() # 关闭客户端 '''注意:服务端与客户端首次交互一边是recv,那么另一边必须是send,否则程序就会一直卡在原地'''
我们上述实现的代码有很多需要优化的地方,比如利用'input'可以动态获取用户输入的信息,还有就是不能发送空信息 ,所以就可以用'len'来判断长度是否为空。但是最大的问题就是只能发送一条信息,所以我们就要加一个循环来让他们循环发送信息。 # 服务端 while True: data = sock.recv(1024) # 接受客户端的消息 print(data.decode('utf8')) # 接受客户端的消息进行解码 flg = input('你想要发送什么信息>>>:').strip() if len(flg) == 0: # 判断长度是否为空 continue sock.send(flg.encode('utf8')) # 给客户端发送消息 # 客户端 while True: flg = input('你想发送什么信息>>>:').strip() if len(flg) == 0: # 判断长度是否为空 continue client.send(flg.encode('utf8')) # 给服务端发信息 data = client.recv(1024) # 接受服务端回复的消息 print(data.decode('utf8')) # 接受服务端回复的消息进行解码
# 1.关于重启服务端可能会报错问题 重启服务端可能会报错:'address in use',这个错误在苹果电脑报的比较频繁,Windows报该错误的频率较低。 解决措施: from socket import SOL_SOCKET,SO_REUSEADDR server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 在bind前加 # 2.链接循环 我们刚刚演示的时候有一个情况,就是在客服端异常退出之后服务端会直接报错,因为我们的是Windows系统,如果是 mac或者Linux系统的话服务端就会接收到一个空消息。但是这不是切合生活实际,生活实际应该是客户端如果异常断开,服务端 代码应该重新回到accept等待新的客户端。 处理方式: Windows系统:进行异常处理 mac、Linux系统:len判断 while True: sock,addr = server.accept() # 等待并接收客户端,没有客户端就会原地等待 '''listen和accept对应TCP三次握手服务端的两个状态''' print(addr) # 打印客户端地址 while True: try: data = sock.recv(1024) # 接受客户端的消息 if len(data) == 0: break print(data.decode('utf8')) # 接受客户端的消息进行解码 flg = input('你想要发送什么信息>>>:').strip() if len(flg) == 0: continue sock.send(flg.encode('utf8')) # 给客户端发送消息 except Exception: break '''目前我们的服务端只能实现一次服务一个客户端,不能做到同时服务多个,等我们学完并发之后才可以'''
'listen(参数)':参数的设置就是最大链接客户端的个数,虽然已经链接多个,但是只能一个一个交互,一个结束之 后才能开始下一个。
# 服务端 import socket server = socket.socket() server.bind(('127.0.0.1', 8888)) server.listen(5) conn,addr = server.accept() data1 = conn.recv(1024) print(data1) data2 = conn.recv(1024) print(data2) data3 = conn.recv(1024) print(data3) # 客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 8888)) client.send(b'jason') client.send(b'oscar') client.send(b'kevin') # 打印结果: b'jasonoscarkevin' b'' b'' 这是因为TCP协议的特点,TCP又叫流式协议,意思就是跟水流一样不间断,会将数据量比较小的并且时间间隔较短的 数据整合到一起发送,并且还会受制于'recv'括号内参数的大小。 '黏包问题'产生的原因其实就是因为recv括号内的参数我们不知道要填多大的,因为我们不知道要接受的数据有多大,如 果我们能够精确的知道要接收数据的大小,就不会出现黏包问题。
import struct data1 = 'hello oscar' print(len(data1)) # 11 res1 = struct.pack('i', len(data1)) # 第一个参数是格式,固定写i就行 print(len(res1)) # 4 ret1 = struct.unpack('i', res1) # 第一个参数是格式,固定写i就行 print(ret1) # (11,) data2 = 'hello oscar hello oscar hello oscar hello oscar' print(len(data2)) # 47 res2 = struct.pack('i', len(data2)) # 第一个参数是格式,固定写i就行 print(len(res2)) # 4 ret2 = struct.unpack('i', res2) # 第一个参数是格式,固定写i就行 print(ret2) # (47,) ''' pack:可以将任意长度的数字打包成固定长度。 unpack:可以将固定长度的数据解包成打包之前的真实长度。 ''' # 解决黏包问题思路 1.先将真实数据打包成固定长度的包 2.将固定长度的包先发给对方 3.对方接收到包之后再解包获取真实数据长度 4.接收真实数据长度
'recv'括号内的数字尽量不要太大,1024、2048、4096就足够了,所以针对大文件的接收应该采用循环的形式一次 接收一点点。 # 服务端 import os import socket import json import struct server = socket.socket() server.bind(('127.0.0.1', 8888)) server.listen(5) conn,addr = server.accept() data_dict = {'file_name': '**文件.txt', 'file_desc': '土狗', 'file_size': os.path.getsize(r'a.py')} # 1.先打包字典 dict_json_str = json.dumps(data_dict) dict_bytes = dict_json_str.encode('utf8') dict_package = struct.pack('i', len(dict_bytes)) # 2.发送报头 conn.send(dict_package) #3.发送字典 conn.send(dict_bytes) # 4.发送真实的数据 with open(r'a.py','rb') as f: for line in f: conn.send(line) # 客户端 import socket import struct import json client = socket.socket() client.connect(('127.0.0.1', 8888)) # 1.先接收固定长度的字典的报头 dict_len = client.recv(4) # 2.解析出字典的真实长度 dict_len_real = struct.unpack('i', dict_len)[0] # 3.接收字典数据 dict_data_bytes = client.recv(dict_len_real) dict_data = json.loads(dict_data_bytes) print(dict_data) # 4.循环接收文件数据,不要一次性接收 recv_size = 0 with open(dict_data.get('file_name'),'wb') as f: while recv_size < dict_data.get('file_size'): data = client.recv(1024) recv_size += len(data) f.write(data)
这里是IT小白陆禄绯,欢迎各位大佬的指点!!!