python 通讯管理机_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的端口只有管理员才可以指定

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

抛出问题:

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

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

针对问题做如下改进:

服务端:

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

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()

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

抛出问题:

当客户端异常关闭一个链接的时候,服务端也会产生异常

windows下会异常退出(由于tcp是双向链接的,客户端异常退出,那么服务端就不能继续循环的收发消息了)

Linux下会进入死循环(收到了空消息)

当一个客户端连接断开,服务端应该可以继续接受其它客户端发来的消息

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

服务端:

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

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

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的效果

执行命令使用subprocess模块的Popen和PIPE

注意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值

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

客户端接收的时候分段接收

定义字典记录报文的长度,以及其他需求:比如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端口可以各自绑定。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值