Python 网络编程(二)

时间:2021-07-11 16:41:19

Python 网络编程

上一篇博客介绍了socket的基本概念以及实现了简单的TCP和UDP的客户端、服务器程序,本篇博客主要对socket编程进行更深入的讲解

一、简化版ssh实现

这是一个极其简单的仿ssh的socket程序,实现的功能为客户端发送命令,服务端接收到客户端的命令,然后在服务器上通过subrocess模块执行命令,如果命令执行有误,输出内容为空,则返回"command error"的语句给客户端,否则将命令执行的结果返回给客户端

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import socket
import subprocess
 
ip = '0.0.0.0'
port = 8005
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((ip, port))
sock.listen(5)
 
while True:
    conn, addr = sock.accept()
    while True:
        try:
            cmd = str(conn.recv(1024), encoding="utf-8")
            if cmd == "exit":
                conn.close()
                break
            p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
            send_data = p.stdout.read()
            if len(send_data) == 0:
                send_data = "command error"
            else:
                send_data = str(send_data, encoding="gbk")
            send_data = bytes(send_data, encoding="utf-8")
            conn.sendall(send_data)
        except Exception as e:
            print (e)
            break

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket
 
ip = '127.0.0.1'
port = 8005
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))
 
while True:
    cmd = input("client>").strip()
    if cmd == "":
        continue
    else:
        sock.sendall(bytes(cmd, encoding="utf-8"))
    if cmd == "exit":
        break
    recv_data = str(sock.recv(1024), encoding="utf-8")
    print (recv_data)
sock.close()

上面的程序有个问题,如果命令执行的结果比较长,那么客户端发送下一个命令过去之后仍然返回上一个命令没接收完的结果,这样的现象我们称作粘包的现象。我们知道TCP传输的是数据流,发送数据时会等缓冲区满了然后再发送,或者等待要发送的时间超时了再发送,几个包组合在一起发送可以提高发送效率,此时也造成了粘包现象的产生,目标机器一次性接收几个包的数据,可能导致本次请求接收多余的数据。粘包还有一种情况,就是本次程序里面出现的情况,因为服务端要发送的数据大于1024,导致客户端无法一次性接收完数据,虽然我们可以修改接收的大小,但是治标不治本。解决方法有我们在发送具体数据前,先将数据的大小发送给客户端,客户端做好接收准备并告诉给服务端,服务端一次性发送数据之后,客户端根据服务器端发送数据的大小进行循环接收,直到数据接收完毕。

改进版

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import socket
import subprocess
 
ip = '0.0.0.0'
port = 8005
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((ip, port))
sock.listen(5)
 
while True:
    conn, addr = sock.accept()
    while True:
        try:
            cmd = str(conn.recv(1024), encoding="utf-8")
            if cmd == "exit":
                conn.close()
                break
            p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
            send_data = p.stdout.read()
            if len(send_data) == 0:
                send_data = "command error"
            else:
                send_data = str(send_data, encoding="gbk")
            send_data = bytes(send_data, encoding="utf-8")
            data_len = len(send_data)
            ready_tag = "Ready|%d" %data_len
            conn.sendall(bytes(ready_tag, encoding="utf-8"))
            start_data = str(conn.recv(1024), encoding="utf-8")
            if start_data.startswith("Start"):
                conn.sendall(send_data)
        except Exception as e:
            print (e)
            break

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import socket
 
ip = '127.0.0.1'
port = 8005
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))
 
while True:
    cmd = input("client>").strip()
    if cmd == "":
        continue
    else:
        sock.sendall(bytes(cmd, encoding="utf-8"))
    if cmd == "exit":
        break
    ready_data = str(sock.recv(1024), encoding="utf-8")
    if ready_data.startswith("Ready"):
        msg_size = int(ready_data.split("|")[-1])
    start_tag = "Start"
    sock.sendall(bytes(start_tag, encoding="utf-8"))
    msg = ""
    recv_size = 0
    while recv_size < msg_size:
        recv_data = sock.recv(1024)
        recv_size += len(recv_data)
        msg += str(recv_data, encoding="utf-8")
    print (msg)
sock.close()

二、IO多路复用

IO多路复用指通过一种机制,可以监视多个描述符,一旦某个描述符就绪,就能够通过程序进行相应的读写操作

Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用

对于select:

1
2
3
4
5
6
7
8
9
10
11
句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)
  
参数: 可接受四个参数(前三个必须)
返回值:三个列表
  
select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。
1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中
2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中
3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中
4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
   当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import select
 
ip = "0.0.0.0"
port = 8003
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((ip, port))
sock.listen(5)
 
inputs = [sock]
outputs = []
message_queues = {}
 
while True:
    read_list, write_list, error_list = select.select(inputs, outputs, inputs, 1)
    print ("inputs: %d outputs: %d read_list: %d write_list: %d" %(len(inputs), len(outputs), len(read_list), len(write_list)))
    for r in read_list:
        if r is sock:
            conn, addr = r.accept()
            conn.sendall(bytes("welcome to here", encoding="utf-8"))
            inputs.append(conn)
            message_queues[conn] = []
        else:
            data = str(r.recv(1024), encoding="utf-8")
            if data == "exit":
                if r in outputs:
                    outputs.remove(r)
                inputs.remove(r)
                r.close()
                del message_queues[r]
            else:
                message_queues[r].append(data)
                if r not in outputs:
                    outputs.append(r)
 
    for w in write_list:
        data = message_queues[w].pop()
        w.sendall(bytes(data, encoding="utf-8"))
        if len(message_queues[w]) == 0:
            outputs.remove(w)
 
    for e in error_list:
        inputs.remove(e)
        if e in outputs:
            outputs.remove(e)
        e.close()
        del message_queues[e]

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket
 
ip = "127.0.0.1"
port = 8003
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))
 
while True:
    data = str(sock.recv(1024), encoding="utf-8")
    print ("server>%s" %data)
    send_data = input("client>").strip()
    if not send_data:
        send_data = "empty"
    sock.sendall(bytes(send_data, encoding="utf-8"))
    if send_data == "exit":
        exit()
sock.close()

三、SocketServer

SocketServer内部使用 IO多路复用 以及多线程和多进程,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求

Python 网络编程(二)
SocketServer有以下几种类型:

  • socketserver.TCPServer

  • socketserver.UDPServer

  • socketserver.UnixStreamServer

  • socketserver.UnixDatagramServer

​每种类型都可以通过多线程或者多进程的方式处理多个客户的请求,这里介绍ThreadingTCPServer的使用方式:

  1. 创建一个继承自 SocketServer.BaseRequestHandler 的类

  2. 类中必须定义一个名称为 handle 的方法

  3. 启动ThreadingTCPServer

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socketserver
 
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):
        request = self.request
        Flag = True
        while Flag:
            data = str(request.recv(1024), encoding="utf-8")
            if data == "exit":
                request.close()
                Flag = False
            else:
                request.sendall(bytes(data, encoding="utf-8"))
 
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('0.0.0.0', 8888), Myserver)
    server.serve_forever()

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket
 
ip = '127.0.0.1'
port = 8888
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.connect((ip, port))
 
while True:
    send_data = input("client>")
    sk.sendall(bytes(send_data, encoding="utf-8"))
    if send_data == "exit":
        sk.close()
        break
    recv_data = str(sk.recv(1024), encoding="utf-8")
    print("server>%s" % recv_data)