《Python学习之路 -- 网络编程》

Posted jonas_von

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Python学习之路 -- 网络编程》相关的知识,希望对你有一定的参考价值。

  在前面已经提到过,互联网的本质就是一堆协议,协议就是标准,比如全世界人通信的标准是英语,所有的计算机都学会了互联网协议,那么所有的计算机就可以按照统一的标准去收发信息完成通信了。

  作为普通开发人员的我们,写的软件/程序都是处于应用层上的,然而,想要让软件接入互联网,就必须得通过传输层,也就是必须遵循TCP协议或者UDP协议。这是两个非常复杂的协议,如果遵循原生的协议,那么必然会大大降低效率,所以就有了socket抽象层的概念。socket是应用层与TCP/IP协议族通信的软件抽象层,它是一组接口。它把复杂的TCP/IP协议族隐藏在socket接口后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。所以,我们无需深入了解TCP/UDP协议,socket已经为我们封装好了,只需要遵循socket的规定去编程,写出来的程序自然就是遵循TCP/UDP协议。

  socket编程的核心就是套接字对象,套接字来源于Unix,起初,套接字被设计用在同一台主机上多个应用程序之间的通讯,也被称为进程间通讯或IPC。套接字有两种:一种是文件型,另一种是网络型。基于文件类型的套接字家族(AF_UNIX),unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来获取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信;基于网络类型的套接字(AF_INET),是应用最广泛的一个,在Python中也支持多种地址家族,但是网络编程只使用AF_INET。

  套接字基于不同的协议也会有不同的工作流程,先来说基于TCP协议,是如何进行网络编程的:先从服务器端说起,服务器先初始化socket,然后与端口绑定,对端口进行监听,调用accept阻塞,等待客户端连接。在这时候如果有一个客户端初始化一个socket,然后连接服务器,如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互就结束了。

# 服务器端(基于TCP协议)
import socket
# tcp_server就是一个套接字对象,参数socket.AF_INET表示使用网络类型的套接字,socket.SOCK_STREAM代表遵循TCP协议
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 绑定ip+端口号(ip+端口就能表示互联网中的一个程序)
tcp_server.bind((127.0.0.1,8000))
# 设置最大连接数
tcp_server.listen(5)
# 等待连接,该方法返回一个元组,第一个元素是发送方的套接字对象,第二个元素是一个元组(ip,端口号)
con,address = tcp_server.accept()
# 在服务器端通过操作套接字对象来收发信息
# 接收信息,参数1024表示接收1024个字节的数据
data = con.recv(1024)
# 因为网络传输必须以二进制的方式进行传输,所以接收到的数据必须解码
print(data.decode(utf-8))  # hello jonas
# 服务器端也可以给客户端返回信息
con.send(data.upper())
# 关闭网络通信
con.close()
tcp_server.close()

 

# 客户端(基于TCP协议)
import socket
# 与服务器端一样,先创建套接字对象
tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 与客服端连接
tcp_client.connect((127.0.0.1,8000))
# 连接上即可进行数据的收发
tcp_client.send(hello jonas.encode(utf-8))
# 接收服务器端返回的数据
data = tcp_client.recv(1024)
print(from server,data.decode(utf-8))  # from server HELLO JONAS

 

注意:必须先运行服务器端。

# 服务器端(基于UDP协议)
import socket
# 创建套接字对象,socket.AF_INET表示网络型套接字,socket.SOCK_DGRAM表示基于UDP协议
udp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 绑定程序
udp_server.bind((127.0.0.1,8000))
# 等待接收信息,该方法返回一个元组,第一个元素是发送的数据,第二个元素是发送端的地址:(ip,port)
data,address = udp_server.recvfrom(1024)
print(data.decode(utf-8))  # hello jonas
# 发送数据,第一个参数表示发送的数据,第二个参数表示发送的地址
udp_server.sendto(data.upper(),address)
# 结束通信
udp_server.close()

 

# 客户端(基于UDP协议)
import socket
# 创建套接字对象
udp_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 因为在UDP协议中没有连接一说,所以只要有了套接字对象就可以收发信息了
# 同样地,发送的数据必须是以字节的方式发送,第二个参数代表接收方的地址
udp_client.sendto(hello jonas.encode(utf-8),(127.0.0.1,8000))
# 接收信息,data是接收的数据,address是发送方的地址
data,address = udp_client.recvfrom(1024)
print(data.decode(utf-8))  # HELLO JONAS
udp_client.close()

 

UDP与TCP不一样的是,UDP是无连接的,所以先启动哪一端都不会报错。上面的例子只是介绍了简单的使用,下面再来细说socket的那些事:

实例:基于TCP制作一个远程执行命令的程序

# 服务端
import socket
import subprocess
# 创建套接字对象
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 解决端口占用问题
tcp_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定端口
tcp_server.bind((127.0.0.1,8000))
tcp_server.listen(5)
while True:
    # 等待连接,程序阻塞
    con,address = tcp_server.accept()
    while True:
        cmd = con.recv(1024)
        # 如果接收到的是空的命令,则退出本次循环
        if not cmd:
            continue
        # 通过subprocess解析命令
        result = subprocess.Popen(cmd.decode(utf-8),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        if result.stderr.read():
            cmd_send = result.stderr.read()
        else:
            cmd_send = result.stdout.read()
        con.send(cmd_send)

 

# 客户端
import socket
tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_client.connect((127.0.0.1,8000))
while True:
    cmd = input(<<<)
    tcp_client.send(cmd.encode(utf-8))
    data = tcp_client.recv(1024)
    # subprocess.Popen()执行的结果的编码是跟随系统的(win默认编码gbk,linux默认编码utf-8),也就是说result.stdout.read()读取的数据是gbk编码的,所以在解码的时候需要使用gbk
    print(data.decode(gbk))

  上面的代码已经初步实现了功能了,但是还存在一个问题——黏包。细心的你可能会发现,通过测试发现:如果输入某些返回数据较多的命令时(比如dir),会接收不全数据,遗漏了一部分,然而在下一次输入命令时则会将上一次未取完的数据继续返回,这就是黏包现象。只有TCP有黏包现象,UDP永远不会黏包。发送端可以是1k1k地发送数据,而接收端的应用程序可以2k2k地提走数据,当然也有可能一次提走3k或4k,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序时不可见的,因此TCP协议是面向流的协议,这也是容易出现黏包现象的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据。基于TCP的套接字发送数据时是一段一段的字节流发送的,在接收端看来,根本不知道该文件的字节流是从何处开始,何处结束。所谓的黏包现象主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的。此外,发送端引起的黏包是由TCP协议本身造成的,TCP为提高传输效率,发送端往往要收集到足够多的数据后才发送一个TCP段。如果连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到黏包的数据了。TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了Nagle算法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。正是因为这样,接收端就难于分辨数据了,必须提供科学的拆包机制(也意味着面向流的通信是无消息保护边界的)。UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效服务。不会使用块合并的优化算法,由于UDP支持的是一对多的模式,所以接收端的套接字缓冲区采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易区分处理了,这也就是说,面向消息的通信是有消息保护边界的。TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,而UDP是基于数据报的,即使输入的内容为空,那也不是空消息,UDP协议会帮你封装上消息头。还有,UDP的接收数据时,recvfrom()方法是阻塞的,一个recvfrom()必须对唯一一个sendto(),收完了x个字节的数据就算完成,如果y>x数据就丢失,这也意味着udp根本不会黏包,但是会丢失数据。然而,TCP协议数据不会丢失,没有接收完的数据储存在套接字缓冲区,下次继续接收,已端总是在收到ack时才会清除缓冲区内容,数据是可靠的,但是会出现黏包的现象。

   黏包分为两种情况,第一:发送端需要等待缓冲区慢才发送出去,造成黏包,也就是说,发送数据时间间隔很短,数据很小,会合到一起,产生了黏包(这是优化算法干的事)。

# 服务器端
import socket
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
tcp_server.bind((127.0.0.1,8000))
tcp_server.listen(5)
con,address = tcp_server.accept()
data1 = con.recv(1024)
print(data1 -----,data1.decode(utf-8))  # data1 ----- jonasjerrytom
data2 = con.recv(1024)
print(data2 -----,data2.decode(utf-8))  # data2 ----- 
con.close()
tcp_server.close()
# 客户端
import socket
tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_client.connect((127.0.0.1,8000))
tcp_client.send(jonas.encode(utf-8))
tcp_client.send(jerry.encode(utf-8))
tcp_client.send(tom.encode(utf-8))
tcp_client.close()

客户端给服务器端发送了三条数据,然而服务器端接收时一次就把这三条数据接收到了,这就是黏包的第一种现象。除此以外,黏包还有第二种现象:接收端不及时接收缓冲区的数据包,造成多个包接收。客户端发送一段数据,服务器端只收了一部分,服务区下次再接收的时候还是从缓冲区拿上次遗留的数据,上面用TCP实现远程命令的实例就是出现了这种黏包现象。那么如何解决黏包问题呢?

# 服务端
import socket
import subprocess
import struct
# 创建套接字对象
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 解决端口占用问题
tcp_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定端口
tcp_server.bind((127.0.0.1,8000))
tcp_server.listen(5)
while True:
    # 等待连接,程序阻塞
    con,address = tcp_server.accept()
    while True:
        cmd = con.recv(1024)
        # 如果接收到的是空的命令,则退出本次循环
        if not cmd:
            continue
        # 通过subprocess解析命令
        result = subprocess.Popen(cmd.decode(utf-8),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        if result.stderr.read():
            cmd_send = result.stderr.read()
        else:
            cmd_send = result.stdout.read()
        # 计算发送数据的长度
        length = len(cmd_send)
        # struct模块的作用就是将一个数据转化为固定长度的bytes,参数i表示整型,结果返回一个4bytes的数据
        data_length = struct.pack(i,length)
        con.send(data_length)
        con.send(cmd_send)
# 客户端
import socket
import struct
from functools import partial
tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_client.connect((127.0.0.1,8000))
while True:
    cmd = input(<<<)
    tcp_client.send(cmd.encode(utf-8))
    # 接收数据的长度
    length_data = tcp_client.recv(4)
    # 结果返回一个元组,第一个元素就是我们要的长度(整型)
    length = struct.unpack(i,length_data)[0]
    # 你可能会认为只要将上面获取到的长度直接作为接收函数的参数就可以解决问题了,但是如果数据一旦比较大的情况这就非常影响效率了,所以并不能直接使用这个数据。
    # data = tcp_client.recv(length)
    recv_size = 0
    recv_msg = b‘‘
    while recv_size < length:
        recv_msg += tcp_client.recv(1024)
        recv_size = len(recv_msg)
    # subprocess.Popen()执行的结果的编码是跟随系统的(win默认编码gbk,linux默认编码utf-8),也就是说result.stdout.read()读取的数据是gbk编码的,所以在解码的时候需要使用gbk
    print(recv_msg.decode(gbk))

 使用上面的方法基本上解决了黏包的问题了,但是这仅仅是一个客户端跟服务器进行交互,如果多个客户端都连接这个服务器的话就会有一大堆的重复代码了,因为每个客户端都必须防止黏包现象的出现,然而服务器端也会发送很多数据,所以可以将这两个功能封装成一个函数,使用函数来简化代码:

import struct
def resolve_bond(data=None,con=None,client=None,server_sendto_client=True):
    ‘‘‘解决黏包现象,参数data表示发送方的数据(bytes),con表示服务端接收到的套接字对象,client表示客户端套接字对象,server_sendto_client=True表示服务器发送给客户端‘‘‘
    if server_sendto_client:
        if not client:
            # 统计数据长度
            length = len(data)
            # 使用struct模块将长度转为固定长度的bytes
            data_length = struct.pack(i,length)
            con.send(data_length)
            con.send(data)
            return None
        if client:
            # 接收数据包的长度
            length_data = client.recv(4)
            # 解包
            length = struct.unpack(i,length_data)[0]
            recv_size = 0
            recv_msg = b‘‘
            while recv_size < length:
                recv_msg += client.recv(1024)
                recv_size = len(recv_msg)
            return recv_msg
    else:
        if not client:
            length_data = con.recv(4)
            length = struct.unpack(i, length_data)[0]
            recv_msg = b‘‘
            recv_length = 0
            while recv_length < length:
                recv_msg += con.recv(1024)
                recv_length = len(recv_msg)
            return recv_msg
        if client:
            length = len(data)
            data_length = struct.pack(i,length)
            client.send(data_length)
            client.send(data)
            return None

 

# 服务端
import socket
import subprocess
import tools
# 创建套接字对象
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 解决端口占用问题
tcp_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定端口
tcp_server.bind((127.0.0.1,8000))
tcp_server.listen(5)
while True:
    # 等待连接,程序阻塞
    con,address = tcp_server.accept()
    while True:
        cmd = con.recv(1024)
        # 如果接收到的是空的命令,则退出本次循环
        if not cmd:
            continue
        # 通过subprocess解析命令
        result = subprocess.Popen(cmd.decode(utf-8),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        if result.stderr.read():
            cmd_send = result.stderr.read()
        else:
            cmd_send = result.stdout.read()
        tools.resolve_bond(cmd_send,con)
# 客户端
import socket
import tools
tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_client.connect((127.0.0.1,8000))
while True:
    cmd = input(<<<)
    tcp_client.send(cmd.encode(utf-8))
    recv_msg = tools.resolve_bond(client=tcp_client)
    print(recv_msg.decode(gbk))

 

  当然,除了这种方法以外还可以通过添加消息头的方式解决黏包的现象。(后续更新)

 

以上是关于《Python学习之路 -- 网络编程》的主要内容,如果未能解决你的问题,请参考以下文章

Python学习之路——模块

Python学习之路3?编程风格

Python学习之路:socket网络编程

Python 之路 Day5 - 常用模块学习

Python学习之路——编程语言介绍

Python 之路 Day5 - 常用模块学习