【Python自动化运维之路Day9】Socket

时间:2021-03-06 06:01:42

socket也可以认为是套接字是一种源IP地址和目的IP地址以及源端口号和目的端口号的组合.网络化的应用程序在开始任何通讯之前都必须要创建套接字.就像电话的插口一样,没有它就没办法通讯.

socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。

看图说话socket作用:

【Python自动化运维之路Day9】Socket

 

如下图是socket的工作流程:

【Python自动化运维之路Day9】Socket

 

 

根据连接启动的方式以及本地 套接字要连接的目标,套接字之间的连接过程可以分为三个步骤: 服务器监听,客户端请求,连接确认。
(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
(2)客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和 端口号,然后就向服务器端套接字提出连接请求。
(3)连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把 服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端 套接字继续处于 监听状态,继续接收其他客户端套接字的连接请求。
 
可以把socket连接过程想象成打电话,还是看实例更加形象,:

 

【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
import socket
ip_port=('127.0.0.1',8888)
#买手机
s=socket.socket()

#插卡
s.bind(ip_port)
#开机
s.listen(5)   #最大连接5个

#等待电话
conn,addr=s.accept()
    #conn就是电话的链路
    #addr客户端的地址

#收消息
recv_data=conn.recv(1024)
print("客户端发送类型:",type(recv_data),recv_data)

#发消息
send_data=recv_data.upper()
conn.send(send_data)

#挂电话
conn.close()

socket_server
Socket server
【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
ip_port=("127.0.0.1",8888)

#买手机
s=socket.socket()

#拨号
s.connect(ip_port)
#发送消息
send_data=input(">>>>:").strip()
s.send(bytes(send_data,encoding="utf-8"))

#收消息
recv_data=s.recv(1024)
print(str(recv_data,encoding="utf-8"))
#挂电话
s.close()

socket_client
Socket Client
【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
sk.bind(address)

  s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。必须是,以元组(host,port)的形式表示地址。

sk.listen(backlog)

  开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。

      backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
      这个值不能无限大,因为要在内核中维护连接队列

sk.setblocking(bool)

  是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。

sk.accept()

  接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。

  接收TCP 客户的连接(阻塞式)等待连接的到来

sk.connect(address)

  连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

sk.connect_ex(address)

  同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061

sk.close()

  关闭套接字

sk.recv(bufsize[,flag])

  接受的时候在python3中是字节的形式,so 打印的时候需要转换成字符str

  接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。

sk.recvfrom(bufsize[.flag])

  与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

sk.send(bytes[,flag])

  这个地方需要注意,在python3发送时必须是以字节的形式才能发送否则报错,python2中可以发送字符

  将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。

sk.sendall(bytes[,flag])

  这个也是需要发送字节

  将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

      内部通过递归调用send,将所有内容发送出去。

sk.sendto(string[,flag],address)

  将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。

sk.settimeout(timeout)

  设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )

sk.getpeername()

  返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

sk.getsockname()

  返回套接字自己的地址。通常是一个元组(ipaddr,port)

sk.fileno()

  套接字的文件描述符
Socket的具体参数

下面写了两个实例主要功能是能执行系统的一切命令并拿到返回值:

【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import subprocess #导入执行命令模块
ip_port=('127.0.0.1',9999) #定义元祖
#买手机
s=socket.socket()  #绑定协议,生成套接字
s.bind(ip_port)    #绑定ip+协议+端口:用来唯一标识一个进程,ip_port必须是元组格式
s.listen(5)        #定义最大可以挂起胡链接数
#等待电话
while True:  #用来重复接收新的链接
    conn,addr=s.accept()   #接收客户端胡链接请求,返回conn(相当于一个特定胡链接),addr是客户端ip+port
    #收消息
    while True: #用来基于一个链接重复收发消息
            try: #捕捉客户端异常关闭(ctrl+c)
                recv_data=conn.recv(1024) #收消息,阻塞
                if len(recv_data) == 0:break #客户端如果退出,服务端将收到空消息,退出

                #发消息
                p=subprocess.Popen(str(recv_data,encoding='utf8'),shell=True,stdout=subprocess.PIPE) #执行系统命令,windows平
                                                                                                      # 台命令的标准输出是gbk编码,需要转换
                res=p.stdout.read()   #获取标准输出
                if len(res) == 0:   #执行错误命令,标准输出为空,
                    send_data='cmd err'
                else:
                    send_data=str(res,encoding='gbk')  #命令执行ok,字节gbk---->str---->字节utf-8

                send_data=bytes(send_data,encoding='utf8')


                #解决粘包问题
                ready_tag='Ready|%s' %len(send_data)
                conn.send(bytes(ready_tag,encoding='utf8')) #发送数据长度
                feedback=conn.recv(1024)  #接收确认信息
                feedback=str(feedback,encoding='utf8')

                if feedback.startswith('Start'):
                    conn.send(send_data)  #发送命令的执行结果
            except Exception:
                break
    #挂电话
    conn.close()

socket_server1
Socket Server1
【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
ip_port=('127.0.0.1',9999)
#买手机
s=socket.socket()
#拨号
s.connect(ip_port)  #链接服务端,如果服务已经存在一个好的连接,那么挂起

while True:        #基于connect建立的连接来循环发送消息
    send_data=input(">>: ").strip()
    if send_data == 'exit':break
    if len(send_data) == 0:continue
    s.send(bytes(send_data,encoding='utf8'))

    #解决粘包问题
    ready_tag=s.recv(1024) #收取带数据长度的字节:Ready|9998
    ready_tag=str(ready_tag,encoding='utf8')
    if ready_tag.startswith('Ready'):#Ready|9998
        msg_size=int(ready_tag.split('|')[-1])  #获取待接收数据长度
    start_tag='Start'
    s.send(bytes(start_tag,encoding='utf8')) #发送确认信息

    #基于已经收到的待接收数据长度,循环接收数据
    recv_size=0
    recv_msg=b''
    while recv_size < msg_size:
        recv_data=s.recv(1024)
        recv_msg+=recv_data
        recv_size+=len(recv_data)
        print('MSG SIZE %s RECE SIZE %s' %(msg_size,recv_size))

    print(str(recv_msg,encoding='utf8'))
    #挂电话
s.close()

socket_client1
Socket Client1

 上面说的socket的多线程:

socketserver模块

注意:在python3中都是小写了,在python2中写法是SocketServer

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

ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。

1、ThreadingTCPServer基础

使用ThreadingTCPServer:

  • 创建一个继承自 SocketServer.BaseRequestHandler 的类
  • 类中必须定义一个名称为 handle 的方法
  • 启动ThreadingTCPServer
【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author:Alex Li


#!/usr/bin/env python
# -*- coding:utf-8 -*-
#import SocketServer
import socketserver,json
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # print self.request,self.client_address,self.server
        self.request.sendall(bytes('欢迎致电 10086,请输入1xxx,0转人工服务.',encoding="utf-8"))
        while True:
            data = self.request.recv(1024)
            if len(data) == 0:break
            print("data", data)
            print("[%s] says:%s" % (self.client_address,data.decode() ))

            task_data = json.loads( data.decode()  )
            task_action = task_data.get("action")
            if hasattr(self, "task_%s"%task_action):
               func = getattr(self,"task_%s" %task_action)
               func(task_data)      
            else:
               print("task action is not supported",task_action)

    def task_put(self,*args,**kwargs):
        print("---put",args,kwargs)         
        filename = args[0].get('filename')
        filesize = args[0].get('file_size')
        server_response = {"status":200}
        self.request.send(bytes( json.dumps(server_response), encoding='utf-8'  ))
        f = open(filename,'wb')
        recv_size = 0
        while recv_size < filesize:
            data = self.request.recv(4096)
            f.write(data)
            recv_size += len(data)
            print('filesize: %s  recvsize:%s' % (filesize,recv_size))
        print("file recv success")
        f.close()

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('0.0.0.0',8009),MyServer)
    server.serve_forever()
Socket Server2
【Python自动化运维之路Day9】Socket【Python自动化运维之路Day9】Socket
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author:Alex Li

import socket
import os ,json
ip_port=('192.168.11.150',8009)
#买手机
s=socket.socket()
#拨号
s.connect(ip_port)
#发送消息
welcome_msg = s.recv(1024)
print("from server:",welcome_msg.decode())
while True:
    send_data=input(">>: ").strip()
    if len(send_data) == 0:continue

    cmd_list = send_data.split()
    if len(cmd_list) <2:continue
    task_type = cmd_list[0]
    if task_type == 'put':
        abs_filepath = cmd_list[1]
        if os.path.isfile(abs_filepath):
            file_size = os.stat(abs_filepath).st_size
            filename = abs_filepath.split("\\")[-1]
            print('file:%s size:%s' %(abs_filepath,file_size))
            msg_data = {"action":"put",
                        "filename":filename,
                        "file_size":file_size}

            s.send(  bytes(json.dumps(msg_data),encoding="utf-8")  )
            server_confirmation_msg = s.recv(1024)
            confirm_data = json.loads(server_confirmation_msg.decode())
            if confirm_data['status'] ==200:

                print("start sending file ",filename)
                f = open(abs_filepath,'rb')
                for line in f:
                    s.send(line)

                print("send file done ")

        else:
            print("\033[31;1mfile [%s] is not exist\033[0m" % abs_filepath)
            continue
    else:
        print("doesn't support task type",task_type)
        continue
    #s.send(bytes(send_data,encoding='utf8'))
    #收消息
    recv_data=s.recv(1024)
    print(str(recv_data,encoding='utf8'))
    #挂电话
s.close()
Socket Client2