网络
OSI模型
物理层-》数据链路层-》网络层-》传输层-》会话层-》表示层-》应用层
TCP/IP模型
网络接口层(物理层,数据链路层)-》互联网层(链路层)-》传输层(传输层)-》应用层(会话层,表示层,应用层)
TCP/UDP协议
TCP和UDP协议是传输层最重要的两种协议,为上层用户提供级别的通信可靠性
TCP协议
传输控制协议(TCP):TCP定义了两台计算机之间进行可靠的传输而交换的数据和确认信息的格式,以及计算机为了确保数据的正确到达而采取的措施。协议规定了TCP软件怎样识别给定计算机上的多个目的进程,如何对分组、重复这类差错进行恢复。还规定了两台计算机如何初始化TCP数据流传输以及如何结束这已传输。
特点:提供的是面向连接、可靠的字节流服务
TCP适用于对可靠性要求高的通信系统
三次握手
- 在建立通道时,客户端首先要先向服务器发送一个SYN同步信号
- 服务端在接收到这个信号之后会向客户端发出SYN同步信号和ACK确认信号
- 当服务端的ACK和SYN到达客户端后,客户端发送ACK确认信号到服务端,客户端与服务端之间的这个通道就会被建立起来
四次挥手
- 在数据传输完毕后,客户端会向服务端发出一个FIN终止信号
- 服务端收到这个信号之后会向客户端发出一个ACK确认信号
- 如果服务端此后也没有数据发送给客户端时,服务端会向客户端发送一个FIN终止信号
- 客户端收到这个FIN信号后,会回复一个确认信号,在服务端接收到这个信号以后,通道关闭
UDP协议
用户数据报协议(UDP):UDP是一个简单的面向数据报的传输层协议。
特点:提供的是非面向连接的、不可靠的数据流传输,由于在传输数据报文前不用在客户端和服务器之间建立一个连接,也没有超时重发等机制,所以传输速度很快。
UDP适用于一次只传输少量数据、对可靠性要求不高的应用环境
TCP协议与UDP协议最大的区别就是:TCP是面向连接的,UDP是无连接的。TCP协议和UDP协议各有所长,各有所短,使用于不同的通信环境
HTTP协议
HTTP是一个简单的响应-请求协议,通常运行在TCP之上。
HTTP是基于客户/服务器模式,且面向连接的,无状态的。
事务处理过程:
- 客户与服务器建立连接
- 客户向服务器提出请求
- 服务器接收请求,并根据请求返回响应的文件作为应答
- 客户与服务器关闭连接
Socket
socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。
本地的进程间通信可以使用队列,同步(互斥锁、条件变量等)
网络进程间的通信:
利用IP地址,协议,端口号,就可以在标识网络中的进程,这样就可以利用这个标识去与其他进程进行交互
scoket是进程间通信的一种方式,它能实现不同主机间的进程通信
TFTP
简单文件传输协议
特点:简单,占用资源小,适合传递小文件,适合在局域网进行传递,端口号为69,基于UDP实现
TFTP数据包格式
使用UDP实现TFTP协议
分为服务端和客户端
- 引入socket包
- 引入struct包,能够将python数据转换成c的字节流数据
- 定义一个socket使用AF_INET,SOCK_DGRAM
- 客户端确定服务器地址,使用69端口;服务器需要绑定69端口
- 将请求做成数据包
- 将请求发给服务器;服务器接收数据并进行操作
- 接收服务器返回的数据(data,(service_ip,server_port))
- 对数据进行判断,然后进行下一步操作
- 不管是服务器端还是客户端,在操作完数据后,要返回给对方一个ack进行确认
服务器端
from socket import *
import struct
# 服务器端socket
s = socket(AF_INET, SOCK_DGRAM)
# 绑定IP和端口号
s.bind(('', 69))
def download(filename, client_ip, client_port):
# 创建一个新的socket,负责发送文件内容的数据包到客户端
new_socket = socket(AF_INET, SOCK_DGRAM)
# 文件内容数据包计数器,其实就是数据块的编号
num = 0
# 定义客户端退出的标签
flag = True
# 定义文件f
f = None
try:
f = open(filename, 'rb')
except:
error_package = struct.pack('!HH5sb', 5, 5, 'error'.encode('utf-8'), 0) # H表示python的Integer转成c的没有符号的short,
# 5s表示把python中含有5个字符的字符串转换成c的字符数组
new_socket.sendto(error_package, (client_ip, client_port)) # 把错误数据包发给客户端
# exit()#当前线程结束,当前客户端退出服务器
flag = False
# 如果文件存在,需要把文件内容切成一个个的数据包发送给客户端,一个数据包数据内容包含512字节数据
while flag:
# 从文件内容中读取512字节
read_data = f.read(512)
# 创建一个数据包
data_package = struct.pack('!HH', 3, num) + read_data
# 发送数据包
new_socket.sendto(data_package, (client_ip, client_port))
if len(read_data) < 512: # 文件内容的数据读完
print(f"客户端{client_ip},文件下载完成")
# exit() # 当前线程退出
break
# 服务器接收ACK的确认数据
recv_ack = new_socket.recvfrom(1024) # 里面有数据包内容,还有客户端ip和端口
oprator_code, ack_num = struct.unpack('!HH', recv_ack[0])
print('客户端:%s,的确认信息是' % client_ip, ack_num)
num += 1
# 保护性代码
if int(oprator_code) != 4 or int(ack_num) < 0: # 不正常的ack确认信息
break
if f:
f.close()
new_socket.close() # 客户端真正退出
def upload(filename, client_ip, client_port):
# 新建一个socket,用来发送数据包
new_socket = socket(AF_INET, SOCK_DGRAM)
# 新建文件,用来写入数据
f = None
# 新建flag,标记客户端退出
flag = True
try:
f = open('service_' + filename, 'ab')
except:
# 建立一个error包,用来给客户端返回错误数据
error_package = struct.pack('!HH5sb', 5, 5, 'error'.encode('utf-8', 0))
# 将数据包发送给客户端
new_socket.sendto(error_package, (client_ip, client_port))
flag = False
# 向客户端返回ack
data_ack = struct.pack('!HH', 4, 0)
new_socket.sendto(data_ack, (client_ip, client_port))
while flag:
# 接收客户端返回的文件内容
recv_data = new_socket.recvfrom(1024)
operator_data, num = struct.unpack('!HH', recv_data[0][:4])
if operator_data == 5:
print('上传出错,文件不存在')
break
# 将文件内容写入文件
f.write(recv_data[0][4:])
# 向客户端返回ack
data_ack = struct.pack('!HH', 4, num)
new_socket.sendto(data_ack, (client_ip, client_port))
if len(recv_data[0]) < 516:
print(f'客户端{client_ip},上传完成')
break
def server():
while True:
# 服务器等待客户端发送过来数据,进行接收
recv_date, (clien_ip, client_port) = s.recvfrom(1024)
print(recv_date, clien_ip, client_port)
# 判断数据包是否是客户端请求的数据包
if struct.unpack('!b5sb', recv_date[-7:]) == (0, b'octet', 0):
# 得到操作码的值
operator_code = struct.unpack('!H', recv_date[:2])
# 得到文件名字
file_name = recv_date[2:-7].decode('utf-8')
if operator_code[0] == 1: # 如果等于1就是下载请求数据包
print(f'客户端想下载文件:{file_name}')
download(file_name, clien_ip, client_port)
elif operator_code[0] == 2: # 如果等于2就是上传请求数据包
print(f'客户端想上传的文件:{file_name}')
upload(file_name, clien_ip, client_port)
if __name__ == '__main__':
server()
客户端代码
from socket import *
import struct # 负责python数据结构和C语言的数据结构转换
def download(file_Name, s, host_port):
# octet是C里面的字节数据
# pack把数据变成包
# '!H%dsb5sb'代表格式,!开头,H:将python整形转换为C中短整型,%ds:字符串长度为len的字符串,b:数字型字符
# 请求的数据包
data_package = struct.pack('!H%dsb5sb' % len(file_Name), 1, file_Name.encode('utf-8'), 0, 'octet'.encode('utf-8'),
0)
# 把数据包发到目标服务器
s.sendto(data_package, host_port)
# 客户端首先创建一个空白文件
f = open('client_' + file_Name, 'ab')
while True:
# 客户端接收服务器发过来的数据,数据只有两种:1.下载文件内容数据包,2.error信息包
recv_date, (server_ip, server_port) = s.recvfrom(1024)
oprator_code, num = struct.unpack('!HH', recv_date[:4]) # 把前四个字节的数据解包出来
if int(oprator_code) == 5: # 判断数据包是否是error信息包
print('服务器返回:要下载的文件不存在')
break
# 如果是文件内容数据包,需要保存文件内容
f.write(recv_date[4:])
if len(recv_date) < 516: # 意味着服务器传送过来的文件已经接受完成了
print('客户端下载成功')
break
# 客户端收到数据包之后还需要发送一个确认ACK给服务器
ack_package = struct.pack('!HH', 4, num)
s.sendto(ack_package, (server_ip, server_port))
# 释放资源
f.close()
def upload(file_Name, s, host_port):
flag = True
num = 0
f = None
try:
f = open(file_Name, 'rb')
except:
print('文件不存在')
return
# 创建请求数据包
data_package = struct.pack(f'!H{len(file_Name)}sb5sb', 2, file_Name.encode('utf-8'), 0, 'octet'.encode('utf-8'), 0)
# 向服务器发送数据包
s.sendto(data_package, host_port)
# 接收服务器传回来的ACK
data_ack, (service_ip, service_port) = s.recvfrom(1024)
oprator_data, ack_num = struct.unpack('!HH', data_ack)
print(f"服务器返回的ack确认信息是{oprator_data},{ack_num}")
if int(oprator_data) != 4 or ack_num < 0:
flag = False
while flag:
# 读取文件中的数据,每次512字节
read_data = f.read(512)
# 创建文件数据包
new_package = struct.pack('!HH', 3, num) + read_data
# 发送数据包
s.sendto(new_package, (service_ip, service_port))
# 接收服务器返回的ack数据
recv_ack = s.recvfrom(1024) # 里面有数据包内容,还有服务端ip和端口
oprator_code, ack_num = struct.unpack('!HH', recv_ack[0])
print('服务端的确认信息是', ack_num)
if int(oprator_code) != 4 or int(ack_num) < 0: # 不正常的ack确认信息
break
# 判断是不是最后一次传输数据
if len(read_data) < 512:
print(f'客户端文件{file_Name},上传完成')
break
else:
num += 1
if f:
f.close()
if __name__ == '__main__':
file_Name = input("请输入文件名:")
while True:
try:
code = int(input("上传还是下载?(1:下载;2:上传)"))
except:
print('请输入1或2!')
else:
break
# 客户端套接字
s = socket(AF_INET, SOCK_DGRAM)
# 定义服务器地址和端口号
host_port = ('192.168.199.103', 69)
if code == 1:
download(file_Name, s, host_port)
elif code == 2:
upload(file_Name, s, host_port)
s.close()
使用TCP模拟QQ聊天
注意点:
- tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
- tcp客户端一般不绑定,因为是主动连接服务器,所以只要确定好服务器的ip,端口就好,本地客户端可以随机
- tcp服务器中通过listen可以将socket创建出来的主动套接字,变为被动的,这是做tcp服务器时必须要做的
- 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen关闭后的套接字是被动套接字,用来接收新的客户端连接请求的,而accept返回的是新套接字,用来标记这个新客户端的
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能连接服务器,但是之前已经连接成功的客户端可以正常通信
- 关闭accept返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用close后,服务器端会recv解阻塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
服务器端
- 创建socket(AF_INET,SOCK_STREAM)
- 绑定ip和端口号bind(’’,8008)
- 启动监听listen(5)
- 接收客户端发来的连接请求accept(),并返回一个新的socket和客户端ip、端口元组
- 接收客户端发来的数据
- 对数据进行处理,发送数据到客户端
- 退出新的socket
- 退出服务器socket
from socket import *
server_socket = socket(AF_INET,SOCK_STREAM)
server_socket.bind(('',8008))
server_socket.listen(1)
while True:
new_socket,client_ip_port = server_socket.accept()
while True:
recv_data = new_socket.recv(1024)
if len(recv_data) > 0:#客户端没有退出,而且发送数据到服务器了
print('客户端:',recv_data.decode('utf-8'))
else:
print('客户端已经退出!')
break
if recv_data == 'exit':
print('客户端已经退出!')
break
# 发送数据给客户端
send_data = input('send:')
if len(send_data) > 0:
new_socket.send(send_data.encode('utf-8'))
new_socket.close()
server_socket.close()
客户端
- 创建socket(AF_INET,SOCK_STREAM)
- 发送连接请求给服务器端connect((ip,port))
- 发送数据给服务器端send()
- 接收服务器返回的数据,并进行处理,使用recv()
- 退出关闭socket
from socket import *
client_socket = socket(AF_INET,SOCK_STREAM)
client_socket.connect(('192.168.199.103',8008))
while True:
send_data = input('send:')
if len(send_data) > 0:
client_socket.send(send_data.encode('utf-8'))
else:
break
if send_data == 'exit':
client_socket.close()
break
# 客户端接收服务器返回的内容
recv_data = client_socket.recv(1024)
print('服务器:',recv_data.decode('utf-8'))
client_socket.close()
黏包问题
黏包指的是数据和数据之间没有明确的分界线,导致不能正确读取数据
- UDP协议的数据传输不存在黏包问题,应为UDP协议是将数据打包,然后将包发送给接收方,数据的顺序不会乱。但是,当数据包太大时,接收方规定接收数据比较小时,会造成数据丢失
- TCP协议会出现黏包问题:数据与数据之间没有明显的界限
- 发送方出现黏包:当数据较小,发送时间间隔比较短时,会出现接收方将两条或两条以上的数据拼接到一起,造成黏包
- 接收方出现黏包:当数据较小时,接收方会将多个包的数据合并到一个包中;当数据较大时,接收方会将接收完后,没有接收的数据合并到下一次接收的数据中
发送方出现黏包
服务器端
# author:ycw
# date: 2020/10/23 11:31
from socket import *
server_socket = socket(AF_INET,SOCK_STREAM)
server_socket.bind(('',8080))
server_socket.listen(5)
new_socket,client_addr = server_socket.accept()
recv_data1 = new_socket.recv(1024)
recv_data2 = new_socket.recv(1024)
print('第一条数据:',recv_data1,'第二条数据:',recv_data2)
new_socket.close()
server_socket.close()
客户端
from socket import *
client_socket = socket(AF_INET,SOCK_STREAM)
client_socket.connect(('192.168.199.103',8080))
client_socket.send('hello'.encode('utf-8'))
client_socket.send('word'.encode('utf-8'))
client_socket.close()
接收方出现黏包
服务器端
# author:ycw
# date: 2020/10/23 11:47
#接收端黏包问题
from socket import *
import time
server_socket = socket(AF_INET,SOCK_STREAM)
server_socket.bind(('',8080))
server_socket.listen(5)
new_socket,client_addr = server_socket.accept()
print('连接成功',client_addr)
data1 = new_socket.recv(2) #第一次没有接收完整
print('第一条数据:',data1)
time.sleep(6)
data2 = new_socket.recv(10)# 第二次会接收旧数据(第一次没有接收完的数据),如果还有空间,再接收新书
print('第二条数据:',data2)
new_socket.close()
server_socket.close()
客户端
# author:ycw
# date: 2020/10/23 11:43
# 接收方可能出现的黏包问题
from socket import *
import time #通过time模块保证客户端发送多个数据包的时候,间隔时间长
client = socket(AF_INET,SOCK_STREAM)
client.connect(('192.168.199.103',8080))
client.send('hello'.encode('utf-8'))
time.sleep(5)#让当前的线程休眠5秒
client.send('world'.encode('utf-8'))
client.close()
黏包成因
- 接收方不知道消息之间的界限,不知道一个消息要提取多少字节的数据造成的。(服务器出现黏包)
- tcp在发送数据少且间隔时间短的数据时,会将几条合并一起发送。(客户端出现黏包)
解决黏包问题
先计算将要发送的数据的长度,发送给接收方,接收方根据这个长度来接收相应的数据
服务器端
# author:ycw
# date: 2020/10/23 12:20
from socket import *
import os
import struct
server = socket(AF_INET,SOCK_STREAM)
server.bind(('',9999))
server.listen(5)
conn,client_addr = server.accept()
f = open('D:\学习内容\软件学习\python网络编程\服务器.flac','wb')
head = conn.recv(4)
size = struct.unpack('!i',head)[0]#unpack返回的都是一个元组,元组的第一个值就是长度
recv_size = 0 # 已经接收到多长的数据
while recv_size<size:
data = conn.recv(1024)
recv_size += len(data) # 接收的字节长度要累加
print(recv_size)
f.write(data)
print('服务器端接收完成')
f.close()
conn.close()
server.close()
客户端
# author:ycw
# date: 2020/10/23 12:10
from socket import *
import struct
import os
client = socket(AF_INET,SOCK_STREAM)
client.connect(('192.168.199.103',9999))
# 客户端传送文件到服务器,new.flac
file_path = 'new.flac'
f = open(file_path,'rb')
#在发送真正的文件数据之前,先准备一个报头
size = os.path.getsize(file_path) # 文件的字节长度.
print(size)
# 创建一个包头,i为4个字节的int,
head = struct.pack('!i',size)#接收方会使用struct解包,得到一个int类型的数字
client.send(head)#发送包头
#发送文件内容
while True:
data = f.read(1024)#每次读1024字节
if not data:
break
client.send(data)#发送给服务器
print('客户端上传文件完成')
f.close()
client.close()