十二、网络编程
十二、网络编程
网络编程:使用编程语言实现多台计算机的通信。
12.1、网络三要素
网络编程三要素:
(1)IP地址:网络中每一台计算机的唯一标识,通过IP地址找到指定的计算机。
(2)端口:用于标识进程的逻辑地址,通过端口找到指定进程。
(3)协议:定义通信规则,符合协议则可以通信,不符合不能通信。一般有TCP协议和UDP协议。
(1)IP地址
计算机分布在世界各地,要想和它们通信,必须要知道确切的位置。确定计算机位置的方式有多种,IP 地址是最常用的,例如,114.114.114.114 是国内第一个、全球第三个开放的 DNS 服务地址,127.0.0.1 是本机地址。
其实,我们的计算机并不知道 IP 地址对应的地理位置,当要通信时,只是将 IP 地址封装到要发送的数据包中,交给路由器去处理。路由器有非常智能和高效的算法,很快就会找到目标计算机,并将数据包传递给它,完成一次单向通信。
目前大部分软件使用 IPv4 地址,但 IPv6 也正在被人们接受,尤其是在教育网中,已经大量使用。
(2)端口
有了 IP 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。
为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。
端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:
(3)协议
协议(Protocol)就是网络通信的约定,通信的双方必须都遵守才能正常收发数据。协议有很多种,例如 TCP、UDP、IP 等,通信的双方必须使用同一协议才能通信。协议是一种规范,由计算机组织制定,规定了很多细节,例如,如何建立连接,如何相互识别等。
协议仅仅是一种规范,必须由计算机软件来实现。例如 IP 协议规定了如何找到目标计算机,那么各个开发商在开发自己的软件时就必须遵守该协议,不能另起炉灶。
所谓协议族(Protocol Family),就是一组协议(多个协议)的统称。最常用的是 TCP/IP 协议族,它包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,由于 TCP、IP 是两种常用的底层协议,所以把它们统称为 TCP/IP 协议族。
(4)数据传输方式
计算机之间有很多数据传输方式,各有优缺点,常用的有两种:SOCK_STREAM 和 SOCK_DGRAM。
-
SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
-
SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
有可能多种协议使用同一种数据传输方式,所以在 socket 编程中,需要同时指明数据传输方式和协议。
综上所述:IP地址和端口能够在广袤的互联网中定位到要通信的程序,协议和数据传输方式规定了如何传输数据,有了这些,两台计算机就可以通信了。
12.2、TCP协议
(1)OSI模型
如果你读过计算机专业,或者学习过网络通信,那你一定听说过 OSI 模型,它曾无数次让你头大。OSI 是 Open System Interconnection 的缩写,译为“开放式系统互联”。 OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
这个网络模型究竟是干什么呢?简而言之就是进行数据封装的。
当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。
2)TCP报文格式
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。
客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:
- [Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
- [Shake 2] 套接字B:“好的,我这边已准备就绪。”
- [Shake 3] 套接字A:“谢谢你受理我的请求。”
-
序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
-
确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。
-
标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:
// URG:紧急指针(urgent pointer)有效。 // ACK:确认序号有效。 // PSH:接收方应该尽快将这个报文交给应用层。 // RST:重置连接。 // SYN:建立一个新连接。 // FIN:断开一个连接。
3)TCP/IP三次握手
使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:
客户端调用 socket() 创建套接字后,因为没有建立连接,所以套接字处于CLOSED
状态;服务器端调用 listen() 函数后,套接字进入LISTEN
状态,开始监听客户端请求。这个时候,客户端开始发起请求:
当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了
SYN-SEND
状态。服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。 服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。服务器将数据包发出,进入
SYN-RECV
状态。客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认(Ack)”字段。客户端将数据包发出,进入
ESTABLISED
状态,表示连接已经成功建立。服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入
ESTABLISED
状态。至此,客户端和服务器都进入了ESTABLISED
状态,连接建立成功,接下来就可以收发数据了。注意:三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看
Ack = Seq + 1
是否成立,如果成立说明对方正确收到了自己的数据包
(4)TCP/IP四次挥手
建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。
建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:
- [Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
- [Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
- 等待片刻后……
- [Shake 3] 套接字B:“我准备好了,可以断开连接了。”
- [Shake 4] 套接字A:“好的,谢谢合作。”
下图演示了客户端主动断开连接的场景:
建立连接后,客户端和服务器都处于ESTABLISED
状态。这时,客户端发起断开连接的请求:
客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入
FIN_WAIT_1
状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入
CLOSE_WAIT
状态。注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。客户端收到“确认包”后进入
FIN_WAIT_2
状态,等待服务器准备完毕后再次发送数据包。等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入
LAST_ACK
状态。客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入
TIME_WAIT
状态。服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入
CLOSED
状态。注意:关于 TIME_WAIT 状态的说明
客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?
/*
TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。
如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。
客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。
那么,要等待多久呢?数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。
这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。
TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。
*/
12.3、socket介绍
12.3.1、什么是 socket?
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。 我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具
12.3.2、socket缓冲区与阻塞
1、socket缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
这些I/O缓冲区特性可整理如下:
- I/O缓冲区在每个TCP套接字中单独存在;
- I/O缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
输入输出缓冲区的默认大小一般都是 8K!
2、阻塞模式
对于TCP套接字(默认情况下),当使用send() 发送数据时:
(1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 send() 会被阻塞(暂停执行),直到缓冲区中的数据被发 送到目标机器,腾出足够的空间,才唤醒 send() 函数继续写入数据。
(2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,send() 也会被阻塞,直到数据发送完毕缓冲区解锁, send() 才会被唤醒。
(3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
(4) 直到所有数据被写入缓冲区 send() 才能返回。
当使用recv() 读取数据时:
(1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
(2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 recv() 函数再次读取。
(3) 直到读取到数据后 recv() 函数才会返回,否则就一直被阻塞。
TCP套接字默认情况下是阻塞模式,也是最常用的。当然你也可以更改为非阻塞模式,后续我们会讲解。
12.3.3、TCP的粘包问题
上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc”,那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。
这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
12.4、Python的socket模块
12.4.1、创建套接字对象
Linux 中的一切都是文件,每个文件都有一个整数类型的文件描述符;socket 也可以视为一个文件对象,也有文件描述符。
import socket sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # <socket.socket fd=496, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> print(sock)
1、AF 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。大家需要记住
127.0.0.1
,它是一个特殊IP地址,表示本机地址。2、 type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
3、
sock = socket.socket()
默认创建TCP套接字。
12.4.2、 套接字对象方法
(1)服务端:bind方法
socket 用来创建套接字对象,确定套接字的各种属性,然后服务器端要用 bind() 方法将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 方法建立连接。
import socket sock = socket.socket() sock.bind(("127.0.0.1",8899))
(2)服务端:listen方法
通过 listen() 方法可以让套接字进入被动监听状态,sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 方法的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
如果将 backlog 的值设置为SOMAXCONN ,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。当请求队列满时,就不再接收新的请求。
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
sock.listen(5)
(3)服务端:accept方法
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
conn,addr=sock.accept() print("conn:",conn) # conn: <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 64915)> print("addr:",addr) # addr: ('127.0.0.1', 64915)
(4)客户端:connect方法
connect() 是客户端程序用来连接服务端的方法,:
import socket ip_port=("127.0.0.1",8899) sk=socket.socket() sk.connect(ip_port)
注意:只有经过connect连接成功后的套接字对象才能调用发送和接受方法(send/recv),所以服务端的sock对象不能send or recv。
(5)收发数据方法:send和recv
方法 | 解析 |
---|---|
s.recv() | 接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。 |
s.send() | 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小。 |
12.4.3、聊天案例
服务端:
import socket sock = socket.socket() sock.bind(("127.0.0.1", 8899)) sock.listen(5) while 1: client_sock, addr = sock.accept() print("客户端%s建立连接" % str(addr)) while 1: try: data = client_sock.recv(1024) # data字节传 except Exception: client_sock.close() print("客户端%s退出"%str(addr)) break print(data.decode()) res = input(">>>") client_sock.send(res.encode())
客户端:
import socket ip_port=("127.0.0.1",8899) sk = socket.socket() sk.connect(ip_port) while 1: data = input(">>>") sk.send(data.encode()) res = sk.recv(1024) print("服务端:%s"%res.decode())
12.4.5、粘包案例
import socket import time s = socket.socket() s.bind(("127.0.0.1",8080)) s.listen(5) client,addr = s.accept() time.sleep(10) data = client.recv(1024) print(data) client.send(data)
import socket s = socket.socket() s.bind(("127.0.0.1",8080)) data = input(">>>") s.send(data.encode()) s.send(data.encode()) s.send(data.encode()) res = s.recv(1024) print(res)
12.4.6、ssh案例
服务端程序:
import socket import subprocess import time import struct sock = socket.socket() sock.bind(("127.0.0.1", 8899)) sock.listen(5) while 1: client_sock, addr = sock.accept() print("客户端%s建立连接" % str(addr)) while 1: try: cmd = client_sock.recv(1024) # data字节串 except Exception: print("客户端%s退出" % str(addr)) client_sock.close() break print("执行命令:", cmd.decode("gbk")) # 版本1:内存问题 # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode() # client_sock.send(cmd_res_bytes) # 版本2:粘包问题 # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode() # cmd_res_bytes_len = bytes(str(len(cmd_res_bytes)),"utf8") # client_sock.sendall(cmd_res_bytes_len) # client_sock.sendall(cmd_res_bytes) # 版本3:粘包解决方案 # result_str = subprocess.getoutput(cmd.decode("gbk")) # result_bytes = bytes(result_str, encoding='utf8') # res_len = struct.pack('i',len(result_bytes)) # client_sock.sendall(res_len) # client_sock.sendall(result_bytes) # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode() # cmd_res_bytes_len = bytes(str(len(cmd_res_bytes)),"utf8") # res_len = struct.pack('i', len(cmd_res_bytes)) # client_sock.sendall(res_len) # client_sock.sendall(cmd_res_bytes)
客户端程序:
import socket import time import struct ip_port=("127.0.0.1",8899) sk = socket.socket() sk.connect(ip_port) while 1: data = input("输入执行命令>>>") sk.send(data.encode()) # 版本1 内存问题 # res = sk.recv(1024) # print("字节长度:",len(res)) # print("执行命令结果:%s"%(res.decode())) # 版本2 粘包问题 # # time.sleep(5) # res_len = sk.recv(1024) # data = sk.recv(int(res_len.decode())) # print(res_len) # print(data.decode()) # 版本3:粘包解决方案 # length_msg = sk.recv(4) # length = struct.unpack('i', length_msg)[0] # msg = sk.recv(length).decode() # print("执行命令结果:",msg)
测试命令
ipconfig
netstat -an
12.4.7、案例之文件上传
服务端代码
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import struct import json import os base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.join(base_dir, 'download') class MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding = 'utf-8' request_queue_size = 5 server_dir = 'file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address = server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): print('server is running .......') while True: self.conn, self.client_addr = self.get_request() print('from client ', self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct: break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) cmd = head_dic['cmd'] if hasattr(self, cmd): func = getattr(self, cmd) func(head_dic) except Exception: break def put(self, args): """ 文件长传 :param args: :return: """ file_path = os.path.normpath(os.path.join( base_dir, args['filename'])) filesize = args['filesize'] recv_size = 0 print('----->', file_path) with open(file_path, 'wb') as f: while recv_size < filesize: recv_data = self.conn.recv(2048) f.write(recv_data) recv_size += len(recv_data) else: print('recvsize:%s filesize:%s' % (recv_size, filesize)) def get(self, args): """ 下载文件 1 检测服务端文件是不是存在 2 文件信息 打包发到客户端 3 发送文件 """ filename = args['filename'] dic = {} if os.path.isfile(base_dir + '/' + filename): dic['filesize'] = os.path.getsize(base_dir + '/' + filename) dic['isfile'] = True else: dic['isfile'] = False str_dic = json.dumps(dic) # 字典转str bdic = str_dic.encode(self.coding) # str转bytes dic_len = len(bdic) # 计算bytes的长度 bytes_len = struct.pack('i', dic_len) # self.conn.send(bytes_len) # 发送长度 self.conn.send(bdic) # 发送字典 # 文件存在发送真实文件 if dic['isfile']: with open(base_dir + '/' + filename, 'rb') as f: while dic['filesize'] > 2048: content = f.read(2048) self.conn.send(content) dic['filesize'] -= len(content) else: content = f.read(2048) self.conn.send(content) dic['filesize'] -= len(content) print('下载完成') tcpserver1 = MYTCPServer(('127.0.0.1', 8083)) tcpserver1.run()
客户端代码
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import struct import json import os import time base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.join(base_dir, 'local_dir') class MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding = 'utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address = server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp = input(">>: ").strip() if not inp: continue l = inp.split() cmd = l[0] if hasattr(self, cmd): func = getattr(self, cmd) func(l) def put(self, args): cmd = args[0] filename = args[1] filename = base_dir + '/' + filename print(filename) if not os.path.isfile(filename): print('file:%s is not exists' % filename) return else: filesize = os.path.getsize(filename) head_dic = {'cmd': cmd, 'filename': os.path.basename(filename), 'filesize': filesize} print(head_dic) head_json = json.dumps(head_dic) head_json_bytes = bytes(head_json, encoding=self.coding) head_struct = struct.pack('i', len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size = 0 t1 = time.time() # with open(filename,'rb') as f: # for line in f: # self.socket.send(line) # send_size+=len(line) # else: # print('upload successful') # t2 = time.time() with open(filename, 'rb') as f: while head_dic['filesize'] > 2048: content = f.read(2048) self.socket.send(content) head_dic['filesize'] -= len(content) else: content = f.read(2048) self.socket.send(content) head_dic['filesize'] -= len(content) t2 = time.time() print(t2 - t1) def get(self, args): cmd = args[0] filename = args[1] dic = {'cmd': cmd, 'filename': filename} """发送dic的步骤 字典转str str转bytes 计算bytes的长度 发送长度 发送字典 """ str_dic = json.dumps(dic) # 字典转str bdic = str_dic.encode(self.coding) # str转bytes dic_len = len(bdic) # 计算bytes的长度 bytes_len = struct.pack('i', dic_len) # self.socket.send(bytes_len) # 发送长度 self.socket.send(bdic) # 发送字典 # 接受 准备下载的文件信息 dic_len = self.socket.recv(4) dic_len = struct.unpack('i', dic_len)[0] dic = self.socket.recv(dic_len).decode(self.coding) dic = json.loads(dic) # 文件存在准备下载 if dic['isfile']: t1 = time.time() with open(base_dir + '/' + filename, 'wb') as f: while dic['filesize'] > 2048: content = self.socket.recv(2048) f.write(content) dic['filesize'] -= len(content) else: while dic['filesize']: content = self.socket.recv(2048) f.write(content) dic['filesize'] -= len(content) t2 = time.time() print(t2 - t1) else: print('文件不存在!') client = MYTCPClient(('127.0.0.1', 8083)) client.run()