一, 预备知识
对于我们, 主要掌握 5 层协议就行.
物理层:
转成二进制数序列
数据链路层:
形成统一的协议: Internet 协议
包括数据头 (18 个字节, 前 6 个字节原地址, 中间 6 个字节为目标地址, 后 6 个字节为数据的描述) 和数据
网络层:
有 IP 协议, 包括 IP 头和数据
传输层:
包括 tcp,UDP 两个协议: 基于端口 (0-65535) 的协议
应用层:
包括 http,ftp 协议
TCP 协议: 流式协议, 先把管道修好
客户端 服务端
- C-------------------------------->S
- <--------------------------------
发包:
C 请求, S 同意后并我也要挖隧道, C 才可以挖隧道到 S.(三次握手)
结束发包:
C 请求, S 确认, S 请求, C 确认(四次挥手)
UDP 协议: 传输不可靠, 但不需要建管道, 直接按 IP 发过去
总结:TCP 传输可靠, 但效率低
UDP 传输不可靠, 但效率高
二, 网络编程 SOCKET
语法:
1 socket.socket(socket.AF_INET,socket.SOCK_STREAM)
其中:
socket.AF_UNIX: 用于本机进程间通讯, 为了实现两个进程间的通讯, 可以通过创建一个本地的 socket 来完成(一个机器两个不同的软件).
socket.AF_INET: 我们只关心网络编程, 因此大多使用这个(还有 socket.AF_INET6 被用于 ipv6.)
socket.SOCK_STREAM: 制动使用面向流的 TCP 协议.
socket.SOCK_DGRAM: 指向 UDP 协议.
2.1 socket 套接字
s.recv(1024)接受数据
s.send(1024)发送数据
s.recvfrom()接收所有数据
s.sendall()发送所有数据(本质是循环调用 send)
s.sendto(信息,(IP 地址, 端口号)), 将发给服务端的消息,(IP 地址, 端口号)发给服务端.
s.close()关闭套接字
一个 sendto 对应一个 recvfrom
2.2 TCP
2.2.1 服务端
由上图可知, 服务端需要先建立 SOCKET 链接, 首先需要导入 socket 模块, 并链接.
- import socket
- s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
之后就需要绑定 (主机, 端口号) 到套接字, 开始监听. 其中绑定时, IP 号和端口号是元组, 并且端口号是 0-65535, 但其中 0-1024 是给操作系统的, 使用需要管理员权限. 监听, 其中 5 代表最大链接数量.
- s.bind(('127.0.0.1',8080))#0-65535:0-1024 给操作系统使用
- s.listen(5)
紧接着, 服务器通过一个永久循环来接收来自客户端的连接, accept()会一直等待, 知道客户端发来信息(暂只考虑单线程情况).
- while True:# 链接循环
- conn,client_addr=s.accept()
接下来就是收发消息了, 并需要进行通信循环.
- # 收发消息
- while True:# 通信循环
- try:
- data=conn.recv(1024) #1024 表示接收数据的最大数, 单位是 bytes
- print('客户端的数据',data)
- conn.send(data.upper())
- except ConnectionResetError:
- break
- conn.close()
接下来就是关闭套接字.
1 s.close()
2.2.2 客户端
首先和服务端一样, 需要先建立 SOCKET 链接, 首先需要导入 socket 模块, 并链接.
- import socket
- s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
之后通过 (主机 IP 号, 端口号) 到套接字连接.
1 s.connect(('127.0.0.1',8080))
之后发收消息, 同样有着通信循环, 和服务端相比, 由于没有等待连接, 因此少个链接循环.
- # 发收消息
- while True:# 通信循环
- msg=input('>>').strip()
- phone.send(msg.encode('utf-8'))
- data=phone.recv(1024)
- print(data.decode('utf-8'))
接下来就是关闭套接字.
1 s.close()
2.3 UDP 协议
相比 TCP 协议, UDP 是面向无连接的协议, 因此使用 UDP 协议时, 不需要建立连接, 只需要知道对方的 IP 地址和端口号, 就可以发送数据包, 其不管是否发送到达.
和 TCP 协议类似, 也是服务端和客户端.
2.3.1 服务端
服务端需要先建立 SOCKET 链接, 首先需要导入 socket 模块, 并绑定端口.
- import socket
- server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
- server.bind(('127.0.0.1',8080))
其不需要监听和连接, 即不需要 listen()和 accept(), 而是直接接收来自客户端的数据.
- while True:
- data,cliend_addr=server.recvfrom(1024)
- print(data)
- server.sendto(data.upper(),cliend_addr)
最后关闭套接字.
1 server.close()
2.3.2 客户端
同样, 也需要先建立 SOCKET 链接, 首先需要导入 socket 模块.
- import socket
- client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
但不需要调用 connect(), 直接通过 sendto()给服务端发数据.
- while True:
- msg=input('>>:').strip()
- data=client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
- data,server_addr=client.recvfrom(1024)
- print(data,server_addr)
最后关闭套接字.
1 server.close()
2.4 粘包现象及解决方案
2.4.1 粘包现象
何为粘包, 在上文中, 我们一直使用 s.recv(1024)来接收数据, 但如果需要接收的数据比 1024 长, 那么剩余的数据会在发送端的 IO 缓冲区暂存下来, 等下次接收端来接收数据时, 先将缓冲区的数据发送出去, 再接收下次的数据. 当然, 我们可以将 1024 改为 8192, 但数据比这个还大呢, 我们接收的额定值就不能变大了, 还是会发生这样的事件. 因此, 这样的事件我们称之为粘包现象. 当然, 粘包现象仅存在于 TCP 协议中, UDP 协议中不存在.
2.4.2 解决方案
粘包问题的根源在于, 接收端不知道发送端将要传送的字节流的长度, 所以解决粘包的方法就是围绕, 如何让发送端在发送数据前, 把自己将要发送的字节流总大小让接收端知晓, 然后接收端来一个死循环接收完所有数据. 此处, 我们就需要借助于第三方模块 struct. 用法为:
- import json,struct
- #为避免粘包, 必须制作固定长度的报头
- header_dic={'file_size':1073741824,'file_name':'a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1G 文件大小, 文件名和 md5 值
- #为了该报头能传送, 需要序列化并且转为 bytes, 用于传输
- header_json = json.dumps(header_dic) # 转成字符串类型
- header_bytes = header_json.encode('utf-8')
- #为了让客户端知道报头的长度, 用 struck 将报头长度这个数字转成固定长度: 4 个字节
- head_len_bytes=struct.pack('i',len(head_bytes)) #这 4 个字节里只包含了一个数字, 该数字是报头的长度
- #客户端开始发送报文长度
- conn.send(head_len_bytes) #先发报头的长度, 4 个 bytes
- #再发报头的字节格式
- conn.send(head_bytes)
- #然后发真实内容的字节格式
- conn.sendall(文件内容)
- #服务端开始接收
- head_len_bytes=s.recv(4) #先收报头 4 个 bytes, 得到报头长度的字节格式
- x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
- header_bytes=s.recv(x) #按照报头长度 x, 收取报头的 bytes 格式
- header_str=header_bytes.decode('utf-8')
- header_dic=json.loads(header_str) #提取报头
- #最后根据报头的内容提取真实的数据, 比如数据的长度
- real_data_len=s.recv(header_dic['file_size'])
- s.recv(real_data_len)
因此对于一个文件传输:
服务端:
- import socket
- import os
- import struct
- import json
- share_dir=r'C:\Users\...\Desktop\python\oldboypython\day6\10 文件传输 \ 服务端 \ share'
- phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
- phone.bind(('127.0.0.1',9901)) #0-65535:0-1024 给操作系统使用
- phone.listen(5)
- print('starting...')
- while True: # 链接循环
- conn,client_addr=phone.accept()
- print(client_addr)
- while True: #通信循环
- try:
- #1, 收命令
- res=conn.recv(8096)#b'get a.txt'
- if not res:break #适用于 linux 操作系统
- #2, 解析命令, 提取相应的命令参数
- cmds=res.decode('utf-8').split()#['get','a.txt']
- filename=cmds[1]
- #3, 以读的方式打开文件, 读取文件内容发送给客户端
- #3.1 制作固定长度的报头
- header_dic={
- 'filename':filename,
- 'md5':'xxdxxx',
- 'file_size':os.path.getsize('%s/%s'%(share_dir,filename))
- }
- header_json=json.dumps(header_dic)# 转成字符串类型
- header_bytes=header_json.encode('utf-8')
- #3.2 先发送报头的长度
- conn.send(struct.pack('i',len(header_bytes)))
- #3.3 再发报头
- conn.send(header_bytes)
- #3.4 发真实的数据
- # conn.send(stdout+stderr) #+ 是一个可以优化的点
- with open('%s/%s'%(share_dir,filename),'rb') as f:
- # conn.send(f.read())
- for line in f:
- conn.send(line)
- except ConnectionResetError: #适用于 windows 操作系统
- break
- conn.close()
- phone.close()
文件传输服务端
客户端:
- import socket
- import struct
- import json
- download_dir=r'C:\Users\...\Desktop\python\oldboypython\day6\10 文件传输 \ 客户端 \ download'
- phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- phone.connect(('127.0.0.1',9901))
- while True:
- #1, 发命令
- cmd=input('>>:').strip() #get a.txt
- if not cmd:continue
- phone.send(cmd.encode('utf-8'))
- #2, 接收文件的内容, 以写的方式打开新文件, 接收服务端发来的文件的内容写入客户端的新文件
- #2.1 先收报头的长度
- obj=phone.recv(4)
- header_size=struct.unpack('i',obj)[0]
- #2.2 在收报头
- header_bytes=phone.recv(header_size)
- #2.3 从包头中解析出对真实数据的描述的信息
- header_json=header_bytes.decode('utf-8')
- header_dic=json.loads(header_json)
- print(header_dic)
- total_size=header_dic['file_size']
- file_name=header_dic['filename']
- #2.4 接收数据
- with open('%s/%s'%(download_dir,file_name),'wb') as f:
- recv_size=0
- while recv_size<total_size:
- line=phone.recv(1024)
- f.write(line)
- recv_size+=len(line)
- print('总大小:%s, 已下载大小:%s'%(total_size,recv_size))
- phone.close()
文件传输客户端
来源: https://www.cnblogs.com/Umay-wm/p/9139299.html