Python网络编程

一、储备知识

1.1 软件开发的架构

我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:

  1. 应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用
  2. web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用

这些应用的本质其实都是两个程序之间的通讯。而这两个分类又对应了两个软件开发的架构

1.1.1 C/S架构

C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。

这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大

1.1.2 B/S架构

B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。

Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查

B/S架构也是C/S架构的一种

互联网中处处是C/S架构

如黄色网站是服务端,你的浏览器是客户端(B/S架构也是C/S架构的一种)

腾讯作为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频

C指的是client(客户端软件),S指的是Server(服务端软件),本章的重点就是教大家写一个C/S架构的软件,实现服务端软件与客户端软件基于网络通信

1.2 计算机基础知识

作为应用开发程序员,我们开发的软件都是应用软件,而应用软件必须运行于操作系统之上,操作系统则运行于硬件之上,应用软件是无法直接操作硬件的,应用软件对硬件的操作必须调用操作系统的接口,由操作系统操控硬件

操作系统:(Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行

比如客户端软件想要基于网络发送一条消息给服务端软件,流程是:

  1. 客户端软件产生数据,存放于客户端软件的内存中,然后调用接口将自己内存中的数据发送/拷贝给操作系统内存
  2. 客户端操作系统收到数据后,按照客户端软件指定的规则(即协议)、调用网卡发送数据
  3. 网络传输数据
  4. 服务端软件调用系统接口,想要将数据从操作系统内存拷贝到自己的内存中
  5. 服务端操作系统收到4的指令后,使用与客户端相同的规则(即协议)从网卡接收到数据,然后拷贝给服务端软件

1.3 什么是网络

硬件之上安装好操作系统,然后装上软件你就可以正常使用了,但此时你也只能自己使用,每个人都拥有一台自己的机器,然而彼此孤立

如何能大家一起玩耍,那就是联网了,即internet

然而internet为何物?举一个简单的例子: 如果把一个人与这个人的有线电话比喻为一台计算机,那么其实两台计算机之间通信与两个人打电话之间通信的原理是一样的。 两个人之间想要打电话首先一点必须是接电话线,这就好比是计算机之间的通信首先要有物理链接介质,比如网线,交换机,路由器等网络设备。 通信的线路建好之后,只是物理层面有了可以承载数据的介质,要想通信,还需要我们按照某种规则组织我们的数据,这样对方在接收到数据后就可以按照相同的规则去解析出数据,这里说的规则指的就是:中国有很多地区,不同的地区有不同的方言,为了全中国人都可以听懂,大家统一讲普通话

普通话属于中国国内人与人之间通信的标准,那如果是两个国家的人交流呢?

问题是,你不可能要求一个人/计算机掌握全世界的语言/标准,于是有了世界统一的通信标准:英语

英语成为世界上所有人通信的统一标准,计算机之间的通信也应该有一个像英语一样的通信标准,这个标准称之为互联网协议, 可以很明确地说:互联网协议就是计算机界的英语,网络就是物理链接介质+互联网协议。 我们需要做的是,让全世界的计算机都学会互联网协议,这样任意一台计算机在发消息时都严格按照协议规定的格式去组织数据,接收方就可以按照相同的协议解析出结果了,这就实现了全世界的计算机都能无障碍通信。

按照功能不同,人们将互联网协议分为osi七层或tcp/ip五层或tcp/ip四层(我们只需要掌握tcp/ip五层协议即可),这种分层就好比是学习英语的几个阶段,每个阶段应该掌握专门的技能或者说完成特定的任务,比如:1、学音标 2、学单词 3、学语法 4、写作文。。。

在这里插入图片描述

二、TCP/IP各层详解

我们将应用层,表示层,会话层并作应用层,从tcp/ip五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议

就理解了整个互联网通信的原理。

首先,用户感知到的只是最上面一层应用层,自上而下每层都依赖于下一层,所以我们从最下一层开始切入,比较好理解

每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件

2.1 物理层

物理层由来:上面提到,孤立的计算机之间要想一起玩,就必须接入internet,言外之意就是计算机之间必须完成组网

物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0

规定:一组物理层数据称之为:位

单纯的电信号毫无意义,必须对其进行分组才能有意义

2.2 数据链路层

数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思

数据链路层的功能:定义了电信号的分组方式

以太网协议:

早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet

ethernet规定

一组电信号构成一个数据包,叫做‘帧’
每一数据帧分成:报头head和数据data两部分
head data
head包含:(固定18个字节)

发送者/源地址,6个字节
接收者/目标地址,6个字节
数据类型,6个字节
data包含:(最短46字节,最长1500字节)

数据包的具体内容
head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送

mac地址:

head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址

mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)

注意:计算机通信基本靠吼,即以太网协议的工作方式是广播

有了mac地址,同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址)

理论上世界上任何一台计算机都可以通信了,但问题是不可能将所有的人都关在一个房间,不可能将所有的计算机都连在一个交换机上,这就会形成灾难

2.3 网络层

网络层由来:有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难。

结论:必须找出一种方法来区分哪些计算机属于同一广播域,哪些不是,如果是就采用广播的方式发送,如果不是,就采用路由的方式(向不同广播域/子网分发数据包),mac地址是无法区分的,它只跟厂商有关

网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址

IP协议

  • 网络部分:标识子网
  • 主机部分:标识主机

注意:单纯的ip地址段只是标识了ip地址的种类,从网络部分或主机部分都无法辨识一个ip所处的子网

例:172.16.10.1与172.16.10.2并不能确定二者处于同一子网

子网掩码

所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0

知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是

比如,已知IP地址172.16.10.1和172.16.10.2的子网掩码都255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,

172.16.10.1:10101100.00010000.00001010.000000001
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
172.16.10.2:10101100.00010000.00001010.000000010
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
结果都是172.16.10.0,因此它们在同一个子网络。

IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络

ip数据包

ip数据包也分为head和data部分,无须为ip包定义单独的栏位,直接放入以太网包的data部分

head:长度为20到60字节

data:最长为65,515字节。

而以太网数据包的”数据”部分,最长只有1500字节。因此,如果IP数据包超过了1500字节,它就需要分割成几个以太网数据包,分开发送了。

ARP协议
arp协议由来:计算机通信基本靠吼,即广播的方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送,在谈及以太网协议时候,我门了解到

通信是基于mac的广播方式实现,计算机在发包时,获取自身的mac是容易的,如何获取目标主机的mac,就需要通过arp协议

arp协议功能:广播的方式发送数据包,获取目标主机的mac地址

协议工作方式:每台主机ip都是已知的

总结

ip地址+mac地址 ==》标识全世界范围内独一无二的一个计算机

由于ARP协议,IP地址可以转换找到mac地址

ip地址==》标识全世界范围内独一无二的一个计算机

2.4 传输层

传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,迅雷等多个应用程序,

那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。

传输层功能:建立端口到端口的通信

补充:端口范围0-65535,0-1023为系统占用端口

IP+port ==>标识全世界范围内独一无二的一个基于网络通信的应用

传输层有两种协议,TCP和UDP

2.4.1 tcp协议

可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割

为什么tcp是可靠的数据传输呢?

最可靠的方式就是只要不得到确认,就重新发送数据报,直到得到对方的确认为止

​ tcp的3次握手和4四挥手

2.4.2 udp协议

不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包

当应用程序希望通过UDP与一个应用程序通信时,传输数据之前源端和终端不建立连接。当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。

总结:TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!

2.5 应用层

应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式

应用层功能:规定应用程序的数据格式。

例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。

可以自定义协议

自定义协议需要注意的问题:

  1. 两大组成部分=头部+数据部分
    • 头部:放对数据的描述信息。比如:数据要发给谁,数据的类型,数据的长度
    • 数据部分:想要发的数据
  2. 头部的长度必须是固定的
    • 因为接收端要通过头部获取所接收数据的详细信息

三、socket介绍

我们已经知道,假设我现在要写一个程序,给另一台计算机发数据,必须通过tcp/ip协议 ,但具体的实现过程是什么呢?我应该怎么操作才能把数据封装成tcp/ip的包,又执行什么指令才能把数据发到对端机器上呢? 不能只有世界观,没有方法论呀。。。此时,socket隆重登场,简而言之,socket这个东东干的事情,就是帮你把tcp/ip协议层的各种数据封装啦、数据发送、接收等通过代码已经给你封装好了,你只需要调用几行代码,就可以给别的机器发消息了

3.1 什么是socket

我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。

能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。

socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

3.2 Socket通信套路

当通过socket建立起2台机器的连接后,本质上socket只干2件事,一是收数据,一是发数据,没数据时就等着。

socket 建立连接的过程跟我们现实中打电话比较像,打电话必须是打电话方和接电话方共同完成的事情,我们分别看看他们是怎么建立起通话的

接电话方:

1.首先你得有个电话
2.你的电话要有号码
3.你的电话必须连上电话线
4.开始在家等电话
5.电话铃响了,接起电话,听到对方的声音

打电话方:

1.首先你得有个电话
2.输入你想拨打的电话
3.等待对方接听
4.say “hi 约么,我有七天酒店的打折卡噢~5.等待回应——》响应回应——》等待回应。。。。

把它翻译成socket通信

接电话方(socket服务器端):

1.首先你得有个电话\(生成socket对象\)
2.你的电话要有号码\(绑定本机ip+port\)
3.你的电话必须连上电话线\(连网\)
4.开始在家等电话\(开始监听电话listen\)
5.电话铃响了,接起电话,听到对方的声音\(接受新连接\)

打电话方(socket客户端):

1.首先你得有个电话\(生成socket对象\)
2.输入你想拨打的电话\(connect 远程主机ip+port\)
3.等待对方接听
4.say “hi 约么,我有七天酒店的打折卡噢~”\(send\(\) 发消息。。。\)
5.等待回应——》响应回应——》等待回应。。。。

3.3 socket套接字方法

3.3.1 socket 实例

socket.socket(family=AF_INET, type=SOCK_STREAM)

family(socket家族)

  • socket.AF_UNIX:用于本机进程间通讯,为了保证程序安全,两个独立的程序(进程)间是不能互相访问彼此的内存的,但为了实现进程间的通讯,可以通过创建一个本地的socket来完成
  • socket.AF_INET:(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

socket type类型

  • socket.SOCK_STREAM #for tcp
  • socket.SOCK_DGRAM #for udp
  • socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头
  • socket.SOCK_RDM #是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用
  • socket.SOCK_SEQPACKET #废弃了

3.3.2 服务端套接字函数

  • s.bind() 绑定(主机,端口号)到套接字
  • s.listen() 开始TCP监听
  • s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

3.3.3 客户端套接字函数

  • s.connect() :主动初始化TCP服务器连接
  • s.connect_ex() :connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

3.3.4 公共用途的套接字函数

  • s.recv() 接收数据
  • s.send() 发送数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完,可后面通过实例解释)
  • s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
  • s.recvfrom() Receive data from the socket. The return value is a pair (bytes, address)
  • s.getpeername() 连接到当前套接字的远端的地址
  • s.close() 关闭套接字
  • socket.setblocking(flag) #True or False,设置socket为非阻塞模式
  • socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息
  • socket.getfqdn() 拿到本机的主机名
  • socket.gethostbyname() 通过域名解析ip地址

四、socket实例

4.1 基本实例

server.py

import socket

# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# print(phone)
# 2、绑定手机卡
phone.bind(('127.0.0.1', 8081))  # 0-65535:0-1024给操作系统使用
# 3、开机  最大可以挂起5个电话等待
phone.listen(5)
# 4、等电话链接
print('starting...')
conn, client_addr = phone.accept()
# print(phone.accept())
# 5、收,发消息
data = conn.recv(1024)  # 1、单位:bytes 2、1024代表最大接收1024个bytes
print('客户端的数据', data)
conn.send(data.upper())
# 6、挂电话
conn.close()
# 7、关机
phone.close()

client.py

import socket

# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# print(phone)
# 2、拨号
phone.connect(('127.0.0.1', 8081))
# 3、发,收消息
phone.send('hello'.encode('utf-8'))
data = phone.recv(1024)
print(data)
# 4、关闭
phone.close()

遇到第一个问题,不可能说一句话就挂电话吧,这不是神经病吗?应该不停的说话,有个循环吧

4.2 实现通信循环

server.py

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8083))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
conn, client_addr = phone.accept()
print(client_addr)
while True:  # 通信循环
    data = conn.recv(1024)
    print('客户端的数据', data)
    conn.send(data.upper())
conn.close()
phone.close()

client.py

import socket

# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.拨号
phone.connect(('127.0.0.1', 8083))
# 3.发收消息
while True:
    msg = input('>>: ').strip()
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data)
# 4.关闭
phone.close()

修复bug

端口被占用的情况

客户端不能发送空

服务端还在运行,客户端突然挂了,分为Windows系统情况和linux的情况

server.py

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 解决端口占用的问题,需要加在bind之前
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8083))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
conn, client_addr = phone.accept()
print(client_addr)
while True:  # 通信循环
    try:
        data = conn.recv(1024)
        if not data: break  # 适用于linux操作系统
        print('客户端的数据', data)
        conn.send(data.upper())
    except ConnectionResetError:  # 适用于windows操作系统
        break
conn.close()
phone.close()

client.py

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8083))
while True:
    msg = input('>>: ').strip()  # msg=''
    if not msg: continue  # 解决不能发空的bug
    phone.send(msg.encode('utf-8'))  # phone.send(b'')
    # print('has send')
    data = phone.recv(1024)
    # print('has recv')
    print(data.decode('utf-8'))
phone.close()

4.3 实现对多个客户端同时提供服务

server.py

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8083))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True:  # 链接循环
    conn, client_addr = phone.accept()
    print(client_addr)
    while True:  # 通信循环
        try:
            data = conn.recv(1024)
            if not data: break  # 适用于linux操作系统
            print('客户端的数据', data)
            conn.send(data.upper())
        except ConnectionResetError:  # 适用于windows操作系统
            break
    conn.close()
phone.close()

client.py内容不变

4.4 UDP实例

udp 不需要经过3次握手和4次挥手,不需要提前建立连接,直接发数据就行。

server端

import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #udp类型
udp_server_client.bind(ip_port)
while True:
    msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print("recv ",msg,addr)
    udp_server_client.sendto(msg.upper(),addr)

client端

import socket
ip_port = ('127.0.0.1',9000)
BUFSIZE = 1024
udp_server_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
    msg=input('>>: ').strip()
    if not msg:continue
    udp_server_client.sendto(msg.encode('utf-8'),ip_port)
    back_msg,addr = udp_server_client.recvfrom(BUFSIZE)
    print(back_msg.decode('utf-8'),addr)

tcp基于链接通信

  • 基于链接,则需要listen(backlog),指定连接池的大小
  • 基于链接,必须先运行的服务端,然后客户端发起链接请求
  • 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
  • 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)

udp无链接

  • 无链接,因而无需listen(backlog),更加没有什么连接池之说了
  • 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失
  • recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
  • 只有sendinto发送数据没有recvfrom收数据,数据丢失

五、沾包现像及解决方案

5.1 模拟ssh远程执行命令

# windows
# dir:查看某一个文件夹下的子文件名与子文件夹名
# ipconfig:查看本地网卡的ip信息
# tasklist:查看运行的进程

# linux:
# ls
# ifconfig
# ps aux
# 执行系统命令,并且拿到命令的结果
import os
res=os.system('dir')
print('命令的结果是:',res)
# 此处的res表示程序执行的标志 0表示成功
import subprocess

obj = subprocess.Popen('dir', shell=True,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE)
print(obj)
print('stdout 1--->: ', obj.stdout.read().decode('GBK'))
print('stderr 1--->: ', obj.stderr.read().decode('GBK'))

server.py

import socket
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 9900))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True:  # 链接循环
    conn, client_addr = phone.accept()
    print(client_addr)
    while True:  # 通信循环
        try:
            # 1、收命令
            cmd = conn.recv(1024)
            if not cmd: break  # 适用于linux操作系统
            # 2、执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode('GBK'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 3、把命令的结果返回给客户端
            print(len(stdout) + len(stderr))
            conn.send(stdout + stderr)  # +是一个可以优化的点
        except ConnectionResetError:  # 适用于windows操作系统
            break
    conn.close()
phone.close()

client.py

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 9900))
while True:
    # 1、发命令
    cmd = input('>>: ').strip()  # ls /etc
    if not cmd: continue
    phone.send(cmd.encode('GBK'))
    # 2、拿命令的结果,并打印
    data = phone.recv(1024)  # 1024是一个坑
    print(data.decode('GBK'))
phone.close()

出现沾包现像:两次结果粘到一起了

那怎么解决呢? 直接把recv(1024)改大不就好了,改成5000\10000或更大。这么干的话,并不能解决实际问题,因为你不可能提前知道对方返回的结果数据大下,无论你改成多大,对方的结果都有可能比你设置的大,另外这个recv并不是真的可以随便改特别大的,有关部门建议的不要超过8192,再大反而会出现影响收发速度和不稳定的情况

你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

粘包问题只存在于TCP中,UDP不存在

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

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

总结

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的
  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

5.2 基于UDP的命令执行程序

udp server

import socket
import subprocess
ip_port = ('127.0.0.1', 9003)
bufsize = 1024
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(ip_port)
while True:
    # 收消息
    cmd, addr = udp_server.recvfrom(bufsize)
    print('用户命令----->', cmd,addr)
    # 逻辑处理
    res = subprocess.Popen(cmd.decode('GBK'), shell=True, stderr=subprocess.PIPE, stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE)
    stderr = res.stderr.read()
    stdout = res.stdout.read()
    # 发消息
    udp_server.sendto(stdout + stderr, addr)
udp_server.close()

udp client

from socket import *
import time
ip_port = ('127.0.0.1', 9003)
bufsize = 1024
udp_client = socket(AF_INET, SOCK_DGRAM)
while True:
    msg = input('>>: ').strip()
    if len(msg) == 0:
        continue
    udp_client.sendto(msg.encode('utf-8'), ip_port)
    data, addr = udp_client.recvfrom(bufsize)
    print(data.decode('utf-8'), end='')

5.3 粘包的解决办法

补充知识

import struct
res=struct.pack('i',123456)
print(res,type(res),len(res))
# #client.recv(4)
obj=struct.unpack('i',res)
print(obj)
print(obj[0])

# res=struct.pack('i',12300000000)
res=struct.pack('l',111232301212312312312312000000)
print(res,len(res))

5.3.1 沾包的解决办法一

server.py

import socket
import subprocess
import struct

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 9909))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True:  # 链接循环
    conn, client_addr = phone.accept()
    print(client_addr)
    while True:  # 通信循环
        try:
            # 1、收命令
            cmd = conn.recv(8096)
            if not cmd: break  # 适用于linux操作系统
            # 2、执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 3、把命令的结果返回给客户端
            # 第一步:制作固定长度的报头
            total_size = len(stdout) + len(stderr)
            header = struct.pack('i', total_size)
            # 第二步:把报头发送给客户端
            conn.send(header)
            # 第三步:再发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:  # 适用于windows操作系统
            break
    conn.close()
phone.close()

client.py

import socket
import struct

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 9909))
while True:
    # 1、发命令
    cmd = input('>>: ').strip()  # ls /etc
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))
    # 2、拿命令的结果,并打印
    # 第一步:先收报头
    header = phone.recv(4)
    # 第二步:从报头中解析出对真实数据的描述信息(数据的长度)
    total_size = struct.unpack('i', header)[0]
    # 第三步:接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(1024)  # 1024是一个坑
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode('utf-8'))
phone.close()

5.3.2 终极版

补充知识

import struct
import json

# res=struct.pack('i',12300000000)
# res=struct.pack('l',111232301212312312312312000000)
# print(res,len(res))
header_dic = {
    'filename': 'a.txt',
    'md5': 'xxdxxx',
    'total_size': 3980472390874
}
header_json = json.dumps(header_dic)
print(type(header_json))
header_bytes = header_json.encode('utf-8')
print(type(header_bytes))
print(len(header_bytes))
struct.pack('i', len(header_bytes))

server.py

import socket
import subprocess
import struct
import json

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 9909))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True:  # 链接循环
    conn, client_addr = phone.accept()
    print(client_addr)
    while True:  # 通信循环
        try:
            # 1、收命令
            cmd = conn.recv(8096)
            if not cmd: break  # 适用于linux操作系统
            # 2、执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode('GBK'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 3、把命令的结果返回给客户端
            # 第一步:制作固定长度的报头
            header_dic = {
                'filename': 'a.txt',
                'md5': 'xxdxxx',
                'total_size': len(stdout) + len(stderr)
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('GBK')
            # 第二步:先发送报头的长度
            conn.send(struct.pack('i', len(header_bytes)))
            # 第三步:再发报头
            conn.send(header_bytes)
            # 第四步:再发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:  # 适用于windows操作系统
            break
    conn.close()
phone.close()

client.py

import socket
import struct
import json

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 9909))
while True:
    # 1、发命令
    cmd = input('>>: ').strip()  # ls /etc
    if not cmd: continue
    phone.send(cmd.encode('GBK'))
    # 2、拿命令的结果,并打印
    # 第一步:先收报头的长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]
    # 第二步:再收报头
    header_bytes = phone.recv(header_size)
    # 第三步:从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('GBK')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['total_size']
    # 第四步:接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = phone.recv(10)  # 1024是一个坑
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode('GBK'))
phone.close()

六、实现文件传输

创建文件夹 client 和 server

在clinet文件夹下创建文件夹download和文件client.py

在server文件夹下创建文件夹share和文件server.py

server.py

import socket
import struct
import json
import os

server_path = os.path.dirname(__file__)
share_dir = os.path.join(server_path,'share')
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 8912))  # 0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True:  # 链接循环
    conn, client_addr = phone.accept()
    print(client_addr)
    while True:  # 通信循环
        try:
            # 1、收命令
            res = conn.recv(8096)  # b'get 1.mp4'
            if not res: break  # 适用于linux操作系统
            # 2、解析命令,提取相应命令参数
            cmds = res.decode('utf-8').split()  # ['get','1.mp4']
            filename = cmds[1]
            # 3、以读的方式打开文件,读取文件内容发送给客户端
            # 第一步:制作固定长度的报头
            header_dic = {
                'filename': filename,  # 'filename':'1.mp4'
                'md5': 'xxdxxx',
                'file_size': os.path.getsize(r'%s/%s' % (share_dir, filename))
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('utf-8')
            # 第二步:先发送报头的长度
            conn.send(struct.pack('i', len(header_bytes)))
            # 第三步:再发报头
            conn.send(header_bytes)
            # 第四步:再发送真实的数据
            with open('%s/%s' % (share_dir, filename), 'rb') as f:
                # conn.send(f.read())
                for line in f:
                    conn.send(line)
        except ConnectionResetError:  # 适用于windows操作系统
            break
    conn.close()
phone.close()

client.py

import socket
import struct
import json
import os


client_path = os.path.dirname(__file__)
download_dir = os.path.join(client_path,'download')
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8912))
while True:
    # 1、发命令
    cmd = input('>>: ').strip()  # get a.txt
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))
    # 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
    # 第一步:先收报头的长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]
    # 第二步:再收报头
    header_bytes = phone.recv(header_size)
    # 第三步:从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    '''
            header_dic={
                'filename': filename, #'filename':'1.mp4'
                'md5':'xxdxxx',
                'file_size': os.path.getsize(filename)
            }
    '''
    print(header_dic)
    total_size = header_dic['file_size']
    filename = header_dic['filename']
    # 第四步:接收真实的数据
    with open('%s/%s' % (download_dir, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)  # 1024是一个坑
            f.write(line)
            recv_size += len(line)
            print('总大小:%s   已下载大小:%s' % (total_size, recv_size))
phone.close()
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值