- 传输层
- 应用层
- socket套接字
- 半连接池
- 黏包问题
传输层除了规定了端口协议,还规定了数据传输所遵循的规则,常见的是TCP和UDP协议
- TCP协议
基于数据流传输数据
发送数据会返回确认消息,否则会在一定时间内反复发送
传输数据需要先三次握手建立双向通道
假设客户端先发送连接请求,SYN=1代表发起一个新链接,seq是一个随机标识数
然后服务端收到连接请求,回复ACK=1表示确认号有效,ack=j+1确认号,同时服务端也向客户端发送连接请求,SYN=1,seq=k
最后客户端收到ACK=1,ack=j+1知道自己连接成功,并回复服务端的连接请求,ACK=1,ack=k+1
客户端发送释放连接请求FIN=1,seq=n
服务端收到并回复ACK=1,ack=n+1,客户端收到后处于半关闭状态
服务端数据处理完毕后,发送释放连接请求FIN=1,seq=m
客户端收到并回复ACK=1,ack=m+1,服务端收到后释放连接,客户端在经过2MSL后也释放连接
2MSL是报文最大生命周期,因为需要确认服务端是否收到确认信息,所以需要等待2MSL - UDP协议
基于报文发送数据,没有任何通道也没有限制
UDP发送数据没有TCP安全,没有二次确认机制,数据丢失也不会管
主要取决于程序员自己采用什么策略和协议
常见的协议有:http,https,ftp…
基于文件类型的套接字家族
AF_UNIX,只能在计算机内不同程序之间做数据交互
基于网络类型的套接字家族
AL_INET,可以在不同计算机之间做数据交互
server:
import socket
server = socket.socket() # 先创建套接字对象,括号内不填参数默认使用基于网络的TCP套接字
server.bind(('127.0.0.1', 8080)) # 绑定固定的地址和端口,127.0.0.1本地回环地址,只能自己的计算机访问
server.listen(5) # 半连接池
sock, address = server.accept() # 等待服务端连接
print(sock,address) # sock是双向通道,address是客户端地址
sock.send('hello'.encode('utf8')) # 朝客户端发送数据,必须使用bytes格式
data = sock.recv(1024) # 接收客户端发送的数据,最多接收1024个bytes
print(data)
sock.close() # 结束与客户端的连接
server.close() # 关闭服务器
client:
import socket client = socket.socket() client.connect((127.0.0.1, 8080)) # 确保和服务端的ip端口一致 data = client.recv(1024) print(data) client.send(b'olleh') client.close()
代码优化:
- send与recv
客户端与服务端不能同时发送或接收 - 自定义消息
可以使用input获取想要传递的信息,主要注意编码与解码 - 通信循环
在程序的发送与接收环节添加循环 - 服务端能够持续的提供服务
服务端会因为客户端的断开连接而报错,使用异常捕获,一旦断开连接,服务端结束通信,回到连接处等待 - 消息不能为空
消息为空会判定为没有发送数据,可能会导致程序出现问题
判断是否为空,如果是则重新输入或自动添加数据 - 服务器频繁重启可能会报端口被占用的错误(主要是mac和Linux)
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 - 客户端异常退出会一直发空消息(mac,linux)
针对接收的消息加判断处理即可
server.listen(5)
后续连接的客户端会在这里阻塞,等待前边的客服端结束
参数是阻塞的计算机数量,超过数量计算机再连接会报错
主要是为了做缓冲,避免太多无效等待
客户端:
client.send(b’jason’)
client.send(b’kevin’)
client.send(b’jerry’)
服务端:
msg1 = sock.recv(1024)
msg2 = sock.recv(1024)
msg3 = sock.recv(1024)
print(msg1,msg2,msg3)
打印结果可能是msg1内有三个名字,后两个结果为空
这是因为TCP是以数据流的方式传输数据,如果数据很小,间隔短,那么久会自动组织到一起
解决方式:
先统计需要发送数据的大小,然后接收指定大小的数据
可以使用struct模块
import struct
info = '123456'
print(len(info)) # 6,数据原本长度
res = struct.pack('i', len(info)) # 将数据长度打包
print(res) # b'x06x00x00x00'
print(len(res)) # 4
ret = struct.unpack('i', res) # 通过解包获取原始长度
print(ret) # (6,)
print(ret[0]) # 6
struct可以帮你把数据长度打包成固定长度,然后可以反向解析出真实长度
但是文件过于庞大也会无法打包导致报错
解决方法:
- 使用os模块获取文件大小
- 打包数据
- 服务端传输打包数据
- 客户端接收4bytes,解包获取文件长度
- 服务端传输文件
- 客户端接收到指定大小后停止接收
还可以使用字典传输一些额外数据
最终版本- server:
import socket
import os
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
sock, address = server.accept()
file_size = os.path.getsize('a.txt')
info_dict = {
'title': 'a1.txt',
'size': file_size
}
dict_json = json.dumps(info_dict)
dict_pack = struct.pack('i', len(dict_json))
sock.send(dict_pack)
sock.send(dict_json.encode('utf8'))
with open(r'a.txt', 'rb') as f:
for file_line in f:
sock.send(file_line)
sock.close()
- client:
import json
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8080)) # 确保和服务端的ip端口一致
while True:
dict_pack = client.recv(4)
dict_len = struct.unpack('i', dict_pack)
dict_json = client.recv(dict_len[0])
info_dict = json.loads(dict_json)
file_size = info_dict.get('size')
total_size = 0
with open(info_dict.get('title'), 'wb') as f:
while total_size < file_size:
data = client.recv(1024)
f.write(data)
total_size += len(data)
break



