网络编程(二)
1.socket套接字简介
当我们想要编写一个C/S架构的软件,实现数据交互,是需要编写代码操作OSI七层的,相当的复杂,由于操作OSI七层是
所有C/S架构的程序都需要经历的过程,所以有固定的模块,就是socket模块。
socket套接字是一种技术,socket是一个模块,socket模块提供了快捷方式,不需要自己处理每一层,这里我们只是简
单了解一下,因为socket是最底层的原理,很多框架都被封装了。
2.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,否则程序就会一直卡在原地'''
3.通信循环
我们上述实现的代码有很多需要优化的地方,比如利用'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')) # 接受服务端回复的消息进行解码
4.链接循环
# 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
'''目前我们的服务端只能实现一次服务一个客户端,不能做到同时服务多个,等我们学完并发之后才可以'''
5.半链接池
'listen(参数)':参数的设置就是最大链接客户端的个数,虽然已经链接多个,但是只能一个一个交互,一个结束之
后才能开始下一个。
6.黏包问题
# 服务端
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括号内的参数我们不知道要填多大的,因为我们不知道要接受的数据有多大,如
果我们能够精确的知道要接收数据的大小,就不会出现黏包问题。
7.解决黏包问题
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.接收真实数据长度
8.代码演示
'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小白陆禄绯,欢迎各位大佬的指点!!!
原创文章,作者:sunnyman218,如若转载,请注明出处:https://blog.ytso.com/244404.html