【Python开发】NO.8:Socket

一、知识背景

1.数据传输协议:
http:网站
smtp:邮件
dns:域名解析成IP地址
ftp:上传下载
ssh:安全外壳协议
snmp:简单网络管理协议
icmp:ping包 (网络层)
dhcp:IP地址分配

2.数据传输的两种形式:
发——send
收——receive

 

3.OSI七层协议:  
⑦应用层
⑥表示层
⑤会话层
④传输层:基于一种特殊的协议
③网络层 :IP地址
②数据链路层: mac地址
①物理层

(图片转自 http://www.cnblogs.com/alex3714/articles/5227251.html)

上述数据传输协议中,icmp在网络层,其他协议在应用层。

 

4.数据传输协议:
TCP/IP 三次握手、四次断开,上述数据传输协议协议均在此协议层级之上。

UDP:一种相对不安全的数据发送(不需要三次握手确认)

 

5.地址簇:

socket.AF_UNIX unix本机进程间通信

socket.AF_INET IPV4

socket.AF_INET6 IPV6

 

对于所有的上层协议,均需要进行数据的发(send)与收(receive),因此将TCP/IP和UDP进行封装,仅暴露接口(send, recv等),供所有协议使用,方便操作,此封装即为socket。

(此部分知道的不是特别多,还需要恶补一下)

 

二、Socket

1.简单的信息交互程序

大致思路:

先写一个简单的信息交互程序,实现的功能为:从客户端输入英文字母,服务端会全部转换为大写发回来。

客户端:

 1 import socket
 2 
 3 client = socket.socket() #定义协议类型,不写为默认 生成socket类型,同时生成socket连接对象
 4                 # def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
 5 client.connect(('localhost',6969))   #connect只接受一个参数,但需要同时传入IP地址和端口,因此需要加括号变成一个元组传当作一个值进去
 6                                     #这里'localhost'指本地,非本地可写IP地址,6969为端口
 7 client.send("我要下载影片asd".encode("utf-8"))    #发数据:此处只能发byte类型数据,不能发字符串类型的 中文文字需要编码
 8 '''即需要记住所用的数据传输都需要用bytes格式!!!'''
 9 
10 data = client.recv(1024)    #接收服务器端的返回 单位为字节 :1024字节
11 print("recv:",data.decode()) #需要再转回来,否则打印的是butes格式看不懂,变成下边这样
12             #recv: b'\xe6\x88\x91\xe8\xa6\x81\xe4\xb8\x8b\xe8\xbd\xbd\xe5\xbd\xb1\xe7\x89\x87'
13 
14 client.close()

服务端:

 1 import  socket
 2 
 3 server = socket.socket()
 4 server.bind(("localhost",6969))  #绑定端口,即选择要监听的端口
 5 server.listen()    #监听  “含义为:我要“开始”监听这个端口了”
 6          #开始监听端口,括号内可传入数字,数字表示允许排队的最大客户端数(不包括现在正在与服务端连接的),一般写5就行。
 7 
 8 print("我要开始等电话了")
 9 conn,addr = server.accept()    #等待  (“就像:等电话打进来”) server.accept()会返回两个值
10                                   # conn:链接的标记位(即客户端连过来而在服务器端为其生成的过来的一个链接实例) addr:对方的地址
11          # (为了解决server需要处理不同的线路,故需要对每一条线路进行标记),所以这里不能直接写server.accept(),而必须写成这种形式
12 print(conn,addr) #打印结果:<socket.socket fd=268, family=AddressFamily.AF_INET,
13                                 # type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969),
14                                 #raddr=('127.0.0.1', 53890)>   ——此为已经生成的一个实例
15                                 #('127.0.0.1', 53890)   ——客户端的(随机)端口
16                                     #此后与client的通信就通过conn来进行
17 print("电话来了")
18 
19 #data = server.recv(1024)
20 data = conn.recv(1024)   
21 print("recv:",data)
22 conn.send(data.upper())  #接收数据后转换成大写再发回去
23 
24 server.close()     #关闭不能写conn

 注意这里的问题,在客户端发送一次之后,由于程序已经走完,因此客户端和服务端均停止运行,如想实现重复地发送和接收数据,则需要加上循环:

客户端

 1 '''要实现多次的接收'''
 2 import socket
 3 
 4 client = socket.socket() 
 5 client.connect(('localhost',6969))
 6 
 7 while True:     #为了可以不断接收,此处使用循环
 8     msg = input(">>:").strip()
 9     client.send(msg.encode("utf-8"))
10 
11     data = client.recv(1024)    
12     print("recv:",data.decode()) 
13 
14 client.close()

服务端

 1 import  socket
 2 
 3 server = socket.socket()
 4 server.bind(("localhost",6969))  #绑定端口,即选择要监听的端口
 5 server.listen()    
 6 
 7 print("我要开始等电话了")
 8 
 9 while True:    #要实现的效果: 在第一个客户断开后,自动连接第二个客户(运行两次客户端即为有两个会话)
10     conn,addr = server.accept()   #这一部分不能放在循环里面,否则就是每循环一次换一个“人”了
11     print(conn,addr)
12 
13     print("电话来了")
14 
15     while True:
16         data = conn.recv(1024)   #将server改成conn
17         print("recv:",data)
18         if not data:
19             print("clint has lost...")
20             break
21         conn.send(data.upper())  
22 
23 
24 server.close()    

 这样就实现了可以在客户端多次输入字母,并得到服务端返回的大写英文字母。

 

2.ssh

可在客户端输入指令,服务端会返回指令结果,实现基本的xshell功能。

客户端

 1 import socket
 2 
 3 client = socket.socket()  #创建client
 4 
 5 client.connect(('localhost',9999))  #将IP地址和端口号写成一个元组作为一个变量传进去
 6 
 7 while True:    #写成循环形式,可以重复向服务端发送命令。
 8     cmd = input(">>:").strip()     #输入要发送的命令
 9     if len(cmd) == 0: continue    #如果输入的数据为空,则跳出本次循环
10     client.send(cmd.encode('utf-8'))  #现在cmd中的信息为字符串,而数据必须用bytes类型进行传输,因此需要进行编码
11     cmd_res = client.recv(1024)    #接收cmd命令所得到的数据,规定接收的数据大小:1024 bytes
12     print(cmd_res.decode())        #将数据解码,打印
13 
14 
15 client.close()

服务端

 1 import socket
 2 import os
 3 
 4 server = socket.socket() #创建server
 5 server.bind(('localhost',9999))  #绑定IP地址和端口(写成元组作为一个变量传入)
 6 
 7 server.listen()  #开始监听端口,括号内可传入数字,数字表示允许排队的最大客户端数(不包括现在正在与服务端连接的),一般写5就行。
 8 
 9 while True:     #此循环用于当一个客户端断开后,不会停止运行,而是继续连接下一个客户端
10     conn, addr = server.accept()  #等待连接。其中conn是新的套接字对象,可以用来接收和发送数据。addr是连接客户端的地址
11     print("new conn:",addr)
12     while True:   #写成循环用以重复接收同一个客户端的命令
13         print("等待新指令")
14         data = conn.recv(1024)  #接收指令的数据(规定为1024 bytes)
15         if not data:           #如果接收到的数据为空则表示客户端已断开,则结束本循环
16             print("客户端已断开")
17             break
18         print("执行指令:",data)  #打印接收到的指令的数据
19         cmd_res = os.popen(data.decode()).read()  #接收字符串,执行结果也是字符串。os.popen()的作用大致相当于在命令窗口执行括号里的命令,并将命令的结果返回
20         print('before send', len(cmd_res))   #打印长度
21         if len(cmd_res) == 0:
22             cmd_res = 'cmd has no output...'
23         conn.send(cmd_res.encode('utf-8'))
24         print('send done')
25 
26 server.close()

需要注意的问题:

socket存在“缓冲区”的概念,由于客户端设定只接收1024字节,故当发过去的数据大小多于1024字节时,剩余的部分会存在缓冲区里,待第二次send时发出,而又将第二次命令的结果放在了缓冲区里。这里,如果我们第一次输入指令“ipconfig”。在客户端里会输出前1024字节,第二次输入指令“dir”后,会输出ipconfig的剩余数据,此时“dir”的命令也执行了,但是数据存在缓冲区里,没有发给客户端。

为解决此问题,客户端需要进行循环接收,此处需要解决的问题是如何判断循环到哪一次时刚好把数据发完。

方法:在服务器端发送的时候,先发送一下此数据的大小,这样客户端就会根据发过来的数据大小知道需要接收几次会把数据完全接收。具体代码详见下文。

 

2.5.ssh(改)

客户端

import socket

client = socket.socket()

client.connect(('localhost',9999))

while True:
    cmd = input(">>:").strip()
    if len(cmd) == 0: continue
    client.send(cmd.encode('utf-8'))
    cmd_res_size = client.recv(1024)   #接收命令结果的长度,收到的是bytes类型
    print("命令结果大小:", cmd_res_size)
    received_size = 0
    received_data = b''
    while received_size < int(cmd_res_size.decode()):
                                #做一个判断:如果接收到的数据大小总和等于发过来的那个数据大小的值,则跳出循环结束数据接收
                                          #并且由于cmd_res_size是bytes类型,此处还需要decode
        data = client.recv(1024)
        received_size += len(data)   #每次收到的有可能小于1024,故此处写len(data)而不是1024
        #print(data.decode())
        received_data += data
    else:
        print("cmd res receive done...", received_size)  #可能出现received_size与cmd_res_size不一样大小的情况,
#这是由于中文在字符串形式下长度为1,而encode为bytes类型后长度变为3,而server端测算的是未encode的长度,而客户端测算的是encode后的
        print(received_data.decode())

    #print(cmd_res.decode())


client.close()

服务端

 1 import socket
 2 import os
 3 
 4 server = socket.socket()
 5 server.bind(('localhost',9999))
 6 
 7 server.listen()
 8 
 9 while True:
10     conn, addr = server.accept()
11     print("new conn:",addr)
12     while True:
13         print("等待新指令")
14         data = conn.recv(1024)
15         if not data:
16             print("客户端已断开")
17             break
18         print("执行指令:",data)
19         cmd_res = os.popen(data.decode()).read()  #接收字符串,执行结果也是字符串
20         print('before send', len(cmd_res))
21         if len(cmd_res) == 0:
22             cmd_res = 'cmd has no output...'
23         conn.send(  str(len(cmd_res)).encode('utf-8')  )     #先发大小给客户端
24         conn.send(cmd_res.encode('utf-8'))
25 
26         print('send done')
27 
28 server.close()

 

3.粘包

然而,上述代码仍然有不完美之处,注意服务端的第23、24行,在实际运行过程中,可能会出现如下现象:由于23和24两条紧挨着的语句都是发送数据的语句,在23条发送数据大小的时候,可能会连带着将24行的命令结果也一起发送到了客户端上,此即所谓的“粘包”现象。

解决方法:在23和24条语句之间插入一条其他语句“client_ack = conn.recv(1024)",用以将这两条发送语句分隔,便可以避免粘包的现象出现。此条语句仅起到分隔作用,无其他实际意义。代码内容为从客户端接收一段大小为1024字节的数据,可以在客户端写上一条“client.send("准备好接收,可以发了。".encode("utf-8"))”来配合此语句。改进后的代码具体见下文:

客户端

 1 import socket
 2 
 3 client = socket.socket()
 4 
 5 client.connect(('localhost',9999))
 6 
 7 while True:
 8     cmd = input(">>:").strip()
 9     if len(cmd) == 0: continue
10     client.send(cmd.encode('utf-8'))
11     cmd_res_size = client.recv(1024)   #接收命令结果的长度,收到的是bytes类型
12     print("命令结果大小:", cmd_res_size)
13     client.send("准备好接收,可以发了。".encode("utf-8"))  #用于回应服务端两个send之间的那条语句,用于解决粘包问题
14     received_size = 0
15     received_data = b''
16     while received_size < int(cmd_res_size.decode()):
17 
18         data = client.recv(1024)
19         received_size += len(data)
20         #print(data.decode())
21         received_data += data
22     else:
23         print("cmd res receive done...", received_size)
24         print(received_data.decode())
25 
26     #print(cmd_res.decode())
27 
28 
29 client.close()

服务端

 1 import socket
 2 import os
 3 
 4 server = socket.socket()
 5 server.bind(('localhost',9999))
 6 
 7 server.listen()
 8 
 9 while True:
10     conn, addr = server.accept()
11     print("new conn:",addr)
12     while True:
13         print("等待新指令")
14         data = conn.recv(1024)
15         if not data:
16             print("客户端已断开")
17             break
18         print("执行指令:",data)
19         cmd_res = os.popen(data.decode()).read()  #接收字符串,执行结果也是字符串
20         print('before send', len(cmd_res))
21         if len(cmd_res) == 0:
22             cmd_res = 'cmd has no output...'
23         conn.send(  str(len(cmd_res)).encode('utf-8')  )     #先发大小给客户端,此处连续两个sand会出现粘包
24         client_ack = conn.recv(1024)       #解决方法:在两者之间加入一个等待客户端确认的语句即可
25         conn.send(cmd_res.encode('utf-8'))
26 
27         print('send done')
28 
29 server.close()

嗯,这样看上去就完美多了。

 

4.ftp

ftp(文件传输协议)用于Internet上控制文件的双向传输。

客户端发来命令要下载一个文件,此时ftp服务端需要做的事情:

①读取客户端发来的文件名

②检测文件是否存在

③打开文件

④检测文件大小

⑤发送文件大小给客户端

⑥等待客户端确认(防止粘包)

⑦开始边读边发数据

⑧发送md5给客户端(加密),客户端要进行验证

⑨关闭文件

客户端

 1 import socket
 2 import hashlib
 3 
 4 client = socket.socket()
 5 
 6 client.connect(('localhost',9999))
 7 
 8 while True:
 9     cmd = input(">>:").strip()
10     if len(cmd) == 0: continue
11     if cmd.startswith("get"):    #当接收到获取某文件指令时触发以下代码
12         client.send(cmd.encode())  #发送命令
13         server_response = client.recv(1024)   #接收文件大小
14         print("server response:", server_response)
15         client.send(b"ready to recv file")   #回应(防止粘包)
16         file_total_size = int(server_response.decode())   #将文件大小转换为int型以用于后续比较大小
17         received_size = 0                                 #用于计算接收到的数据大小
18         filename = cmd.split()[1]   #获取文件名
19         f = open(filename, 'wb')      #将下载的数据存到文件里面
20         m = hashlib.md5()
21 
22         while received_size < file_total_size:
23             if file_total_size - received_size > 1024:   #要收不止一次   此处用于解决使用了md5后可能出现的粘包问题
24                 size = 1024
25             else:       #最后一次接收,剩多少收多少(这样就不会接收超出此文件大小的其他数据了,也就杜绝了粘包的出现)
26                 size = file_total_size - received_size
27             data = client.recv(size)
28             received_size += len(data)
29             m.update(data)
30             f.write(data)              #将数据写入文件
31             print(file_total_size, received_size)   #用于直观显示循环语句的效果
32         else:
33             new_file_md5 = m.hexdigest()
34             print("file recv done")
35             f.close()      #别忘了关闭文件
36         server_file_md5 = client.recv(1024)
37         print("server file md5:", server_file_md5)
38         print("client file md5:", new_file_md5)

服务端

 1 import socket
 2 import os
 3 import hashlib
 4 
 5 server = socket.socket()
 6 server.bind(('localhost',9999))
 7 
 8 server.listen()
 9 
10 while True:
11     conn, addr = server.accept()
12     print("new conn:",addr)
13     while True:
14         print("等待新指令")
15         data = conn.recv(1024)
16         if not data:
17             print("客户端已断开")
18             break
19 
20         cmd,filename = data.decode().split()  #读取文件名
21         print(filename)
22         if os.path.isfile(filename):          #判断文件是否存在
23             f = open(filename + "new", 'rb')    #打开文件(这里使用二进制读方便后续传输,就不用再转码了)
24                                                 #由于目前对文件的操作仅限当前目录,故此处修改接收后的文件名称,以便于区分
25             m = hashlib.md5()
26             file_size = os.stat(filename).st_size   #判断文件大小(这里st_size为os.stat()方法所返回的值中的一个,为文件大小)
27             conn.send( str(file_size).encode('utf-8'))  #将文件大小发给客户端
28             conn.recv(1024)   #等待客户确认(防止粘包)
29             for line in f:
30                 m.update(line)     #将每一行进行md5加密
31                 conn.send(line)    #以上三行为逐行读取并发送文件数据
32             print("file md5", m.hexdigest())      #打印md5
33             f.close()
34             conn.send(m.hexdigest().encode())     #将md5发送给客户端 此处还是可能会粘包
35         print('send done')
36 
37 server.close()

 

三、SocketServer

socketserver的作用相当于并发处理,是socket的再封装,用于简化网络服务器的开发。

socketserver共有如下四种类型:TCPServer、UnixStreamServer、UDPServer、UnixDatagramServer,关系如下:

+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+

创建一个socketserver至少分以下几步:

①必须创建一个请求处理类,这个类要继承BaseRequestHandler类,并且要重写这个父类里handle()的方法。

②必须实例化其中的一个server class(TCPServer、UnixStreamServer、UDPServer、UnixDatagramServer中的一个),并将服务器的地址和上边创建的请求处理类传给这个server class。

 ③接下来可以通过上述实例来调用“handle_request()——只处理一个请求、serve_forever()——处理多个请求(基本都用这个)”方法。

④调用server_close()来关闭socket。

 1.使用socketserver实现多并发

来看一下代码:

此处只给出服务端的代码,客户端的代码使用“2.ssh”的客户端代码。

服务端

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):  #1.继承BaseRequestHandler的请求处理类
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):                                  #2.重写handle()方法,跟客户端所有的交互都是在handle里完成的。
        while True:   #循环用于重复执行客户端的命令,如不加循环,则在执行一次客户端的命令之后便不再运行,客户端的1第二条指令将无作用。
            try:
                # self.request is the TCP socket connected to the client
                self.data = self.request.recv(1024).strip()    #这里不是conn.recv而是self.request.recv() :每过来一个请求就会实例化一个MyTCPHandler
                print("{} wrote:".format(self.client_address[0])) #打印客户端IP地址
                print(self.data)
                # just send back the same data, but upper-cased
                self.request.sendall(self.data.upper())   #把数据传回,改成大写
            except ConnectionResetError as e:
                print("err",e)
                break

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)  #2.实例化server class (此处为TCPServer)
                                              #(HOST, PORT)为作为一个参数的ip地址,MyTCPHandler为刚刚创建的请求处理类

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()     #3.调用serve_forever()

此处代码转自(http://www.cnblogs.com/alex3714/articles/5830365.html)

然而,实际运行了之后会发现第二个以及之后访问的用户仍然会挂起,并没有起到多并发效果。这是由于用来实现多并发的功能还没有使用(笑),在原代码的某个位置进行一个修改便可。

服务端(多并发)

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):  #1.继承BaseRequestHandler类
   
    def handle(self):                                  #2.重写handle()方法,跟客户端所有的交互都是在handle里完成的。
        while True:   #循环用于重复执行客户端的命令,如不加循环,则在执行一次客户端的命令之后便不再运行,客户端的1第二条指令将无作用。
            try:
                self.data = self.request.recv(1024).strip()    #这里不是conn.recv而是self.request.recv() :每过来一个请求就会实例化一个MyTCPHandler
                print("{} wrote:".format(self.client_address[0])) #打印客户端IP地址
                print(self.data)
                self.request.sendall(self.data.upper())   #把数据传回,改成大写
            except ConnectionResetError as e:
                print("err",e)
                break

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)       #将原来这个位置的代码改成这样,实现“多线程”
    server.serve_forever()

要实现多并发功能,只需要将原句TCPServer改成ThreadingTCPServer。threading是线程的意思,每来一个请求,服务端就会开一个新的线程。现在可以实现多个用户同时连接服务器的功能,不会出现挂起的状态。
这个地方还可以写成ForkingTCPServer,此为“多进程”,在这个脚本里可以与多线程实现同样的功能。(但是这个方法只适用于Linux系统,Windows上不行。)

 就现阶段而言,只掌握handle()方法用法即可。

 

 2.使用socketserver实现多用户在线ftp程序

这里仅给出能实现用户上传功能的代码:

客户端

 1 import socket
 2 import os
 3 import json
 4 
 5 class FTPClient(object):
 6     def __init__(self):     #构造函数
 7         self.client = socket.socket()
 8 
 9     def help(self):  #用于打印某些指令的帮助信息,就是说列出来都有哪些指令是可以用的(这里的都是linux命令) 。
10         msg = '''
11         ls
12         pwd
13         cd ..
14         get filename
15         put filename
16         '''
17         print(msg)
18 
19     def connect(self, ip, port):    #地址、端口
20         self.client.connect((ip, port))
21 
22     def interactive(self):    #与服务器交互
23         #self.authenticate()   #此处需要另封装一个方法(自己写)用于用户验证,如果成功则会继续运行,如不成功则直接退出。
24         while True:
25             cmd = input(">>").strip()
26             if len(cmd) == 0: continue
27             cmd_str = cmd.split()[0]     #由于cmd输入的第一个值肯定是某个指令,即为cmd.split()[0]
28             if hasattr(self, "cmd_%s"% cmd_str):  #即:如果用户输入“put”指令,则对应服务端应调用cmd_put方法,所有可用方法在类中一律写成“cmd_...”的形式。
29                 func = getattr(self, "cmd_%s"% cmd_str)  #检测类中是否有输入的方法,如果有则获取该方法的内存地址
30                 func(cmd)    #执行这个方法
31             else:
32                 self.help()    #表示没有用户输入的这个命令,并列出所有可用的命令。
33 
34     def cmd_put(self, *args):    #上传   因为要接收数据进来,所以需要有参数,使用*args是为了将来有接收多个参数的需要
35         cmd_split = args[0].split()    #解析命令
36         if len(cmd_split) > 1:     #判断合法性,否则取不到文件,而且必须大于1,因为需要用get filename来先获取文件,而get本身也算一个长度。
37             filename = cmd_split[1]   #命令的第二个词是文件名
38             if os.path.isfile(filename):  #如果文件存在,则开始传文件
39                 filesize = os.stat(filename).st_size   #先将文件名和大小发给服务器端
40                 msg_dic = {          #这里最好写成字典的形式而不是写成字符串拼接的形式,一是方便服务器端处理,二是方便后续扩展,比如会发一些其他的数据等。
41                     "action": "put",
42                     "filename": filename,
43                     "size": filesize,
44                     "overridden": True     #可以扩展的选项距离:如果服务器上有同名的文件,则覆盖此文件,这一项可作为选项添加到字典里
45                 }
46                 self.client.send(json.dumps(msg_dic).encode("utf-8"))   #json.dumps(msg_dic)先将字典转成json格式,再encode成bytes格式
47                 server_respone = self.client.recv(1024)     #为防止粘包,需要等服务器确认。
48 # 这里最好不要随便确认。而是加入一个目的性需求:将文件名和大小传给服务器后,服务器需要确认客户端是否有权限传这么大的文件,如没有,则返回响应结果。
49 # 这里最好先确定好一些标准请求码,如300表示没有权限,400表示文件过大等。遇到相应的问题则直接传回相应的请求码。
50                 f = open(filename, "rb")
51                 for line in f:
52                     self.client.send(line)
53                 else:
54                     print("file upload success...")
55                     f.close()
56             else:
57                 print(filename, "does not exist!")
58 
59     def cmd_get(self):    #下载
60         pass
61 
62 ftp = FTPClient()
63 ftp.connect("localhost", 9999)
64 ftp.interactive()
View Code

服务端

 1 import socketserver
 2 import os, json
 3 
 4 class MyTCPHandler(socketserver.BaseRequestHandler):
 5 
 6     def put(self, *args):
 7         '''接收客户端的文件'''
 8         cmd_dic = args[0]
 9         filename = cmd_dic["filename"]
10         filesize = cmd_dic["size"]
11         if os.path.isfile(filename):    #如果在服务器中文件存在,则覆盖,否则新建
12             f = open(filename + ".new", "wb")
13         else:
14             f = open(filename, "wb")
15 
16         self.request.send(b"200 ok")
17         received_size = 0
18         while received_size < filesize:
19             data = self.request.recv(1024)
20             f.write(data)
21             received_size += len(data)
22         else:
23             print("file [%s] has uploaded..."% filename)
24 
25     def handle(self):
26         while True:   #循环用于重复执行客户端的命令,如不加循环,则在执行一次客户端的命令之后便不再运行,客户端的1第二条指令将无作用。
27             try:
28                 self.data = self.request.recv(1024).strip()
29                 print("{} wrote:".format(self.client_address[0])) #打印客户端IP地址
30                 print(self.data)
31                 cmd_dic = json.loads(self.data.decode())   #先把json格式的转回来
32                 action = cmd_dic["action"]
33                 if hasattr(self, action):    #这个反射的用处是将用户的命令解耦,这个是服务端和客户端的重点!
34                     func = getattr(self, action)
35                     func(cmd_dic)
36 
37             except ConnectionResetError as e:
38                 print("err",e)
39                 break
40 
41 if __name__ == "__main__":
42     HOST, PORT = "localhost", 9999
43 
44     server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
45 
46     server.serve_forever()
View Code

代码中一些需要注意的点:

①使用“反射”中的“hasattr”和“getattr”来对需要发送和接收的命令进行解析,比如“get 数学课.avi”中,第一个字符串为命令内容,第二个为文件名,需要使用“反射”来对不同位置的内容进行分别的处理。

②之前的socket代码多为用于理解,在真正的开发中都是像上述代码一样写成类的形式。

③注意客户端的第34行,cmd_put方法在形参部分写的是*args,是为了后续扩展需要,之后如有其他功能还可以在msg_dic里添加,也就是还可以往cmd_put里传更多其它的参数,此处若是写死成仅接收文件大小和文件名,从长远考虑则弊端很大,这一点在开发时需要注意。

④注意客户端第47行的防粘包处理,这里没有像之前一样只是写一句无意义的话,而是起到一个确认的作用:客户端是否有权限传这么大的文件等一些有关传输的认证处理。并且最好确定一些双方共同认定的标准请求码,如300代表用户没有权限、400代表文件过大等,以便于管理(类比于http的error 403等)。

⑤客户端别忘了实例化才能开始传文件(笑)。

posted on 2018-01-28 23:17  Learza  阅读(172)  评论(0)    收藏  举报