Python自动化运维 - day8 - socket编程

概述

  自从互联网诞生以来,现在基本上所有的程序都是网络程序,很少有单机版的程序了。

  计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。

  举个例子,当你使用浏览器访问新浪网时,你的计算机就和新浪的某台服务器通过互联网连接起来了,然后,新浪的服务器把网页内容作为数据通过互联网传输到你的电脑上。

  由于你的电脑上可能不止浏览器,还有QQ、微信、邮件客户端等,不同的程序连接的别的计算机也会不同,所以,更确切地说,网络通信是两台计算机上的两个进程之间的通信。比如,浏览器进程和新浪服务器上的某个Web服务进程在通信,而QQ进程是和腾讯的某个服务器上的某个进程在通信。

  网络编程对所有开发语言都是一样的,Python也不例外。用Python进行网络编程,就是在Python程序本身这个进程内,连接别的服务器进程的通信端口进行通信。

TCP/IP协议基础

  计算机为了联网,就必须规定通信协议,早期的计算机网络,都是由各厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就好比一群人有的说英语,有的说中文,有的说德语,说同一种语言的人可以交流,不同的语言之间就不行了。

  后来为了打破这个局面,出现了一套全球通用协议族,叫做互联网协议,互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。

  通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,是由4个点分十进制数组成(例如:12.21.21.41)。

  下面是TCP/IP协议分层:

  TCP/UDP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。相对于TCP(面向连接)来说,UDP则是面向无连接的协议,使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。

  许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。

  一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。那么端口有什么作用呢?在两台计算机通信时,只发IP地址是不够的,因为同一台计算机上跑着多个网络程序。一个IP包来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,这样,两个进程在两台计算机之间建立网络连接就需要各自的IP地址和各自的端口号。

TCP编程

  Socket成为安全套接字,是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。

客户端:

  大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

import  socket
client = socket.socket(socke.AF_INET,socket.SOCK_STREAM)  #指定这个socket链接的协议,以及指定数据流的类型
client.connect(('127.0.0.1',8080))  #连接server端,需要知道服务端的IP和PORT(元组的形式)
client.send('hello'.encode='UTF-8')  #发送消息,注意Python3中,传输的数据都是bytes格式的
server_msg = client.recv(1024)  #接收1024个字节的数据
print(server_msg)
  • socke.AF_INET 指的是使用 IPv4

  • socket.SOCK_STREAM 指定使用面向流的TCP协议

服务端:

  和客户端编程相比,服务器编程就要复杂一些。

import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 实例化一个链接
server.bind(('127.0.0.1',8080)) #server端监听一个地址,等待client连接
server.listen(5)  # 指定TCP连接池的可用连接个数,Linux中的backlog概念
conn,addr = server.accept() # 接收客户端连接,获取客户端的信息,会返回两个元素,连接标识符,和客户端的地址/端口(元组的形式)
client_msg = conn.recv(1024)   #接收1024个字节的数据
print(client_msg)
conn.send(client_msg.upper())   #通过连接标识符发送数据给客户端
conn.close()  # 关闭连接
server.close()  # 服务端关闭端口
  • 小于1024的端口只有管理员才可以指定

通讯循环及客户端发空消息时的问题

抛出问题:

  1. 通讯不应该是单次的,应该至少是多次的

  2. 如果我们发送的消息为空的时候,就会卡住,服务端无法接受,客户端无法继续发送

针对问题做如下改进:

服务端:

  增加循环,完成通信循环,并且把客户端发来的消息转换成大写的并返回。

import socket

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
print('wait for connect')
conn,addr = server.accept()
print('client connect',addr)
while True:    #循环的接受消息
    client_msg = conn.recv(1024)
    print('client msg :', client_msg)
    conn.send(client_msg.upper())

conn.close()
server.close()

客户端:

  增加循环,完成通信循环,并且发送的消息由用户来输入,当输入为空的时候,继续循环。

import socket

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:    #通信循环
    msg = input('>>:').strip()
    if not msg:continue        #当用户输入为空的时候,继续循环
    client.send(msg.encode('utf-8'))
    server_msg = client.recv(1024)
    print(server_msg.decode('utf-8'))
client.close()

链接循环及客户端强制退出时的问题

抛出问题:

  1. 当客户端异常关闭一个链接的时候,服务端也会产生异常
    • windows下会异常退出(由于tcp是双向链接的,客户端异常退出,那么服务端就不能继续循环的收发消息了)
    • Linux下会进入死循环(收到了空消息)
  2. 当一个客户端连接断开,服务端应该可以继续接受其它客户端发来的消息

由于问题集中在服务端,所以对服务端做如下改进:

服务端:

  添加链接循环,当一个链关闭时,可以继续接受其他链接。

  添加异常处理,当客户端异常关闭时,主动的关闭服务端的链接。

import socket

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:     #链接循环
    print('wait for connect')
    conn,addr = server.accept()
    print('client connect',addr)
    while True:
        try:       #Windows下捕捉客户端异常关闭连接
            client_msg = conn.recv(1024)
            if not client_msg:break     #Linux下处理客户端异常退出问题
            print('client msg :', client_msg)
            conn.send(client_msg.upper())
        except (ConnectionResetError,Exception):   #except可以同时指定多个异常
            break
    conn.close()
server.close()

  客户端异常关闭时,服务端的异常为:ConnectionResetError,我们可以通过捕捉其,来控制服务端的推出,也可以使用 Exception(通用)异常来捕捉。

模拟远程执行命令

利用socket,远程执行命令,并返回,模拟ssh的效果

  1. 执行命令使用subprocess模块的Popen和PIPE
  2. 注意subprocess的Popen模块执行结果就是bytes格式的str,所以不用转换即可直接发送

以上需求都针对服务端,那么对服务端做如下修改

import socket
from subprocess import Popen,PIPE

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# server.bind(('192.168.56.200',8080))
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
    print('wait for connect')
    conn,addr = server.accept()
    print('client connect',addr)
    while True:
        try:
            cmd = conn.recv(1024).strip()
            if not cmd:break
            p = Popen(cmd.decode('utf-8'),shell=True,stdout=PIPE,stderr=PIPE)
            stdout,stderr = p.communicate()   #执行的结果就是bytes格式的string
            if stderr:
                conn.send(stderr)
            else:
                conn.send(stdout)
        except (ConnectionResetError,Exception):
            break
    conn.close()
server.close()

粘包问题

  由于我们在接受和发送数据的时候,都指定了每次接收1024个字节的数据,而发送的数据我们是不可估量的,如果发送的时候超过1024字节,那么在接收端就无法一次收取完毕,这些数据会存放在操作系统缓存中,那么下次再接收1024字节的数据的时候,会从缓存中继续读取,那么就会发生粘包现象。

  所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

只有TCP有粘包现象,UDP永远不会粘包
  • UDP是面向报文的,发送方的UDP对应用层交下来的报文,不合并,不拆分,只是在其上面加上首部后就交给了下面的网络层,也就是说无论应用层交给UDP多长的报文,它统统发送,一次发送一个。而对接收方,接到后直接去除首部,交给上面的应用层就完成任务了。因此,它需要应用层控制报文的大小
  • TCP是面向字节流的,它把上面应用层交下来的数据看成无结构的字节流来发送,可以想象成流水形式的,发送方TCP会将数据放入“蓄水池”(缓存区),等到可以发送的时候就发送,不能发送就等着,TCP会根据当前网络的拥塞状态来确定每个报文段的大小。
不是server端直接发送,client端直接接收

服务端

  • 应用程序是运行在用户态的,发送数据的时候,需要去调用物理网卡,而这个操作是不许允许的,必须预先将运行状态切换为内核态才可以操作网卡发送数据,所以引入操作系统缓存的概念。
  • 应用程序把需要进行系统调用(用户态-->内核态的切换)的指令放入操作系统缓存,然后由操作系统统一去执行。

客户端

  • 操作系统把从网卡接收到的数据存入操作系统缓存中去,供应用程序读取
  • 应用程序直接从操作系统缓存中将数据读出,然后进行处理

整个过程如图:

  

解决黏包问题的绝招

发生黏包的本质问题是对端不知道我们发送数据的总长度,如果能否让对方提前知道,那么就不会发生粘包现象。

根据TCP报文的格式得到启发:

  • 发送真正的数据前,需要预先发送本次传送的报文大小(增加报头
  • 报头的长度必须是固定的

1、引入struct模块

  struct.pack()  打包

struct.pack('i',int)
#i表示把数字用4个字节进行表示,这样的话就可以表示2的32次方的数字,已经满足需求
#后面的int表示要打包的数字(要发送的报文长度)
#通过struct.pack 会得到bytes格式的数据,可以直接进行发送

  struct.unpack() 解包

struct.unpack('i',obj)
#obj表示收取到数据
#会返回一个元组,元组的第一个元素为对方传过来的报文长度
#可以复制给一个变量来指定接收的报文长度

2、通过struct传递包头解决粘包问题

# 服务端 
#!/usr/bin/env python
# Author:Lee Sir 
#_*_ coding:utf-8 _*_ 

import socket 
from subprocess import Popen,PIPE 
import struct 

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
server.bind(('127.0.0.1',8000)) 
server.listen(5) 

while True: 
    print('等待连接......') 
    conn,addr = server.accept() 
    print('客户端地址为:',addr) 
        while True: 
            try: 
                cmd_bytes = conn.recv(1024) 
                if not cmd_bytes:continue 
                cmd_str = cmd_bytes.decode('utf-8') 
                print('执行的命令是:',cmd_str) 

                #执行命令 
                p = Popen(cmd_str,shell=True,stdout=PIPE,stderr=PIPE) 
                stdout,stderr = p.communicate() 

                #返回的数据 
                if stderr: 
                    send_data = stderr 
                else: 
                    send_data = stdout 

                #构建报头并发送报头 
                conn.send(struct.pack('i',len(send_data))) 

                #发送数据 
                conn.send(send_data) 
            except Exception: 
                break 


#客户端 

#!/usr/bin/env python 
# Author:Lee Sir 
#_*_ coding:utf-8 _*_ 

import socket 
import struct 

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8000)) 

while True: 
    msg = input('Please input msg: ') 
    if not msg:continue 
    client.send(msg.encode('utf-8')) 

    #接收报头,服务端使用i模式,所以固定是4个字节 
    server_data_head = client.recv(4) 
    server_data_len = struct.unpack('i',server_data_head)[0] 

    #根据传递的报头长度接收报文 
    server_data = client.recv(server_data_len) 
    print(server_data.decode('gbk'))

3、当数据量比较大以及需要额外其他数据的场合下,以上的解决方案就有问题

  • 数据量非常大,上百T,在打包的时候有可能struct.pack的i模式无法满足需求,因为只能打长度为2的32次方的数据,虽然可以使用Q模式,支持2的64次方,但是也不能准确的预测是否满足数据的最大长度,另外客户端直接接受那么大的数据就显得非常笨拙,也很吃力
  • 在下载的场景下,我们可能需要的数据还有文件名、以及hash值

针对上面的问题有以下解决方案:

  1. 客户端接收的时候分段接收
  2. 定义字典记录报文的长度,以及其他需求:比如filename,hash值等其他信息

服务端

#!/usr/bin/env python
# Author:Lee Sir
#_*_ coding:utf-8 _*_

import socket
from subprocess import Popen,PIPE
import struct
import json

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(('127.0.0.1',8080))
server.listen(5)

while True:
    print('等待连接......')
    conn,addr = server.accept()
    print('客户端地址为:',addr)
    while True:
        try:
            cmd_bytes = conn.recv(1024)
            if not cmd_bytes:continue
            cmd_str = cmd_bytes.decode('utf-8')
            print('执行的命令是:',cmd_str)

            #执行命令
            p = Popen(cmd_str,shell=True,stdout=PIPE,stderr=PIPE)
            stdout,stderr = p.communicate()

            #返回的数据
            if stderr:
                send_data = stderr
            else:
                send_data = stdout

            #创建报头内容及获取包头长度
            file_dict = {'filename':None,'hash':None,'size':len(send_data)}
            file_json = json.dumps(file_dict).encode('utf-8')
            file_json_len = len(file_json)

            #构建报头
            file_head = struct.pack('i',file_json_len)

            #发送报头长度
            conn.send(file_head)

            #发送报头
            conn.send(file_json)

            #发送数据
            conn.send(send_data)

        except Exception:
            break

客户端

#!/usr/bin/env python
# Author:Lee Sir


import socket
import struct
import json

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))

while True:
    msg = input('Please input msg: ')
    if not msg:continue
    client.send(msg.encode('utf-8'))

    #接收报头,服务端使用i模式,所以固定是4个字节
    server_file_head = client.recv(4)
    server_file_len = struct.unpack('i',server_file_head)[0]

    #接收报文头部信息
    server_head_file = client.recv(server_file_len)

    #报文头部信息
    server_head =  json.loads(server_head_file.decode('gbk'))

    #获取报文的头部信息
    server_file_name = server_head['filename']
    server_file_hash = server_head['hash']
    server_file_size = server_head['size']

    #根据传递的报头长度分段接收报文
    recv_len = 0
    server_data = b''
    while recv_len < server_file_size:
        recv_data = client.recv(1024)
        server_data += recv_data
        recv_len += len(recv_data)

    print(server_data.decode('gbk'))

UDP编程

使用udp编程和使用tcp编程用于相似的步骤,而因为udp的特性,所以它的服务端不需要监听端口,并且客户端也不需要事先连接服务端。

服务端:

import socket


server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)      # 指定socket的协议,UDP使用的是SOCK_DGRAM
server.bind(('127.0.0.1', 9999))                               # 绑定端口

print('UDP Server is Starting...')
data, addr = server.recvfrom(1024)                             # 接受(包含数据以及客户端的地址)
print('Received from {}'.format(addr))
server.sendto('hello,{}'.format(addr).encode('utf-8'), addr)   # 应答,格式为(应答的数据,客户端的IP和Port元组)

客户端:

import socket


client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)            # 指定socket的协议,UDP使用的是SOCK_DGRAM
client.sendto('hello world'.encode('utf-8'), ('127.0.0.1', 9999))    # 发送数据,格式为(发送的数据,服务端的IP和Port元组)
print(client.recv(1024).decode('utf-8'))                             # 同样使用recv来接受服务端的应答数据

PS:UDP的使用与TCP类似,但是不需要建立连接。此外,服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。 

  

转载于:https://www.cnblogs.com/dachenzi/articles/7067128.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值