《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‘))

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

原文地址:https://www.cnblogs.com/jonas-von/p/9047239.html

时间: 2024-10-12 09:13:54

《Python学习之路 -- 网络编程》的相关文章

CI框架源码阅读笔记3 全局函数Common.php

从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作). 打开Common.php中,第一行代码就非常诡异: if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 上一篇(CI框架源码阅读笔记2 一切的入口 index

IOS测试框架之:athrun的InstrumentDriver源码阅读笔记

athrun的InstrumentDriver源码阅读笔记 作者:唯一 athrun是淘宝的开源测试项目,InstrumentDriver是ios端的实现,之前在公司项目中用过这个框架,没有深入了解,现在回来记录下. 官方介绍:http://code.taobao.org/p/athrun/wiki/instrumentDriver/ 优点:这个框架是对UIAutomation的java实现,在代码提示.用例维护方面比UIAutomation强多了,借junit4的光,我们可以通过junit4的

Yii源码阅读笔记 - 日志组件

?使用 Yii框架为开发者提供两个静态方法进行日志记录: Yii::log($message, $level, $category);Yii::trace($message, $category); 两者的区别在于后者依赖于应用开启调试模式,即定义常量YII_DEBUG: defined('YII_DEBUG') or define('YII_DEBUG', true); Yii::log方法的调用需要指定message的level和category.category是格式为“xxx.yyy.z

源码阅读笔记 - 1 MSVC2015中的std::sort

大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格式化,去掉或者展开用于条件编译或者debug检查的宏,依重要程度重新排序函数,但是不会改变命名方式(虽然MSVC的STL命名实在是我不能接受的那种),对于代码块的解释会在代码块前(上面)用注释标明. template<class _RanIt, class _Diff, class _Pr> in

CI框架源码阅读笔记5 基准测试 BenchMark.php

上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功能,各模块之间可以相互调用,共同构成了CI的核心骨架. 从本篇开始,将进一步去分析各组件的实现细节,深入CI核心的黑盒内部(研究之后,其实就应该是白盒了,仅仅对于应用来说,它应该算是黑盒),从而更好的去认识.把握这个框架. 按照惯例,在开始之前,我们贴上CI中不完全的核心组件图: 由于BenchMa

CI框架源码阅读笔记2 一切的入口 index.php

上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里这次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中,我们并不会逐行进行解释,而只解释核心的功能和实现. 1.       设置应用程序环境 define('ENVIRONMENT', 'development'); 这里的development可以是任何你喜欢的环境名称(比如dev,再如test),相对应的,你要在下面的switch case代码块中

Apache Storm源码阅读笔记

欢迎转载,转载请注明出处. 楔子 自从建了Spark交流的QQ群之后,热情加入的同学不少,大家不仅对Spark很热衷对于Storm也是充满好奇.大家都提到一个问题就是有关storm内部实现机理的资料比较少,理解起来非常费劲. 尽管自己也陆续对storm的源码走读发表了一些博文,当时写的时候比较匆忙,有时候衔接的不是太好,此番做了一些整理,主要是针对TridentTopology部分,修改过的内容采用pdf格式发布,方便打印. 文章中有些内容的理解得益于徐明明和fxjwind两位的指点,非常感谢.

CI框架源码阅读笔记4 引导文件CodeIgniter.php

到了这里,终于进入CI框架的核心了.既然是"引导"文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.com/usr/reg 经过引导文件,实际上会交给Application中的UsrController控制器的reg方法去处理. 这之中,CodeIgniter.php做了哪些工作?我们一步步来看. 1.    导入预定义常量.框架环境初始化 之前的一篇博客(CI框架源码阅读笔记2 一切的入

jdk源码阅读笔记之java集合框架(二)(ArrayList)

关于ArrayList的分析,会从且仅从其添加(add)与删除(remove)方法入手. ArrayList类定义: p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Monaco } span.s1 { color: #931a68 } public class ArrayList<E> extends AbstractList<E> implements List<E> ArrayList基本属性: /** *

dubbo源码阅读笔记--服务调用时序

上接dubbo源码阅读笔记--暴露服务时序,继续梳理服务调用时序,下图右面红线流程. 整理了调用时序图 分为3步,connect,decode,invoke. 连接 AllChannelHandler.connected(Channel) line: 38 HeartbeatHandler.connected(Channel) line: 47 MultiMessageHandler(AbstractChannelHandlerDelegate).connected(Channel) line: