第三模块:网络编程

Posted gbq-dog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第三模块:网络编程相关的知识,希望对你有一定的参考价值。

1.基础知识

现有的软件,绝大多数是基于C/S结构,那么就需要介绍网络编程,毕竟现在的绝大多数数据还是在网络中传输。下面先说明一些网络的基础知识,不过对于从事网络工程的来说只是很简单的基础知识,

1.1 C/S架构

C/S架构中C指的是client(客户端软件),s指的是server(服务器端软件),而本章的主要学习目的是写一个基于C/S架构的软件,客户端软件与服务器端基于网络通信。现在基本的C/S架构基本是下图这样:客户端与服务器基于网络传输互相传输数据。

技术分享图片

1.2 OSI的七层协议

了解了C/S结构的大概构成,就说一下OSI七层协议,在美国军方发展ARPA网络之后,将他公布用于学术网络知乎,就大爆发一样的发展了,由于网络协议的公开性,各家发展各自的标准,以至于各种网络之间并不能互通,国际ISO标准组织就颁发一套标准七层网络协议,七层网络协议有应用层,表示层,会话层,传输层,网络层,数据链路层,物理层

应用层(7) -------------------------------------------------- 提供应用程序间的通信

表示层 (6)-------------------------------------------------- 处理数据格式以及数据加密等

会话层 (5)-------------------------------------------------- 建立,维护管理会话

传输层 (4)-------------------------------------------------- 建立主机端到端的连接

网络层 (3)-------------------------------------------------- 寻址以及路由选择

数据链路层(2)--------------------------------------------- 提供介质访问,链路管理等

物理层(1) -------------------------------------------------- 比特流传输

一般567整合为应用程序,1234为数据流层

1.3 常用的TCP/IP的五层协议

由于ios标准组织制定标准时间长,在厂商中TCP/IP更容易理解,虽然有一些结构性的缺陷,但是TCP/IP已经成为名副其实的标准了

主要有应用层,传输层,网络层,数据链路层,物理层

应用层 -------------------------------------------------- 用户数据

传输层 -------------------------------------------------- TCP报头+上层数据

网络层 -------------------------------------------------- IP报头+上层数据

数据链路层 --------------------------------------------- LLC报头+上层数据+FCS MAC报头+上层数据+FCS

物理层 -------------------------------------------------- 0101的Bit

以上都是由上向下传输或者是由下向上传输

1.4 TCP与UDP

基于TCP/IP协议,主要有俩种传输形式,一种是TCP,一种UDP

TCP(传输控制协议):面向连接 重传机制 确认机制 流量控制  (保证可靠)

UDP:面向无连接 低开销 传输效率高,速度快

1.4.1 TCP的三次握手与四次挥手

TCP由于传输数据,要和对端要先建立通道才可以传输数据,所以被称之为可靠的传输协议,传输数据之前需要建立通道,等通道建立成功后,发送数据片段,每发送一个数据片段,发送一个确认码ack,发送端只有在收到ack确认码才会发送下一个数据片段,否则会重新发送未被确认数据片段。由于要确认的东西很多,所以TCP的报头有20字节。这样TCP传输就很占用传输带宽。

以下图片就是三次握手以及四次挥手的过程,这个会后面网络编程中较大联系技术分享图片

1.4.2 UDP协议

UDP协议由于不需要和对端确认通道以及对方是否存在,只需要知道对端是谁就可以,所以UDP也被称之为不可靠传输协议。UDP协议只负责传送,不负责数据是否到达,所以低开销,传输速率高,UDP头部只有8字节。

1.4.3端口基础

端口范围在0----65535(2*16)

知名端口号(0----1023,其他软件禁止使用),

注册端口号(1024----49151,一般用于软件注册,不过一些知名的端口还是建议不使用)

随机端口号(49152----65535,一般用于客户端软件,随机端口)

2.基于c/s结构的服务器客户端的实验

2.1基础知识点-socket

对于上面网络基础了解后,我们可以这么想以后我们自己敲代码了,那我是不是就需要记住这些几层协议,传输层,网络层具体做什么,这个时候就需要一个新的模块了,socket,python中处理网络编程相关的问题,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。这样的话,用户就不需要再次了解下面具体怎么实施,只需要知道怎么操作socket就好了,结构如图。

技术分享图片

2.1.1socket的套接字

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 #废弃了

(Only SOCK_STREAM and SOCK_DGRAM appear to be generally useful.)

 服务器端套接字类型

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

用户端套接字类型

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

公用套接字类型

  • 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为非阻塞模式,以后讲io异步时会用
  • socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息,例子 socket.getaddrinfo(‘luffycity.com‘,80)
  • socket.getfqdn() 拿到本机的主机名
  • socket.gethostbyname() 通过域名解析ip地址

2.1.2 tcp建立连接的过程

技术分享图片

2.2实验一

目的:建立一个简单的服务器客户端程序,可以建立服务器端与客户端的简单通信,例如输入一串英文字母返回大写

 服务器端代码

技术分享图片
import socket

#1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字

#2.绑定电话卡
phone.bind((127.0.0.1,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入

#3.开机
phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个

#4.等待接入
print("holding.............")#验证已经等待接入
conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息


#5.互相回话
cmd =conn.recv(1024)#cmd是接收的数据,最大是1024
conn.send(cmd.upper())#将接收的数据,变为大写发送回去

#6.结束通话
conn.close()#通话结束

#7.关机
phone.close()#电话关机
View Code

客户端代码

技术分享图片
import socket

#1.买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字

#2.拨打
phone.connect((127.0.0.1,9997))#联络服务器,拨打服务器ip以及端口

#3.通话
cmd = input(---->)
phone.send(cmd.encode(utf-8))#将要发送的信息已utf-8的形式

#4.接收数据
data = phone.recv(1024)#接收数据最大是1024字节

print(data)#打印数据

#5.关机
phone.close()#客户端关机
View Code

不过这样只实现了一次简单的发送,然后就中止了,还需要一些改善

  1. 人不能只说一句话,需要重复说话,总不能说完一句就关闭
  2. 程序结束,重启程序提示占用端口
  3. 客户端输入为空,客户端与服务器端假死情况
  4. linux环境下,客户端中止,,服务器一直接收出现死循环,windows环境下,服务器端直接报错退出

先实现第一个,服务器端与客户端添加一个死循环,让他可以一直循环的输入输出

在来实现第二个,调用socket的内置方法,实现端口复用,加快系统内部端口的回收速率

在来实现第三个,在客户端配置一个判断,输入为空继续循环,并不输出

最后来实现第四,需要些一个判断条件,在linux环境下,接收不到数据就直接退出,windows设定一个判断条件

最后看以下实现后的代码

服务端代码

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#实现端口的复用
phone.bind((‘127.0.0.1‘,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入

phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个


print("holding.............")#验证已经等待接入
conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息

while True:
try:
cmd =conn.recv(1024)#cmd是接收的数据,最大是1024
if not cmd :break#只用于linux环境下
print("客户端数据:",cmd)
conn.send(cmd.upper())#将接收的数据,变为大写发送回去
except ConnectionResetError:#用于windows
break


conn.close()#通话结束
phone.close()#电话关机

客户端代码

import socket


phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字


phone.connect((‘127.0.0.1‘,9997))#联络服务器,拨打服务器ip以及端口

while True:
cmd = input(‘---->‘).strip()
if not cmd:continue#如果输入为空则继续下一次循环
phone.send(cmd.encode(‘utf-8‘))#将要发送的信息已utf-8的形式
data = phone.recv(1024)#接收数据最大是1024字节
print(data.decode(‘utf-8‘))#打印数据


phone.close()#客户端关机

这样就实现了一些小bug,我们来说一下程序中系统的实现方法,对于我们写的小程序,并不是我们直接建立连接,服务器端与客户端之间直接收发数据,真正的做法就是我们把发送的数据交给操作系统,操作系统在调用物理硬件将数据发出,接收端也是发送信息交给操作系统让他从网卡这里获取收到的数据,传递给应用程序。

这里我们说一下send与recv的区别

send:不管recv还是send并不是直接接受或者发送对方的数据,而是操作自己操作系统的内存,将数据copy一份给内存,不过并不是一个send对应一个recv

recv:主要是有俩个过程wait data与copy data 俩个过程,wait data 时间是最长的,中间要经过网络传输,内存获取到数据将数据copy到应用层

补充一个:服务器端接收数据中的conn是一个套接字对象,是一个基于TCP协议建立的一个链接

2.3实验二

目的:实现多客户端连接服务器

其实挺简单的,客户端的代码多复制几份,服务器代码在加上一个循环就可以了

服务器端代码

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#实现端口的复用
phone.bind((127.0.0.1,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入

phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个

while True:
    print("holding.............")#验证已经等待接入
    conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息

    while True:
        try:
            cmd =conn.recv(1024)#cmd是接收的数据,最大是1024
            if not cmd :break#只用于linux环境下
            print("客户端数据:",cmd)
            conn.send(cmd.upper())#将接收的数据,变为大写发送回去
        except ConnectionResetError:#用于windows
            break


    conn.close()#通话结束,每一次连接结束,都应该将连接结束然后等待新的连接
phone.close()#电话关机

由于我们设定的listen最多监听五个客户端,由于还没有学习并发进程,目前只能实现一个一个链接,上述的实现过程就是服务器端开启,客户端也开启,一个先链接,其他就等待之前链接的结束,之前一个客户端结束,则服务器端就接收另外一个客户端的数据。

2.4实验三

目的:改装现有的程序,将其变更为一个可以远程执行命令的一个小程序

补充知识点:由于我们需要调用系统内的语句,所以我们这个时候就需要很早之前学习的subprocess模块的知识,先来复习一下这个知识点

我们经常需要通过Python去执行一条系统命令或脚本,系统的shell命令是独立于你的python进程之外的,每执行一条命令,就是发起一个新进程,通过python调用系统命令或脚本的模块在python2有os.system,除了os.system可以调用系统命令,,commands,popen2等也可以,比较乱,于是官方推出了subprocess,目地是提供统一的模块来实现对系统命令或脚本的调用。

一般的使用方法:
run()方法:

标准写法

subprocess.run([‘df‘,‘-h‘],stderr=subprocess.PIPE,stdout=subprocess.PIPE,check=True)

涉及到管道|的命令需要这样写

subprocess.run(‘df -h|grep disk1‘,shell=True) #shell=True的意思是这条命令直接交给系统去执行,不需要python负责解析

call()方法:

#执行命令,返回命令执行状态 , 0 or 非0
>>> retcode = subprocess.call(["ls", "-l"])

#执行命令,如果命令结果为0,就正常返回,否则抛异常
>>> subprocess.check_call(["ls", "-l"])
0

#接收字符串格式命令,返回元组形式,第1个元素是执行状态,第2个是命令结果 
>>> subprocess.getstatusoutput(ls /bin/ls)
(0, /bin/ls)

#接收字符串格式命令,并返回结果
>>> subprocess.getoutput(ls /bin/ls)
/bin/ls

#执行命令,并返回结果,注意是返回结果,不是打印,下例结果返回给res
>>> res=subprocess.check_output([ls,-l])
>>> res
btotal 0
drwxr-xr-x 12 alex staff 408 Nov 2 11:05 OldBoyCRM

popen()方法

常用参数:

args:shell命令,可以是字符串或者序列类型(如:list,元组)
stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
preexec_fn:只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
shell:同上
cwd:用于设置子进程的当前目录
env:用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。
下面这2条语句执行会有什么区别?

a=subprocess.run(sleep 10,shell=True,stdout=subprocess.PIPE)
a=subprocess.Popen(sleep 10,shell=True,stdout=subprocess.PIPE)
区别是Popen会在发起命令后立刻返回,而不等命令执行结果。这样的好处是什么呢?

如果你调用的命令或脚本 需要执行10分钟,你的主程序不需卡在这里等10分钟,可以继续往下走,干别的事情,每过一会,通过一个什么方法来检测一下命令是否执行完成就好了。

Popen调用后会返回一个对象,可以通过这个对象拿到命令执行结果或状态等,该对象有以下方法

poll()

Check if child process has terminated. Returns returncode

wait()

Wait for child process to terminate. Returns returncode attribute.

terminate()终止所启动的进程Terminate the process with SIGTERM

kill() 杀死所启动的进程 Kill the process with SIGKILL

communicate()与启动的进程交互,发送数据到stdin,并从stdout接收输出,然后等待任务结束

>>> a = subprocess.Popen(python3 guess_age.py,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE,shell=True)

>>> a.communicate(b22)

(byour guess:try bigger
, b‘‘)
send_signal(signal.xxx)发送系统信号

pid 拿到所启动进程的进程号

复习完了,我们来写一下,修改后的小程序

服务器端代码

import socket
import subprocess

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#实现端口的复用
phone.bind((127.0.0.1,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入

phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个


print("holding.............")#验证已经等待接入
conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息

while True:
    try:
        cmd =conn.recv(1024)#cmd是接收的数据,最大是1024
        if not cmd :break#只用于linux环境下
        obj = subprocess.Popen(cmd.decode(gbk),shell=True,#跳出python,用系统自带的系统解析器解析,由于是windows,解码使用gbk
                               stdout=subprocess.PIPE,#输出正确的输出结果
                               stderr=subprocess.PIPE #输出错误的输出结果
                               )
        stdout = obj.stdout.read() #正确输出
        stderr = obj.stderr.read() #错误输出
        conn.send(stdout+stderr)#将系统执行的结果发送回去
    except ConnectionResetError:#用于windows
        break


conn.close()#通话结束
phone.close()#电话关机

客户端实验结果

C:Usersgao-bq.KDDICANAppDataLocalProgramsPythonPython35python.exe D:/PycharmProjects/s2018/day6/实验三/客户端.py
---->ping 192.168.0.1

正在 Ping 192.168.0.1 具有 32 字节的数据:
来自 192.168.0.1 的回复: 字节=32 时间=1ms TTL=64
来自 192.168.0.1 的回复: 字节=32 时间=1ms TTL=64
来自 192.168.0.1 的回复: 字节=32 时间=4ms TTL=64
来自 192.168.0.1 的回复: 字节=32 时间=2ms TTL=64

192.168.0.1 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 1ms,最长 = 4ms,平均 = 2ms

---->ipconfig A

错误: 无法识别或不完整的命令行。

这样貌似正确与错误的结果都得到了,小兴奋

这个时候貌似就实现了一个可以远程实现ssh服务的小程序,不过这个有一个点需要注意,命令结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果。

简单的SSH远程服务算是实现了,不过还有一些坑还没实现,下个实验填坑的同时将程序做一个提升。

2.5实验四

实验现象:根据上面写的小程序,我们就可以放肆的去练习了,不过我们在实验的时候还是有bug产生,先看实验现象

技术分享图片
C:Usersgao-bq.KDDICANAppDataLocalProgramsPythonPython35python.exe D:/PycharmProjects/s2018/day6/实验四/客户端.py
---->dir
 驱动器 D 中的卷没有标签。
 卷的序列号是 B8C1-218B

 D:PycharmProjectss2018day6实验四 的目录

2018/06/16  10:22    <DIR>          .
2018/06/16  10:22    <DIR>          ..
2018/06/13  00:53                 0 __init__.py
2018/06/14  00:40               607 客户端.py
2018/06/16  10:22             1,317 服务器端.py
               3 个文件          1,924 字节
               2 个目录 79,045,222,400 可用字节

---->ipconfig

Windows IP 配置


以太网适配器 以太网:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 1:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 WLAN:

   连接特定的 DNS 后缀 . . . . . . . : DHCP HOST
   本地链接 IPv6 地址. . . . . . . . : fe80::6443:8467:1b69:5693%10
   IPv4 地址 . . . . . . . . . . . . : 192.168.0.102
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 192.168.0.1

无线局域网适配器 本地连接* 14:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::560:b85b:e1c7:8a63%2
   IPv4 地址 . . . . . . . . . . . . : 192.168.137.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

以太网适配器 蓝牙网络连接 2:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

隧道适配器 Teredo 
---->dir
Tunneling Pseudo-Interface:

   连接特定的 DNS 后缀 . . . . . . . : 
   IPv6 地址 . . . . . . . . . . . . : 2001:0:9d38:953c:4ee:519:c273:54a5
   本地链接 IPv6 地址. . . . . . . . : fe80::4ee:519:c273:54a5%19
   默认网关. . . . . . . . . . . . . : ::

---->dir
 驱动器 D 中的卷没有标签。
 卷的序列号是 B8C1-218B

 D:PycharmProjectss2018day6实验四 的目录

2018/06/16  10:22    <DIR>          .
2018/06/16  10:22    <DIR>          ..
2018/06/13  00:53                 0 __init__.py
2018/06/14  00:40               607 客户端.py
2018/06/16  10:22             1,317 服务器端.py
               3 个文件          1,924 字节
               2 个目录 79,045,222,400 可用字节

---->ipconfig /all
 驱动器 D 中的卷没有标签。
 卷的序列号是 B8C1-218B

 D:PycharmProjectss2018day6实验四 的目录

2018/06/16  10:22    <DIR>          .
2018/06/16  10:22    <DIR>          ..
2018/06/13  00:53                 0 __init__.py
2018/06/14  00:40               607 客户端.py
2018/06/16  10:22             1,317 服务器端.py
               3 个文件          1,924 字节
               2 个目录 79,045,222,400 可用字节
View Code

哎,我们可以看到,我们需要的东西怎么都滞后了或者是混乱了,本来一个命令下就应该全部看到,结果分俩次,而第二个命令又没有显示,反而成为了上一个命令结余的部分,这样不就错了吗,这样说明上面那个小程序还是有很多BUG的,不过这种现象就叫做黏包现象。

改善目的:去除服务器发送时的‘+’号,改善客户端接收接收,不管一个命令结果是多大,都可以在一次命令行下都返回回来

补充知识1:由于TCP连接,是面向连接的,在客户端设定接收最大1024的时候,在收一个超过1024字节的结果的时候,会因为一次性收不完,只在下一次结果中体现出来,收到的结果并不会丢失。而这种在管道内多个包粘在一起的就叫做粘包现象。

TCP是面向连接,面向流的,为了将多个发往接收端的包,更有效的发送给对象,采用优化算法,将多个间隔较小的且数据量的数据合并成一个大的数据,然后进行封包

改善1:那么我们就可以这么改

conn.send(stdout+stderr)#原本的

conn.send (stdout)#修改后,将一个拆分为俩个,由于俩个发送间隔短,所以俩个根据TCP的算法会一起打包发送
conn.send(stderr)

补充知识2:在解决上面+号问题之后,我们就要解决第二个问题,怎么才能一个命令接收全部回复呢,要吗就是把接收端这边最大值不设定成1024,设置成9999999这种无限大,可是这种也不合适啊,那么就剩下一个方法,就是在发送数据之前,将数据包的大小告诉我,我来调整接收大小,这个就像TCP传输的过程中头文件,那么这样就需要一个新的模块,struct模块。那么下面说一下struct模块了,不过这个模块也没有深入学习,查找了各方资料,只学习了一部分

struct模块的用处

  1. 按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;
  2. 按照指定格式将字节流转换为Python指定的数据类型;
  3. 处理二进制数据,如果用struct来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
  4. 处理c语言中的结构体;

目前只学习了其中的pack与unpack俩个用法,就暂时只说这俩个把

pack 按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回.  返回值格式为string

unpack 按照给定的格式(fmt)解析字节流,并返回解析结果  返回值格式为tuple

那么我们就用这个先改写一下吧

服务器端代码

import socket
import subprocess
import struct

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#实现端口的复用
phone.bind((127.0.0.1,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入
phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个


print("holding.............")#验证已经等待接入
conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息
print(client_add)

while True:
    try:
        cmd =conn.recv(1024)#cmd是接收的数据,最大是1024
        if not cmd :break#只用于linux环境下
        obj = subprocess.Popen(cmd.decode(gbk),shell=True,#跳出python,用系统自带的解析器解析
                               stdout=subprocess.PIPE,#输出正确的输出结果
                               stderr=subprocess.PIPE #输出错误的输出结果
                               )
        stdout = obj.stdout.read()
        stderr = obj.stderr.read()
        #发送数据
        #第一步:制作固定长度的报头
        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()#电话关机

客户端代码

import socket
import struct

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.connect((127.0.0.1,9997))#联络服务器,拨打服务器ip以及端口

while True:
    cmd = input(---->).strip()
    if not cmd:continue#如果输入为空则继续下一次循环
    phone.send(cmd.encode(gbk))#将要发送的信息已utf-8的形式
    #接收数据
    #第一步:接收报头
    header = phone.recv(4)#这里接收4的原因是在服务器端固定报头长度为4,i为4,l为8
    #第二步:从报头中接收真实的描述信息
    total_size = struct.unpack(i,header)[0]
    #第三步:接收真实的数据
    recv_size = 0
    recv_data =b‘‘
    while recv_size<total_size:
        res = phone .recv(1024)
        recv_data += res
        recv_size += len(res)

    print(recv_data.decode(gbk))#打印数据


phone.close()#客户端关机

看上面的代码,虽然是实现了,不过还是有些问题的,对于报头也太过于简单了,虽然是一个自定义协议,但是这报头简单的不像话了,就像报头的话,也应该包含多个信息

对此修改了最新的版本,这个代码下面的实验也会用到

服务器端

import socket
import subprocess
import struct
import json

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#实现端口的复用
phone.bind((127.0.0.1,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入
phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个


print("holding.............")#验证已经等待接入
conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息
print(client_add)

while True:
    try:
        cmd =conn.recv(1024)#cmd是接收的数据,最大是1024
        if not cmd :break#只用于linux环境下
        obj = subprocess.Popen(cmd.decode(gbk),shell=True,#跳出python,用系统自带的解析器解析
                               stdout=subprocess.PIPE,#输出正确的输出结果
                               stderr=subprocess.PIPE #输出错误的输出结果
                               )
        stdout = obj.stdout.read()
        stderr = obj.stderr.read()
        #发送数据
        #第一步:制作固定长度的报头
        header_dict ={
            file_name:a.txt,
            md5:xxxxx,
            total_size:len(stdout) + len(stderr),
        }#报头字典
        header_json = json.dumps(header_dict)#将报头转换为json模式
        header_bytes = header_json.encode(gbk)#将json模式的数据转换为byte类型

        #第二步,发送报头长度
        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()#电话关机

用户端代码

import socket
import struct
import json

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.connect((127.0.0.1,9997))#联络服务器,拨打服务器ip以及端口

while True:
    cmd = input(---->).strip()
    if not cmd:continue#如果输入为空则继续下一次循环
    phone.send(cmd.encode(gbk))#将要发送的信息已utf-8的形式
    #接收数据
    #第一步:接收报头长度
    obj = phone.recv(4)
    header_size = struct.unpack(i,obj)[0]

    #第二步:接收真实的报头信息
    header_bytes = phone.recv(header_size)

    #第三步:从报头中接收真实的描述信息
    header_json = header_bytes.decode(gbk) #从接收的包中反解码出json格式的包
    header_dic = json.loads(header_json)#从json包中反解码字典
    print(header_dic)
    total_size = header_dic[total_size]#从字典中获取文件大小

    #第四步:接收真实的数据
    recv_size = 0
    recv_data =b‘‘
    while recv_size<total_size:
        res = phone .recv(1024)
        recv_data += res
        recv_size += len(res)

    print(recv_data.decode(gbk))#打印数据


phone.close()#客户端关机

2.6实验五

目的:用上面的代码实现简单的FTP小程序

服务器端代码

import socket
import struct
import json
import os

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#实现端口的复用
phone.bind((127.0.0.1,9997))#服务器端必须绑定ip以及端口,整个是以集合的方式导入
phone.listen(5)#服务器端侦听,队列中最大值为5,不过一次只能对应一个


print("holding.............")#验证已经等待接入
conn,client_add = phone.accept() #分别是数据以及客户端的ip地址信息
print(client_add)

while True:
    try:
        #1.收命令
        rec =conn.recv(8096)#接收文件
        if not rec :break#只用于linux环境下

        #2.解析命令
        cmds = rec.decode(utf-8).split()
        filname = cmds[1]
        #发送数据
        #第一步:制作固定长度的报头
        header_dict ={
            file_name:filename,
            md5:xxxxx,
            total_size:os.path.getsize(filname)
        }#报头字典
        header_json = json.dumps(header_dict)#将报头转换为json模式
        header_bytes = header_json.encode(utf-8)#将json模式的数据转换为byte类型

        #第二步,发送报头长度
        conn.send(struct.pack(i, len(header_bytes)))  # 发送报头的长度

        #第三步:发送报头信息
        conn.send(header_bytes)  # 发送报头

        #第四步:发送真实数据
        with open(filname,rb) as f:
            for line in f:
                conn.send(line)

    except ConnectionResetError:#用于windows
        break


conn.close()#通话结束
phone.close()#电话关机

客户端代码

import socket
import struct
import json

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象,括号内第一个是基于网络通信的套接字,第二个是基于TCP的套接字
phone.connect((127.0.0.1,9997))#联络服务器,拨打服务器ip以及端口

while True:
    cmd = input(---->).strip()
    if not cmd:continue#如果输入为空则继续下一次循环
    phone.send(cmd.encode(utf-8))#将要发送的信息已utf-8的形式
    #接收数据
    #第一步:接收报头长度
    obj = phone.recv(4)
    header_size = struct.unpack(i,obj)[0]

    #第二步:接收真实的报头信息
    header_bytes = phone.recv(header_size)

    #第三步:从报头中接收真实的描述信息
    header_json = header_bytes.decode(gbk) #从接收的包中反解码出json格式的包
    header_dic = json.loads(header_json)#从json包中反解码字典
    print(header_dic)
    total_size = header_dic[total_size]#从字典中获取文件大小
    filename = header_dic[file_name]


    #第四步:接收真实的数据
    with open(filename,wb)as f:
        recv_size = 0
        while recv_size<total_size:
            line = phone .recv(1024)
            f.write(line)
            recv_size += len(line)



phone.close()#客户端关机

就这样就实现了,不过还有些要改善的

2.7

2.8

2.9

 









































以上是关于第三模块:网络编程的主要内容,如果未能解决你的问题,请参考以下文章

第三模块:面向对象&网络编程基础 第2章 网络编程

如何使用模块化代码片段中的LeakCanary检测内存泄漏?

python 模块的概念介绍

如何有条件地将 C 代码片段编译到我的 Perl 模块?

第三章 : 面向切面编程(AOP)

python - 模块