文章目录
- wireshark 抓包工具的快速入门
- TCP/IP 协议详解
- TCP/IP 协议概述
- 部分协议补充
- OSI 参考模型及 TCP/IP 参考模型
- 以太网帧格式
- ARP 数据报格式
- IP 段格式
- ICMP 协议
- TCP协议
- UDP 协议
- MTU和MSS
- 补充
- 产品模式【C/S B/S】
- C/S 模式
- B/S 模式
- DDOS简单介绍(SYN Flooding)
- SYN Flood 攻击防护手段
- epoll 的使用(多路复用技术)
- 相关用法
- 即时聊天案例
- 聊天室
- 非阻塞编程(VS多路复用技术来说用处不是很大,了解)
- 补充
- 持续发送多个文件,协议设计
- Python 发送整型数/浮点数
- 多个文件传输的例子,自行加for
- 补充
wireshark 抓包工具的快速入门
wireshark 是捕获机器上的某一块网卡的网络包,当你的机器上有多块网卡的时候,你需要选择一个网卡。点击 Caputre->Interfaces… 出现下面对话框,选择正确的网卡。然后点击"Start"按钮, 开始抓包。
WireShark 主要分为这几个界面
- Display Filter(显示过滤器), 用于过滤
- Packet List Pane(封包列表), 显示捕获到的封包, 有源地址和目标地址,端口号。
- Packet Details Pane(封包详细信息), 显示封包中的字段
- Dissector Pane(16 进制数据)
- Miscellanous(地址栏,杂项)
停止捕获之后,我们可以通过左下角的这堆数据,进而分析
Frame: 物理层的数据帧概况
Ethernet II: 数据链路层以太网帧头部信息
Internet Protocol Version 4: 互联网层 IP 包头部信息
Transmission Control Protocol: 传输层 T 的数据段头部信息,大概率是是 TCP
Hypertext Transfer Protocol: 应用层的信息,大概率是 HTTP 协议
TCP/IP 协议详解
发送一句话的流程
TCP/IP 协议概述
tcp/ip 模型 4 层:
四层模型 | 包含 |
---|---|
应用层 | http 超文本传输协议 ftp 文件传输协议 telnet 远程登录 ssh 安全外壳协议 smtp 简单邮件发送 pop3 收邮件 |
传输层 | tcp 传输控制协议,udp 用户数据包协议 |
网络层 | ip 网际互联协议 icmp 网络控制消息协议 igmp 网络组管理协议 |
网络接口层 | arp 地址转换协议,rarp 反向地址转换协议,mpls 多协议标签 |
部分协议补充
- ARP:(地址转换协议)用于获得同一物理网络中的硬件主机地址。是设备通过自己知道的 IP 地址来获得自己不知道的物理地址的协议。
- RARP:反向地址转换协议(RARP)允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。网络管理员在局域网网关路由器里创建一个表以映射物理地址(MAC)和与其对应的 IP 地址。当设置一台新的机器时,其 RARP 客户机程序需要向路由器上的 RARP 服务器请求相应的 IP 地址。假设在路由表中已经设置了一个记录,RARP 服务器将会返回 IP 地址给机器,此机器就会存储起来以便日后使用。 RARP 可以使用于以太网、光纤分布式数据接口及令牌环 LAN。
- IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。
- ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。Ping 命令所使用的协议
- IGMP:(网络组管理协议)被 IP 主机用来向本地多路广播路由器报告主机组成员的协议。主机与本地路由器之间使用 Internet 组管理协议(IGMP,Internet Group Management Protocol)来进行组播组成员信息的交互。
- TCP:(传输控制协议)为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。
- UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。
OSI 参考模型及 TCP/IP 参考模型
TCP/IP 协议族的每一层的作用:
- 网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。
- 网络层:负责将数据帧封装成 IP 数据报,并运行必要的路由算法。
- 传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。
- 应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进
程。
以太网帧格式
其中的源地址和目的地址是指网卡的硬件地址(也叫 MAC 地址),长度是 48bit,是在网卡出厂时固化的。可在 shell 中使用 ifconfig 命令查看,“HWaddr00:15:F2:14:9E:3F”部分就是硬件地址。协议字段有三种值,分别对应 IP、ARP、RARP。帧尾是 CRC 校验码。
以太网帧中的数据长度规定最小 46 字节,最大 1500 字节,ARP 和 RARP 数据包的长度不够 46 字节,要在后面补填充位。最大值 1500 称为以太网的最大传输单元(MTU),不同的网络类型有不同的 MTU,如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的 MTU,则需要对数据包进行分片。ifconfig 命令输出中也有“MTU:1500”。注意,MTU 这个概念指数据帧中有效载荷的最大长度,不包括帧头长度。
ARP 数据报格式
在网络通讯时,源主机的应用程序知道目的主机的 IP 地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP 协议就起到这个作用。源主机发出 ARP 请求,询问“IP 地址是 192.168.0.1 的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填 FF:FF:FF:FF:FF:FF 表示广播),目的主机接收到广播的ARP 请求,发现其中的 IP 地址与本机相符,则发送一个 ARP 应答数据包给源主机,将自己的硬件地址填写在应答包中。
每台主机都维护一个 ARP 缓存表,可以用 arp -a 命令查看。缓存表中的表项有过期时间(一般为 20 分钟),如果 20 分钟内没有再次使用某个表项,则该表项失效,下次还要发 ARP 请求来获得目的主机的硬件地址。
硬件类型指链路层网络类型,1 为以太网,协议类型指要转换的地址类型,
0x0800 为 IP 地址,后面两个地址长度对于以太网地址和 IP 地址分别为 6 和 4
(字节),==op 字段为 1 表示 ARP 请求,op字段为 2 表示 ARP 应答 ==
样例分析:
Ethernet II, Src: Tp-LinkT_37:4f:8c (f4:83:cd:37:4f:8c), Dst: Broadcast (ff:ff:ff:ff:ff:ff)Destination: Broadcast (ff:ff:ff:ff:ff:ff)Source: Tp-LinkT_37:4f:8c (f4:83:cd:37:4f:8c)Type: ARP (0x0806)
Address Resolution Protocol (request)Hardware type: Ethernet (1)Protocol type: IPv4 (0x0800)Hardware size: 6Protocol size: 4Opcode: request (1)Sender MAC address: Tp-LinkT_37:4f:8c (f4:83:cd:37:4f:8c)Sender IP address: 192.168.0.1Target MAC address: 00:00:00_00:00:00 (00:00:00:00:00:00)Target IP address: 192.168.0.108
IP 段格式
报文头5字节
- 版本号:长度 4 bit。标识目前采用的 IP 协议的版本号。一般的值为 0100(IPv4),0110(IPv6)
- 首部长度:长度 4 比特。这个字段的作用是为了描述 IP 包头的长度,因为在 IP 包头中有变长的可选部分。该部分占 4 个 bit 位,单位为32bit(4 个字节),即本区域值= IP 头部长度(单位为 bit)/(84),因此,一个IP 包头的长度最长为“1111”,即 154=60 个字节。IP 包头最小长度为 20 字节。
- 服务类型(TOS):长度 8 比特。8 位 按位被如下定义 PPP DTRC0
PPP:定义包的优先级,取值越大数据越重要
000 普通 (Routine)
001 优先的 (Priority)
010 立即的发送 (Immediate)
011 闪电式的 (Flash)
100 比闪电还闪电式的 (Flash Override)
101 CRI/TIC/ECP(找不到这个词的翻译)
110 网间控制 (Internetwork Control)
111 网络控制 (Network Control)
D 时延: 0:普通 1:延迟尽量小
T 吞吐量: 0:普通 1:流量尽量大
R 可靠性: 0:普通 1:可靠性尽量大
M 传输成本: 0:普通 1:成本尽量小
0 最后一位被保留,恒定为 0 - IP 包总长(Total Length):长度 16 比特。 以字节为单位计算的 IP 包的长度(包括头部和数据),所以 IP 包最大长度 65535 字节。
- 标识符(Identifier):长度 16 比特。该字段和 Flags 和 Fragment Offest 字段联合使用,对较大的上层数据包进行分段(fragment)操作。路由器将一个包拆分后,所有拆分开的小包被标记相同的值,以便目的端设备能够区分哪个包属于被拆分开的包的一部分。
- 标记(Flags):长度 3 比特。该字段第一位不使用。第二位是 DF(Don’tFragment)位,DF 位设为 1 时表明路由器不能对该上层数据包分段。如果一个上层数据包无法在不分段的情况下进行转发,则路由器会丢弃该上层数据包并返回一个错误信息。第三位是 MF(More Fragments)位,当路由器对一个上层数据包分段,则路由器会在除了最后一个分段的 IP 包的包头中将 MF 位设为 1。
- 片偏移(Fragment Offset):长度 13 比特。表示该 IP 包在该组分片包中位置,接收端靠此来组装还原 IP 包。片偏移量,13 位,指出较长的分组在分片后,某段在原分组的相对位置。也就是说相对原分组数据段的起点,该片从何处开始。段偏移以 8 字节为偏移单位。这就是,每个分片的长度一定是 8 字节(64 位)的整数倍
- 生存时间(TTL):长度 8 比特。当 IP 包进行传送时,先会对该字段赋予某个特定的值。当 IP 包经过每一个沿途的路由器的时候,每个沿途的路由器会将 IP 包的 TTL 值减少 1。如果 TTL 减少为 0,则该 IP 包会被丢弃。这个字段可以防止由于路由环路而导致 IP 包在网络中不停被转发。
- 协议(Protocol):长度 8 比特。标识了上层所使用的协议。(1 ICMP 2 IGMP 6 TCP 17 UDP 88 IGRP 89 OSPF)
- 校验(Header Checksum):长度 16 位。用来做 IP 头部的正确性检测,但不包含数据部分。 因为每个路由器要改变 TTL 的值,所以路由器会为每个通过的数据包重新计算这个值。
- 起源和目标地址(Source and Destination Addresses):这两个地段都是 32 比特。标识了这个 IP 包的起源和目标地址。要注意除非使用 NAT,否则整个传输的过程中,这两个地址不会改变。至此,IP 包头基本的 20 字节已介绍完毕,此后部分属于可选项,不是必须的部分。
- 可选项(Options):这是一个可变长的字段。该字段属于可选项,主要用于测试,由起源设备根据需要改写。暂时不介绍。
ICMP 协议
ICMP 是Internet 控制报文协议。它是TCP/IP 协议族的一个子协议,用于在 IP 主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。
我们在网络中经常会使用到 ICMP 协议,比如我们经常使用的用于检查网络通不通的 Ping 命令(Linux 和 Windows 中均有),这个“Ping”的过程实际上就是ICMP 协议工作的过程。还有其他的网络命令如跟踪路由的 Tracert 命令(windows)、traceroute(linux)也是基于ICMP 协议的。
TCP协议
报文头5字节
Transmission Control Protocol, Src Port: 14660, Dst Port: 443, Seq: 1, Ack: 1, Len: 279Source Port: 14660Destination Port: 443[Stream index: 31][Conversation completeness: Complete, WITH_DATA (63)][TCP Segment Len: 279]Sequence Number: 1 (relative sequence number)Sequence Number (raw): 466040739[Next Sequence Number: 280 (relative sequence number)]Acknowledgment Number: 1 (relative ack number)Acknowledgment number (raw): 14604059820101 .... = Header Length: 20 bytes (5)Flags: 0x018 (PSH, ACK)000. .... .... = Reserved: Not set...0 .... .... = Accurate ECN: Not set.... 0... .... = Congestion Window Reduced: Not set.... .0.. .... = ECN-Echo: Not set.... ..0. .... = Urgent: Not set.... ...1 .... = Acknowledgment: Set.... .... 1... = Push: Set.... .... .0.. = Reset: Not set.... .... ..0. = Syn: Not set.... .... ...0 = Fin: Not set[TCP Flags: ·······AP···]Window: 514[Calculated window size: 131584][Window size scaling factor: 256]Checksum: 0x865b [unverified][Checksum Status: Unverified]Urgent Pointer: 0[Timestamps][Time since first frame in this TCP stream: 0.038470000 seconds][Time since previous frame in this TCP stream: 0.001132000 seconds][SEQ/ACK analysis][iRTT: 0.037338000 seconds][Bytes in flight: 279][Bytes sent since last PSH flag: 279]TCP payload (279 bytes)
- 源端口、目的端口:16 位长。标识出远端和本地的端口号。
- 序号:32 位长。标识发送的数据报的顺序。
- 确认号:32 位长。希望收到的下一个数据报的序列号。
- TCP 头长:4 位长。表明 TCP 头中包含多少个 32 位字。就是有多少个 4 个字节
- 4位未用。
- 12位表示状态,按顺序分别是:
CWR: 拥塞窗口减(发送方降低它的发送速率)
ECE: ECN 回显(发送方收到了一个更早的拥塞报告)
URG:紧急指针(urgent pointer)有效,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号
ACK:ACK 位置 1 表明确认号是合法的。如果 ACK 为 0,那么数据报不包含确认信息,确认字段被省略。
PSH:表示是带有 PUSH 标志的数据。接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才发送。当 PSH=1 时,则报文段会被尽快地交付给目的方,不会对这样的报文段使用缓存策略
RST:用于复位由于主机崩溃或其他原因而出现的错误的连接。还可以用于拒绝非法的数据报或拒绝连接请求。当 RST 为 1 时,表明 TCP 连接中出现了严重的差错,必须释放连接,然后再重新建立连接。
SYN:用于建立连接。当 SYN=1 时,表示发起一个连接请求。
FIN:用于释放连接。当 FIN=1 时,表明此报文段的发送端的数据已发送完
成,并要求释放连接。 - 窗口大小:16 位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。此字段用来进行流量控制。单位为字节数,这个值是本机期望一次接收的字节数
- 校验和:16 位长。是为了确保高可靠性而设置的。它校验头部、数据和伪 TCP头部之和。
- 可选项:0 个或多个 32 位字。包括最大 TCP 载荷,窗口比例、选择重复数据报等选项。
所以对于tcp来说他发一个包,当中最多能有:1500-20(ip中的5字节报头)-20(tcp当中的5字节报头)=1460个数据位
UDP 协议
2字节报头
- 源、目标端口号字段:占 16 比特。作用与 TCP 数据段中的端口号字段相同,用来标识源端和目标端的应用进程。
- 长度字段:占 16 比特。标明 UDP 头部和 UDP 数据的总长度字节。数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为 65535字节。不过,一些实际应用往往会限制数据报的大小,有时会降低到 8192 字节。
- 校验和字段:占 16 比特。用来对 UDP 头部和 UDP 数据进行校验。和 TCP 不同的是,对 UDP 来说,此字段是可选项,而 TCP 数据段中的校验和字段是必须有的
相同的对于udp来说他发一个包,当中最多能有:1500(mtu)-20(ip中的5字节报头)-8(udp当中的字节报头)=1472个数据位
MTU和MSS
MTU 是网络传输最大报文包,MSS 是网络传输数据最大值。
- mss 加包头数据就等于 mtu. 简单说拿 TCP 包做例子。 报文传输 1400 字节的数据的话,那么 mss 就是 1400,再加上 20 字节 IP 包头,20 字节 tcp 包头,那么 mtu 就是 1400+20+20. 当然传输的时候其他的协议还要加些包头在前面,总之mtu 就是总的最后发出去的报文大小。mss 就是你需要发出去的数据大小。
- MSS: MSS 就是 TCP 数据包每次能够传输的最大数据分段。
- 为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS值,这个值 TCP 协议在实现的时候往往用 MTU 值代替(需要减去 IP 数据包包头的大小 20Bytes 和 TCP 数据段的包头 20Bytes)所以往往 MSS 为 1460。通讯双方会根据双方提供的 MSS 值得最小值确定为这次连接的最大 MSS 值.
补充
TCP 和 UDP,一台机器向另外一台机器发送了 3 个包,对方可能收到几个包?
TCP >=3 3 个到任意多个 实际上就是超时重传导致的
UDP 0 到 3 个
产品模式【C/S B/S】
C/S 模式
传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通
讯两端各自部署客户机和服务器来完成数据通信。
B/S 模式
浏览器()/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台
PC 都默认配置的浏览器即可完成数据的传输。
DDOS简单介绍(SYN Flooding)
在探讨 SYN 攻击之前,我们先看看 linux 内核对 SYN 是怎么处理的: 1. Server 接收到 SYN 连接请求。 内部维护一个队列(我们暂称之半连接队列,半连接并不准确), 发送 ack 及 syn 给 Client 端,等待 Client 端的 ack 应答,接收到则完成三次握手建立连接。 如果接收不到 ack 应答,则根据延时重传规则继续发送 ack 及 syn 给客户端。
利用上述特点。我们构造网络包,源地址随机构建,意味着当 Server 接到SYN 包时,应答 ack 和 syn 时不会得到回应。在这种情况下, Server 端,内核就会维持一个很大的队列来管理这些半连接。 当半连接足够多的时候,就会导致新来的正常连接请求得不到响应, 也就是所谓的 DOS 攻击。
SYN Flood 攻击防护手段
- tcp_max_syn_backlog: 半连接队列长度
- tcp_synack_retries: syn+ack 的重传次数
- tcp_syncookies : syn dookie
上面的三个路径在linux服务器下的/proc/sys/net/ipv4下
一般的防御措施就是就是减小 SYN+ACK 重传次数,增加半连接队列长度,启用 syn cookie。不过在高强度攻击面前,调优 tcp_syn_retries 和tcp_max_syn_backlog 并不能解决根本问题,更有效的防御手段是激活 tcp_syncookies,在连接真正创建起来之前,它并不会立刻给请求分配数据区存储连接状态,而是通过构建一个带签名的序号来屏蔽伪造请求。
epoll 的使用(多路复用技术)
通常来说,实现处理tcp请求,为一个连接一个线程,在高并发的场景,这种多线程模型与Epoll相比就显得相形见绌了。epoll是linux2.6内核的一个新的系统调用,epoll在设计之初,就是为了替代select, poll线性复杂度的模型,epoll的时间复杂度为O(1), 也就意味着,epoll在高并发场景,随着文件描述符的增长,有良好的可扩展性。
相关用法
import select 导入 select 模块
epoll = select.epoll() 创建一个 epoll 对象
epoll.register(文件句柄,事件类型) 注册要监控的文件句柄和事件事件类型:select.EPOLLIN 可读事件select.EPOLLOUT 可写事件select.EPOLLERR 错误事件select.EPOLLHUP 客户端断开事件
epoll.unregister(文件句柄) 销毁文件句柄
epoll.poll(timeout) 当文件句柄发生变化,则会以列表的形式主动报告给用户进程,timeout为超时时间,默认为-1,即一直等待直到文件句柄发生变化,如果指定为 1,那么 epoll 每 1 秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
epoll.fileno() 返回 epoll 的控制文件描述符(Return the epoll control file descriptor)
epoll.modfiy(fineno,event) fineno 为文件描述符 event 为事件类型 作用是修改文件描述符所对应的事件
epoll.fromfd(fileno) 从 1 个指定的文件描述符创建 1 个 epoll 对象
epoll.close() 关闭 epoll 对象的控制文件描述符
即时聊天案例
服务端
import socket
import sys
import selectdef tcp_server():# 创建一个tcp的socket对象server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)# 绑定if len(sys.argv) == 2:server.bind((sys.argv[1],2000))else:server.close()return# 监听server.listen(128)# accepts_client,s_arr = server.accept()# 构建epoll对象s_epoll = select.epoll()# 进行监听s_epoll.register(s_client.fileno(),select.EPOLLIN) # 监听s_client是否接收到消息,并转化为可读s_epoll.register(sys.stdin.fileno(),select.EPOLLIN)# 监听标准缓冲区stdin是否接收到消息,并转化为可读# while进行聊天while True:# 开始进行监听 -1代表永久时长events = s_epoll.poll(-1)for fd,event in events:if fd == s_client.fileno():data = s_client.recv(1000)# TCP 对方断开时,内核把 socket 对象对应的描述符标记为可读状态,epoll 就会检测到,这时候 recv 读时,读到到的内容为空,通过这个来判断,进行断开if data:print(data.decode(utf8))else:print('对方断开连接')s_epoll.close()breakelif fd == sys.stdin.fileno():data = input()s_client.send(data.encode(utf8))s_client.close()server.close()if __name__ == '__main__':tcp_server()
客户端
import socket
import sys
import selectdef tcp_client():# 创建一个tcp的socket对象client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)# 绑定if len(sys.argv) == 2:client.connect((sys.argv[1],2000))else:client.close()return# 构建epoll对象c_epoll = select.epoll()# 进行监听c_epoll.register(client.fileno(),select.EPOLLIN) # 监听s_client是否接收到消息,并转化为可读c_epoll.register(sys.stdin.fileno(),select.EPOLLIN)# 监听标准缓冲区stdin是否接收到消息,并转化为可读# while进行聊天while True:# 开始进行监听 -1代表永久时长events = c_epoll.poll(-1)for fd,event in events:if fd == client.fileno():data = client.recv(1000)# TCP 对方断开时,内核把 socket 对象对应的描述符标记为可读状态,epoll 就会检测到,这时候 recv 读时,读到到的内容为空,通过这个来判断,进行断开if data:print(data.decode(utf8))else:print('对方断开连接')c_epoll.close()breakelif fd == sys.stdin.fileno():data = input()client.send(data.encode(utf8))client.close()if __name__ == '__main__':tcp_client()
聊天室
客户端
import select
import socket
import sysdef tcp_client():"""客户端:return:None"""client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)if len(sys.argv) == 2:client.connect((sys.argv[1],2000))else:client.connect(('',2000))epoll = select.epoll()epoll.register(client.fileno(),select.EPOLLIN)epoll.register(sys.stdin.fileno(),select.EPOLLIN)while True:events = epoll.poll(-1)for fd, event in events:if fd == client.fileno():data = client.recv(100)if data:print(data.decode('utf8'))else:print('对方断开了')returnelif fd == sys.stdin.fileno():data = input()client.send(data.encode('utf8'))client.close()if __name__ == '__main__':tcp_client()
服务端:
import socket
import select
import sysdef tcp_server():"""聊天室的中转服务器:return:None"""server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)if len(sys.argv) == 2:server.bind((sys.argv[1],2000))else:server.bind(('',2000))server.listen(128)epoll = select.epoll()# 让epoll监控server对象,如果传来消息,就创建相对应的连接epoll.register(server.fileno(),select.EPOLLIN)total_client = {}while True:events = epoll.poll(-1)for fd,event in events:if fd == server.fileno():client,client_arr = server.accept()# client = Client(fd,s_client,s_client_arr)# 值得关注的室这里total_client[client.fileno()],自己写错了成了total_client[fd]卡了很久total_client[client.fileno()] = tuple(client,client_arr)print(str(client_arr) + "加入群聊")epoll.register(client.fileno(),select.EPOLLIN)else:remove_client = Nonedata = total_client[fd][0].recv(1000)str_data = str(total_client[fd][1]) + ': ' + data.decode()if data:for other_fd in total_client:if other_fd != fd:total_client[other_fd][0].send(str_data.encode())else:remove_client = total_client[fd]if remove_client:print(remove_client[1]+"离开群聊")total_client.pop(remove_client[0].fileno())epoll.unregister(remove_client[0].fileno(), select.EPOLLIN)remove_client[0].close()server.close()
非阻塞编程(VS多路复用技术来说用处不是很大,了解)
from socket import *
import select
import systcp_server_socket = socket(AF_INET, SOCK_STREAM)# 重用对应地址和端口
tcp_server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)# 本地IP地址和端口
address = ('192.168.5.7', 2000)tcp_server_socket.bind(address)
# 端口激活
tcp_server_socket.listen(100)# 非阻塞编程的的关键在于这一句话,实际上就是通过这一句话让程序遇到阻塞的地方不进行阻塞,而进行爆异常
tcp_server_socket.setblocking(False)client_socket = None
temp_client =None
while True:try:temp_client, clientAddr = tcp_server_socket.accept()except Exception as e:# print(e)client_socket = temp_clientif client_socket:client_socket.setblocking(False)try:text = client_socket.recv(1024)#如果对方断开if not text:print('byebye')client_socket.close()temp_client=Nonecontinueprint(text.decode('utf-8'))except Exception as e:pass
以及:这里才用到的
重用对应地址和端口:
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
补充
相同的 send也会阻塞,在send发送的过多次的时候,send也需要进行epoll或者非阻塞编程
持续发送多个文件,协议设计
粘包:两次发送的报文挨在一起,就好比如我们百度网盘在下载多个文件的时候,抓包的结果就是文件名1+文件内容1+文件名2+文件内容2······的字节流
首先我们采用的方法就是在接受一个文件之前,先接受一个整形的数,然后拿这个整型数接着接后续的字节流,再接整型数,最后的效果给个例子就是:
先读到5 然后读取文件名file1 然后读取100 然后读取100个文件内容1的字节流and so on
那么就出现一个问题,我们如何使用python来发送一个整形/浮点数的数据呢?
Python 发送整型数/浮点数
参考的详细链接
这边简单只要会使用pack和unpack就差不多
还有关于其struct模块定义的数据类型表(常用):
Format | C Type |
---|---|
c | char |
? | bool |
i | int |
I | unsigned int |
f | float |
d | double |
s | string |
给个使用的样例的话:
import structfile_content_bytes = '我是一个文件'.encode('utf8')
print(file_content_bytes.decode())
print(file_content_bytes)
print(100*"*")file_head = len(file_content_bytes)
print(file_head) # 18个 3*6
print(type(file_head))
print(100*"*")file_head_bytes = struct.pack('I',file_head)
print(file_head_bytes)
print(type(file_head_bytes))
print(100*"*")file_head2 = struct.unpack('I',file_head_bytes)
print(file_head2)
print(type(file_head2)) # 出来的是一个tuple类型,所以实际上需要使用tuple接口
print(file_head2[0])
print(type(file_head2[0]))
print(100*"*")
值得关注的是,我们传进去的字节流,如果仔细观察,你会发现他是小端传入,但是计网中传入的数据似乎是大端,这里打个问号,笔者也不知道
多个文件传输的例子,自行加for
服务端
from socket import *
import select
import sys
import time
import struct
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 重用对应地址和端口
tcp_server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 本地 IP 地址和端口
address = ('192.168.1.112', 2000)
tcp_server_socket.bind(address)
# 端口激活
tcp_server_socket.listen(100)
client_socket, clientAddr = tcp_server_socket.accept()
#连上了
print(clientAddr)
#发文件名
file_name = "Readme"
#先发报文头--文件名的长度
b_file_name =file_name.encode('utf-8')
client_socket.send(struct.pack("I", len(b_file_name)))
client_socket.send(b_file_name)
#发文件内容
file = open(file_name,"rb")
text_bytes = file.read()
client_socket.send(struct.pack("I", len(text_bytes)))
client_socket.send(text_bytes)
file.close()
time.sleep(5)
client_socket.close()
tcp_server_socket.close()
客户端
from socket import *
import select
import sys
import time
import struct
tcp_client_socket = socket(AF_INET, SOCK_STREAM)
# 本地 IP 地址和端口
address = ('192.168.1.112', 2000)
# 连接服务器
tcp_client_socket.connect(address)
time.sleep(1)
#接文件名
train_len=tcp_client_socket.recv(4)
file_name=tcp_client_socket.recv(struct.unpack('I',train_len)[0])
print(file_name)
file=open(file_name.decode('utf-8'),"wb")
train_len=tcp_client_socket.recv(4)
text_bytes=tcp_client_socket.recv(struct.unpack('I',train_len)[0])
file.write(text_bytes)
file.close()
tcp_client_socket.close()
补充
接收方不知道该接收多大的数据才算接收完毕
stat 接口
print(os.stat('Readme').st_size)
print(os.stat('Readme').st_ino)
print(hex(os.stat('Readme').st_mode))
print(os.stat('Readme').st_uid)
print(os.stat('Readme').st_gid)
print(time.ctime(os.stat('Readme').st_mtime))