十二、网络编程

十二、网络编程

网络编程:使用编程语言实现多台计算机的通信。

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。

  1. SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。

  2. 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:“谢谢你受理我的请求。”

 

 

 

  1. 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。

  2. 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。

  3. 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:

// URG:紧急指针(urgent pointer)有效。
// ACK:确认序号有效。
// PSH:接收方应该尽快将这个报文交给应用层。
// RST:重置连接。
// SYN:建立一个新连接。
// FIN:断开一个连接。

3)TCP/IP三次握手

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:

 

 

客户端调用 socket() 创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。这个时候,客户端开始发起请求:

  1. 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。

  2. 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。 服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。服务器将数据包发出,进入SYN-RECV状态。

  3. 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认(Ack)”字段。客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。

  4. 服务器端收到数据包,检测到已经设置了 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状态。这时,客户端发起断开连接的请求:

  1. 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。

  2. 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。

  3. 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。

  4. 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。

  5. 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。

  6. 服务器收到客户端的 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()

 

 

 

 

posted @ 2022-05-21 15:45  xiaohaoge  阅读(119)  评论(0编辑  收藏  举报