桐花万里python路-高级篇-网络编程
- CS架构
- 客户端 client
- 服务端 server
- 网络协议
- TCP/IP 网络通讯协议 Transmission Control Protocol/Internet Protocol
- 互联网协议分为osi七层或tcp/ip五层或tcp/ip四层
- 应用层,表示层,会话层
- 传输层
- 建立端口到端口的通信
- 端口范围0-65535,0-1023为系统占用端口
- 传输层有两种协议,TCP(可靠传输)和UDP(不可靠传输)
- TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割
- 只要不得到确认,就重新发送数据报,直到得到对方的确认为止
- TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!
- 网络层
- 网络地址,用来区分不同的广播域/子网
- IP协议 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址。ipv4,ipv6
- 子网掩码 表示子网络特征的一个参数,将某个IP地址划分成网络地址和主机地址两部分。网络部分全部为1,主机部分全部为0
- IP地址分类
- A类IP地址:一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”, 地址范围从1.0.0.0 到126.0.0.0。可用的A类网络有126个,每个网络能容纳1亿多个主机。
- B类IP地址:一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机 。
- C类IP地址:一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。
- D类地址用于多点广播(Multicast): D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。
- E类IP地址 以“llll0”开始,为将来使用保留
- IP报文
- 所有的TCP,UDP,IMCP,IGCP的数据都以IP数据格式传输
- TCP是一个可靠的协议,而UDP就没有那么可靠的区别
- 协议头
- ARP协议 广播的方式发送数据包,获取目标主机的mac地址
- 网络接口层:数据链路层
- 定义了电信号的分组方式
- 以太网协议ethernet
- 一组电信号构成一个数据包,叫做‘帧’
- 每一数据帧分成:报头head和数据data两部分
- head固定18个字节
- 发送者 / 源地址 6字节
- 接受者 / 目标地址 6字节
- 数据类型 6字节
- data 数据包的具体内容 最短46字节,最长1500字节
- mac地址
- 广播发送方式 同一网络内的两台主机,通过arp协议获取另外一台主机的mac地址
- 网络接口层:物理层
- 主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
- Socket 是一组应用层与TCP/IP协议族通信的中间软件抽象层的接口。通过代码封装了tcp/ip协议层的各种数据封装、数据发送、接收等
-
- 工作模式
- 套接字方法
- 语法规范
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
- family
- socket.AF_UNIX 本机进程间通讯
- socket.AF_INET AF_INET6被用于ipv6
- type
- socket.SOCK_STREAM #for tcp
- socket.SOCK_DGRAM #for udp
- socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文
- socket.SOCK_RDM #是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
- 服务端方法
- s.bind() 绑定(主机,端口号)到套接字
- s.listen() 开始TCP监听
- s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
- 客户端方法
- s.connect() 主动初始化TCP服务器连接
- s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
- 公用方法
- s.recv() 接收数据
- s.send() 发送数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完,可后面通过实例解释)
- s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
- s.recvfrom() Receive data from the socket. The return value is a pair (bytes, address)
- s.getpeername() 连接到当前套接字的远端的地址
- s.close() 关闭套接字
- socket.setblocking(flag) #True or False,设置socket为非阻塞模式,以后讲io异步时会用
- socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息,例子 socket.getaddrinfo('luffycity.com',80)
- socket.getfqdn() 拿到本机的主机名
- socket.gethostbyname() 通过域名解析ip地址
- 语法规范
-
- 粘包 只存在于TCP中,socket缓冲区导致的,多次发送的数据包粘到一起
- 接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
- 解决方案
- 服务端在处理数据的时候
- 先将数据总长计算出
- 先整理成报头字典,包含内容长度
- 发送报文长度
- 再发送报头
- 最后发送具体数据
import struct import json import socket import subprocess import settings serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serv.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) serv.bind((settings.HOST, settings.TCPPORT)) serv.listen(5) print("服务器已启动") while True: conn, addr = serv.accept() print("客户端:",conn) while True: content = conn.recv(1024) ret = content.decode("utf-8") print("recv:%s" % (ret)) res = subprocess.Popen(ret,shell=True,stdout=subprocess.PIPE,stdin=subprocess.PIPE,stderr=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() # stdout = content print("res length", len(stdout)) resp = bytes(json.dumps({'content-length':len(stdout)}),encoding="utf-8") conn.send(struct.pack('i',len(resp))) # 发送报头长度 conn.send(resp) # 发送报头 conn.send(stdout) # 发送具体数据
- 客户端接受数据
- 先接受固定长度的报头数据得到报头长度
- 再按报头长度接受报头,解析报头得到具体内容长度
- 循环接收具体内容到缓冲区,拼接缓冲区数据
- 最后再将接收的数据转码
import struct import json import socket import settings cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) cli.connect((settings.HOST, settings.TCPPORT)) while True: echo = input(":>>").strip() cli.send(echo.encode('utf-8')) if len(echo) == 0: continue if echo == 'q': break head = cli.recv(4) # 接收报头长度 header_bytes = struct.unpack('i', head) # 解出报头的长度 head_info = cli.recv(header_bytes[0]) # 接收报头 header = json.loads(head_info) # 解析报头 print(header) content_length = header.get('content-length') print(content_length, type(content_length)) recv_length = 0 content = b'' # 数据缓存区 while recv_length < content_length: if recv_length >= content_length: break _content = cli.recv(settings.BUFFSIZE) # 循环接收数据 content += _content recv_length += settings.BUFFSIZE # print(content_length, ' = ', len(content), content.decode('gbk')) # 转码接收的数据
- 其他