Python网络编程

文章目录结构如下:

  • TCP/UDP介绍
  • Socket套接字
  • TCP网络通信
  • UDP网络通信
  • 实现执行命令

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

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

由于电脑上布置浏览器,还有其他的应用程序,所以更确切地说,网络通信是两台计算机上的两个进程之间的通信。

TCP/UDP介绍

互联网协议包含了上百种协议标准,但最重要的两个协议是TCP和IP协议,所以大家把互联网协议简称为TCP/IP协议。

主机中常常有多个应用进程同时在与外部通信(比如浏览器和QQ同时运行),下图中,A 主机的 AP1 进程在与 B 主机的 AP3 进程通信,同时主机 A 的 AP2 进程也在与 B 主机的 AP4 进程通信。

两个主机的传输层之间有一个灰色双向箭头,写着“传输层提供应用进程间的逻辑通信”。

逻辑通信:看起来数据似乎是沿着双向箭头在传输层水平传输的,但实际上是沿图中的虚线经多个协议层次而传输。

图片描述

TCP/IP 协议栈 传输层有两个重要协议——UDP 和 TCP,不同的应用进程在传输层使用TCP 或 UDP 之一。

端口

同一台主机上的不同应用程序之间通过端口来进行区分,端口号有 0 ~ 65535 的编号,其中:

  • 编号 0 ~ 1023 为系统端口号,这些端口号可以在网址 www.iana.org 查询到,它们被指派给了 TCP/IP 中最重要的一些应用程序,以下是一些常见的端口号:
应用层协议: FTP TELNET SMTP DNS TFTP HTTP SNMP
系统端口号: 21 23 25 53 69 80 161
  • 编号 1024 ~ 49151 为登记端口号,为没有系统端口号的应用程序使用,使用这类端口号必须在 IANA 按规定手续登记,以防止重复。
  • 编号 49152 ~ 65535 为短暂端口号,是留给客户进程选择暂时使用的,使用结束后,这类端口会被放开一共其他程序使用。

UDP协议

UDP(User Datagram Protocol)用户数据报协议,它的主要特点是:

  • UDP 是无连接的,发送数据之前不需要建立连接,减小了开销和时延。
  • UDP 尽最大努力交付,不保证交付可靠性。
  • UDP 是面向报文的,对于从应用层交付下来的 IP 数据报,只做很简单的封装(8字节 UDP 报头),首部开销小。
  • UDP 没有拥塞控制,出现网络拥塞时发送方也不会降低发送速率。这种特性对某些实时应用很重要,比如 IP 电话,视频会议等。
  • UDP 支持一对一,一对多,多对一和多对多的交互通信。

从应用层到传输层,再到网络层的各层次封装:

图片描述

UDP 数据报可分为两部分:UDP 报头和数据部分。其中数据部分是应用层交付下来的数据。UDP 报头总共 8 字节,而这 8 字节又分为 4 个字段:

图片描述

  • 源端口:2 字节,在对方需要回信时可用,不需要时可以全0
  • 目的端口:2 字节,必须,也是最重要的字段
  • 长度:2 字节,长度值包括报头和数据部分
  • 校验和:2 字节,用于检验 UDP 数据报在传输过程中是否有出错,有错就丢弃

TCP协议

TCP(Transmission Control Protocol)传输控制协议,它的主要特点如下:

  • TCP 提供可靠的数据传输服务,TCP 是面向连接的
  • TCP 连接是点对点的,一条 TCP 连接只能连接两个端点
  • TCP 提供可靠传输,无差错,不丢失,不重复,按顺序
  • TCP 提供全双工通信,允许通信双方任何时候都能发送数据,因为 TCP 连接的两端设有发送缓存和接收缓存
  • TCP 是面向字节流。TCP 并不知道所传输的数据的含义,仅把数据看作一连串的字节序列,它不保证接收方收到的数据块和发送方发出的数据块具有大小对应关系
报文

TCP 报文段分为两部分:报头和数据部分。数据部分是上层应用交付的数据,报头是TCP功能的关键。

TCP 报头有前 20 字节的固定部分,后面 4n 字节是根据需要而添加的字段,如图所示

图片描述

20 字节的固定部分,各字段功能说明:

  • 源端口和目的端口:2 字节,分别为源端口号和目的端口号。
  • 序号:4 字节,范围[0, 2^32-1],序号增加到最大后,下一个序号从零开始。通过 TCP 传送的字节流中的每个字节都按顺序编号,报头中的序号字段值指的是本报文段数据的第一个字节的序号
  • 确认序号:4 字节,期望收到对方下个报文段的第一个字节的序号。
  • 数据偏移:4 位,指 TCP 报文段的报头长度,包括固定的 20 字节和选项字段。
  • 保留:6 位,保留为今后使用,目前为零。
  • 控制位:共有 6 个控制位,说明报文的性质,意义如下:
    • URG 紧急:当 URG=1 时,它告诉系统此报文中有紧急数据,应优先传送(比如紧急关闭),这要与紧急指针字段配合使用。
    • ACK 确认:仅当 ACK=1 时确认号字段才有效。建立 TCP 连接后,所有报文段都必须把 ACK 字段置为 1。
    • PSH 推送:若 TCP 连接的一端希望另一端立即响应,PSH 字段便可以“催促”对方,不再等到缓存区填满才发送。
    • RST 复位:若 TCP 连接出现严重差错,RST 置为 1,断开 TCP 连接,再重新建立连接。
    • SYN 同步:用于建立和释放连接。
    • FIN 终止:用于释放连接,当 FIN=1,表明发送方已经发送完毕,要求释放 TCP 连接。
  • 窗口:2 字节。窗口值是指发送者自己的接收窗口大小,因为接收缓存的空间有限。
  • 校验和:2 字节。和 UDP 报文一样,用于检查报文是否在传输过程中出差错。
  • 紧急指针:2 字节。当 URG=1 时才有效,指出本报文段紧急数据的字节数。
  • 选项:长度可变,最长可达 40 字节。
连接建立与释放

在传输 TCP 报文之间需要创建连接,发起方称为客户端,响应连接请求的一方被称为服务端,这个创建连接的过程被称为三次握手

图片描述

  • 客户端发出请求连接报文段,其中报头控制位 SYN=1,初始序号 seq=x。客户端进入 SYN-SENT(同步已发送)状态。
  • 服务端收到请求报文段后,向客户端发送确认报文段。确认报文段的首部中 SYN=1,ACK=1, 确认号是 ack=x+1,同时为自己选择一个初始序号 seq=y。服务端进入 SYN-RCVD(同步收到)状态。
  • 客户端收到服务端的确认报文段后,还要给服务端发送一个确认报文段。这个报文段中 ACK=1,确认号 ack=y+1,而自己的序号为 seq=x+1。这个报文段已经可以携带数据,如果不携带数据则不消耗序号,则下一个报文段序号仍为 seq=x+1
  • 至此 TCP 连接已经建立,客户端进入 ESTABLISHED(已建立连接)状态,当服务端收到确认后,也进入 ESTABLISHED 状态,它们之间便可以正式传输数据了。

当传输数据结束后,通信双方都可以释放连接,这个释放连接过程被称为释放连接,释放连接的过程被称为四次挥手:

图片描述

  • 此时 TCP 两端都还处于 ESTABLISHED 状态,客户端停止发送数据,并发出一个 FIN 报文段。首部 FIN=1,序号 seq=u(u 等于客户端传输数据最后一字节的序号加 1)。客户端进入 FIN-WAIT-1(终止等待1)状态。

  • 服务端恢复确认报文段,确认号 ack=u+1,序号 seq=v(v 等于服务端传输数据最后一字节的序号加 1),服务端进入 CLOSE-WAIT(关闭等待)状态。现在 TCP 连接处于半开半闭状态,服务短短如果继续发送数据,客户端依然接收。

  • 客户端收到确认报文,进入 FIN-WAIT-2状态。服务端发送完数据后,发出 FIN 报文段,FIN=1,确认号 ack=u+1,序号 seq=w,然后进入 LAST-ACK(最后确认)状态。

  • 客户端回复确认报文段,ACK=1,确认号 ack=w+1(w为半开半闭状态时,收到的最后一个字节数据的编号),序号 seq=u+1,然后进入 TIME-WAIT(时间等待)状态。

此时连接还没有释放,需要等待状态结束后(4分钟)连接连接两端才会 CLOSED。设置等待时间是因为有可能最后一个确认报文丢失而需要重传。

特点

超时重传

TCP 规定,接收者收到数据报文段后,需回复一个确认报文段,以告知发送者数据已经收到。而发送者如果一段时间内(超时计时器)没有收到确认报文段,便重复发送。

为实现超时重传,需要注意:

  • 发送者发送一个报文段后,暂时保存改报文段的副本,为发生超时重传时使用,收到确认报文后删除该报文段。
  • 确认报文段也需要序号,才能明确是发出去的哪个数据报得到了确认。
  • 超时计时器比传输往返时间略长,但具体值是不确定的,根据网络情况而变。

连续 ARQ 协议

超时重传机制很费时间,每发送一个数据包都要等待确认。在实际应用中采用了流水线传输:发送方可以连续发送多个报文段(连续发送的数据长度叫做窗口),而不必每发完一段就停下来等待确认。接收方也不必对每个报文都做回复,而是采用累积确认的方式:接收者收到多个连续的报文段后,只回复确认最后一个报文段,表示在这之前的数据都已收到。

图片描述

流量控制和拥塞控制

由于接收方缓存的限制,发送窗口不能大于接收方接收窗口。在报文段首部就有一个字段就叫做窗口(rwnd),即告诉对方自己的接收窗口大小,窗口的大小是可以变化的。

TCP 对于拥塞控制总结为:慢启动,加性增,乘性减,如图所示:

6-7-1

  • 慢启动:初始窗口值很小,但是按指数规律渐渐增长,直到达到慢开始门限(ssthresh)
  • 加性增:窗口值达到慢开始门限后,每发送一个报文段,窗口值增加一个单位量。
  • 乘性减:无论什么阶段,只要出现超时,则把窗口值减小一半。

Socket套接字

Socket 又称“套接字”,应用程序通常通过套接字向网络发出请求活着应答网络请求,使主机间或者一台计算机上的进程间可以通讯。

socket() 函数

在 Python 中,使用 socket() 函数创建套接字,语法如下:

socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

参数:

  • family:套接字家族可以是 AF_UNIX 或者 AF_INET,AF_UNIX 用于单一的 Unix 系统进程间通信,AF_INET 表示使用 IPv4 进行通信。
  • 套接字类型根据面向连接的还是非连接分为 SOCK_STREAM 和 SOCK_DGRAM,即 TCP 和 UDP 的区别。
  • protocol:一般不填,默认为 0

Socket 对象方法

服务端套接字 描述
s.bind() 绑定地址(host,port)到套接字, 在 AF_INET 下,以元组(host,port)的形式表示地址。
s.listen() 开始 TCP 监听。backlog 指定在拒绝连接之前,操作系统可以挂起得到最大连接数。该值至少为 1,大部分应用程序设置为 5 就可以了。
s.accept() 被动接受 TCP 客户端连接,(阻塞式)等待连接的到来
客户端套接字 描述
s.connect() 主动初始化 TCP 服务器连接,一般 address 的格式为元组(hostname,port),如果连接出错,返回 socket.error 错误
s.connect_ex() connect() 函数的扩展版本,出错时范围出错码,而不是抛出异常,执行成功是返回 0
公共方法 描述
s.recv() 接受 TCP 数据,数据以字符串形式返回,bufsize 指定要接受的最大数据量,flag 提供有关消息的其他信息,通常可以忽略
s.send() 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小
s.sendall() 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom() 接收 UDP 数据,与 recv() 类似,但返回值是(data,address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址
s.sendto() 发送 UDP 数据,将数据发送到套接字,address 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字
s.settimeout() 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())

注意事项:

  1. Python3以后,socket传递的都是bytes类型的数据,字符串需要先转换一下,string.encode()即可;另一端接收到的bytes数据想转换成字符串,只要bytes.decode()一下就可以。
  2. 在正常通信时,accept()recv()方法都是阻塞的。所谓的阻塞,指的是程序会暂停在那,一直等到有数据过来。

TCP 网络通信

客户端 / 服务器架构,即 Client-Server 架构,简称 CS 架构,它是最常见的网络编程设计模式。

套接字通信流程如下

img

服务器端代码 tcp_serversocket.py

import socket

# 创建套接字
serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = socket.gethostname()
# print('serverhost:',host)
port = 9999

# 绑定地址及监听
ADDR = (host, port)
serversock.bind(ADDR)
serversock.listen()

# 等待连接
print('等待客户端连接......')
sock,ip = serversock.accept()
print('已连接')

while True:
	accept_message = sock.recv(1024).decode()
	print('client message:',accept_message)
	if accept_message.lower() == 'exit':
		break
	send_message = input('>>>:')
	sock.send(send_message.encode())
	if send_message.lower() == 'exit':
		break

sock.close()
serversock.close()

客户端代码 tcp_clientsocket.py

import socket

# 创建套接字
clientsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = socket.gethostname()
# print('clienthost:',host)
port = 9999

# 连接服务端
clientsock.connect((host, port))

while True:
	send_message = input('>>>')
	clientsock.send(send_message.encode())
	if send_message.lower() == 'exit':
		break
	accept_message = clientsock.recv(1024).decode()
	print('server message:',accept_message)
	if accept_message.lower() == 'exit':
		break

clientsock.close()

将两个文件保存在同一目录下,首先运行 python tcp_serversocket.py 程序,然后运行 python tcp_clientsocket.py 程序,可以实现双方交互信息输入。当一方输入 exit 时,双方断开。

效果如下

image-20211017190742773

image-20211017190940250

UDP 网络通信

UDP 通信相对 TCP 要简单些,不需要服务器进行监听与建立连接,相对的在发送数据与接收数据时需要注意 Python 方法的用法。

服务器端代码 udp_serversocket.py

import socket

serversock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
host = socket.gethostname()
port = 9999
ADDR = (host, port)
serversock.bind(ADDR)

while True:
	# recvfrom 的返回值,第一个是 消息,第二个是地址
	message, address = serversock.recvfrom(1024)
	print('client message:', message.decode()) 
	if message.decode() == 'exit':
		break

serversock.close()

客户端代码 udp_clientsocket.py

import socket

clientsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
host = socket.gethostname()
port = 9999
ADDR = (host, port)

while True:
	message = input('>>>')
	# sendto 的第1个参数是 消息,第2个参数是 地址
	clientsock.sendto(message.encode(), ADDR)
	if message == 'exit':
		break

clientsock.close()

效果如图

image-20211017194146985

image-20211017194203171

实现执行命令

利用客户端执行 linux 云服务器命令。

注意:本地需绑定云服务器的公网 IP 地址,而云服务器程序绑定自己的内网 IP。

Python3 提供 subprocess 模块,它允许启动一个新进程,并连接到它们的输入/输出/错误管道,从而获取返回值。

Popen 是 subprocess 的核心,子进程的创建和管理都靠它处理。

subprocess.Popen(args, bufsize=-1, stdin=None, stdout=Nonek, stderr=None, shell=False, cwd=None, env=None)

  • args:shell 命令,可以是字符串或者序列类型
  • bufsize:缓冲区大小,当创建标准流的管道对象时使用,默认 -1
    • 0:不使用缓冲区
    • 1:表示行缓冲,仅当universal_newlines=True时可用,也就是文本模式
    • 正数:表示缓冲区大小
    • 负数:表示使用系统默认的缓冲区大小。
  • stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
  • shell:如果该参数为 True ,将通过操作系统的 shell 执行指定的命令
  • cwd:用于设置子进程的当前目录
  • env:用于指定子进程的环境变量。如果 env = None,子进程的环境变量从父进程中继承

服务端的代码 tcp_server_command.py

#! /usr/bin/env python
# coding:utf8
import socket	#socket 模块
import subprocess	# 执行系统命令模块
import time
# 获取服务端内网 IP 地址
def get_host_ip():
	try:
        s = socket.socket()
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
    finally:
        s.close()
    return ip

HOST = get_host_ip()
PORT = 9999
s = socket.socket()		# 创建 TCP 套接字
s.bind((HOST,PORT))		# 绑定地址
s.listen(5)		# 开始监听
while True:
    conn, addr = s.accept()	
    print('Connected by', addr)		# 输出客户端的 IP 地址
    print('conn by', conn)
    flag = True
    while flag:
        try:
            cmd = conn.recv(1024)	# 接受数据
            cmdstr = cmd.decode('utf-8')
            r = subprocess.Popen(cmdstr, shell=True, stdout=subprocess.PIPE, srderr=subprocess.PIPE)
            stdout = r.stdout.read()
            stderr = r.stderr.read()
            time.sleep(2)
            print(stdout)
            print(stderr)
            if stderr:
                conn.send(stderr)
            else:
                conn.send(stdout)
        except Exception as e:
            flag = False
            # print(f'client abnormal interrupt:{e}')
    conn.close()
s.close()          

客户端代码 tcp_client_command.py

#! /usr/bin/env/ python
# coding:utf8
import socket
HOST = '云服务器公网 IP '
PORT = 9999
s = socket.socket()
s.connect((HOST, PORT))		# 客户端连接服务端
while True:
    try:
        cmd = input('请输入一个命令>>>')
        s.send(cmd.encode('utf-8'))
        # 客户端是 Windows,执行的命令是 GBK 格式,传回来的数据需要进行 GBK 解码
        result = s.recv(102400).decode('gbk')
        print(result)
    except Exception as e:
        print(f"client received's data exception occurred:{e}")
s.close()

注意将客户端代码中的 HOST 改为自己云服务器的公网 IP。执行命令前确保云服务器的相应端口能够接受 TCP 数据,可以再云服务器控制台界面添加规则,开放端口。按 Ctrl+C 中断程序。

服务端效果如下

image-20211018101236710

客户端效果如下:

image-20211018101009658

即可实现在客户端输入命令,并在云服务器上执行命令,将命令结果返回到客户端进行显示。

之前只是对网络通信有一定的了解,但没接触过用程序执行远程命令,猜想 Xshell 这种是不是和这种类似,只是使用的是 ssh 和 22 端口。

希望自己能坚持下去,加油!

参考资料:

蓝桥云课:《TCP IP 网络协议基础入门》

Python 3 网络编程:https://www.runoob.com/python3/python3-socket.html

Python3-实现网络通信:https://blog.csdn.net/xianjie0318/article/details/107128160

原文地址:https://www.cnblogs.com/augustine0654/p/15510845.html