Particles

Python网络编程

网络基础

  • 如何在网络中区分每一台计算机? 使用IP
  • 一台计算机上多个程序如何共用网络而不冲突呢? 网络端口
  • 不同的计算机通信怎么样才能互信理解? 使用相同的协议

#IP地址

  • 用来在网络中标识一台计算机的一串数字,比如:192.168.34.203,在一段网络中是唯一的。
  • 每一个IP地址包括两部分:网络地址和主机地址(分不同类)
  • 主机号0,255两个数字是不能用的(网络号,广播地址)

#A\B\C\D\E类IP

  • A类IP地址由1字节的⽹络地址和3字节主机地址组成, ⽹络地址的最⾼位必须是“0”,地址范围1.0.0.1-126.255.255.254可⽤的A类⽹络有126个, 每个⽹络能容纳1677214个主机
  • B类IP地址由2个字节的⽹络地址和2个字节的主机地址组成, ⽹络地址的最⾼位必须是“10”,地址范围128.1.0.1-191.255.255.254 可⽤的B类⽹络有16384个,每个⽹络能容纳65534主机
  • C类IP地址由3字节的⽹络地址和1字节的主机地址组成, ⽹络地址的最⾼位必须是“110”范围192.0.1.1-223.255.255.254 C类⽹络可达2097152个, 每个⽹络能容纳254个主机
  • D类IP地址第⼀个字节以“1110”开始, 它是⼀个专⻔保留的地址。它并不指向特定的⽹络, ⽬前这⼀类地址被⽤在多点⼴播(一对多) 中多点⼴播地址⽤来⼀次寻址⼀组计算机 地址范围224.0.0.1-239.255.255.254
  • E类IP地址以“1111”开始, 为将来使⽤保留 E类地址保留, 仅作实验和开发⽤

#私有IP

  • 本地局域网上的IP,专门为组织机构内部使用

  • 在这么多⽹络IP中, 国际规定有⼀部分IP地址是⽤于我们的局域⽹使⽤, 属于私⽹IP, 不在公⽹中使⽤的, 它们的范围是:

    --10.0.0.0~10.255.255.255

    --172.16.0.0~172.31.255.255

    --192.168.0.0~192.168.255.255

  • 私有IP:局域网通信,内部访问,不能在外网公用。私有IP禁止出现在Internet中,来自于私有IP的流量全部都会阻止并丢掉

  • 公有IP:全球访问

  • IP地址127.0.0.1用于回路测试

    • 测试当前计算机的网络通信协议
    • 127.0.0.1可以代表本机IP地址, ⽤ http://127.0.0.1 就可以测试本机中配置的Web服务器
    • 常用来ping 127.0.0.1来看本地ip/tcp正不正常,如能ping通即可正常使用

#子网掩码

  • 是用于测试两个IP是不是属于同一网段的工具

  • 子网掩码不能单独存在,它必须结合IP地址一起使用

  • ⼦⽹掩码只有⼀个作⽤, 就是将某个IP地址划分成⽹络地址和主机地址两部分

  • ⼦⽹掩码的设定必须遵循⼀定的规则:

    --与IP地址相同, ⼦⽹掩码的长度也是32位

    --左边是⽹络位, ⽤⼆进制数字“1”表示;

    --右边是主机位, ⽤⼆进制数字“0”表示

  • 假设IP地址为“192.168.1.1”⼦⽹掩码为“255.255.255.0”。

    --其中,“1”有24个, 代表与此相对应的IP地址左边24位是⽹络号;

    --“0”有8个代表与此相对应的IP地址右边8位是主机号

#端口号

  • 用来标记区分进程

  • 一台拥有IP地址的主机可以提供许多服务,比如HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)等,这些服务完全可以通过1个IP地址来实现。因为IP地址与网络服务是一对多的关系,实际上是通过IP地址和端口号相结合来区分不同的服务的。

  • 端口号是一个数字,只有整数,范围是从0到65535(分知名和动态两种)

  • 知名端口是装所周知的端口号(用来做固定的事情)

    --80端口分配给了HTTP服务(网站)

    --21端口分配了FTP服务(文件下载)

    --可以理解为就是,一些常用的功能使用的端口号是固定的

  • 动态端口是1024--65535

    之所以是动态端口,是因为他一般不固定的分配某种服务,而是动态分配,动态分配是指当一个进程系统或者应用程序进行网络通信时,他向主机请求一个端口,主机从可用的端口中分配一个拱它使用。

#协议

  • 协议:约定好的规范

  • 早期的计算机网络,都是由各个厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容(语言、方言、阿帕网)为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Interner Protocol Suite)就是通过协议标准

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

  • TCP/IP协议是大家都遵循的最基本网络通信协议

  • 是完成进程之间通信的规范

#不同层次

  • 应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

  • 但是一般写代码只需:应用层、传输层、网络层、网络接口层(数据链路层)四层即可。

#基础

​ 在早期,不同的公司都推出了属于自己的私有网络协议,相互之间不能兼容于是,ISO(国际标准化组织)站出来:干脆这样,我给大家制定一个通用的网络通信协议,该协议是国际标准。 于是ISO博览众家之长,制订了“一堆”详细的,复杂的,繁琐的,精确的网络通信协议。• 不过这堆协议太复杂了,为了理清思路,便于学习,将他们分了7类(也就是分了7层),不同层代表不同的功能,并把这些协议归到相应的层里面去。• 国际标准出来了,接下来就要软件/硬件厂商去实现了。但实际上各厂商并没有完整实现7层协议,因为7层协议栈追求全能、完善,导致它太过复杂,实现起来太难了。

​ 于是:实际使用的时候,就是按4层划分:应用层、传输层、网络层、网络接口层(链路层)

  • TCP/IP定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准
  • 4层的层级结构中,每一层都呼叫它的下一层所提供的网络来完成自己的需求
  • 其中的应用层关注的是应用程序的细节,而不是数据在网络中的传输活动,其他三层主要处理所有的通信细节,对应用程序一无所知。
  • 应用层
    • 应用程序沟通的层,不同的文件系统有不同的文件命名原则和不同的文本行表示方法等,不同的系统之间传输文件还有各种不兼容问题,这些都将由应用层来处理
  • 传输层
    • 提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据协议(UDP)等,这一层负责传送数据,并且确定数据以被送达并被接收
  • 网络层
    • 负责提供基本的数据包传送功能,让每一块数据包都能够到达目的主机,网络层接收由更低层发来的数据包,并把该数据包发送到更高层,相反,IP层也把从TCP或UDP层接收来的数据包传送到更低层
  • 网络接口层
    • 对实际的网络媒体的管理,定义如何使用实际网络来传送数据(处理机械的、电气
      的和过程的接口)

#Socket编程

  • socket:通过网络完成进程间通信的过程(区别于一台计算机之间进程通信)
  • socket英文翻译为“插孔”,也常被称为“套接字”
    Socket是应用层与传输层之间的桥梁

#Socket初识

  • 本质是编程接口(API):Socket是对TCP/IP协议的封装,Socket只是个编程接口而不是协议,通过Socket我们才能使用TCP/IP协议簇
  • TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或许显示数据的具体形式;Socket是发动机,提供了网络通信的能力
  • 最重要的是:Socket是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的Socket系统调用
  • 套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认

Socket编程

  • 网络通信的方式一般情况只有两种:TCP和UDP
###创建Socket
from socket import *  #导入套接字模块
s = socket(AF_INET,SOCK_STREAM) #创建套接字

s此时是一个socket对象,拥有发送和接收网络数据的功能

  • 该函数带有的两个参数时必须要写的
    • AF_INET(IPV4协议用于Ineternet进程间通信)
    • SOCK_STREAM 套接字类型,可以是SOCK_STREAM(流式套接字,用于TCP协议)或者SOCK_DGRAM(数据报套接字,用于UDP协议)
      • TCP慢但是稳定不会丢失
      • UDP快但是可能会丢失数据(黑客攻击)
  • 确定了IP地址端口号(ipv4协议),TCP或UDP协议之后,计算机之间可以进行通信

#Socket编程-UDP

  • UDP --- User Data Protocol,用户数据报协议,是一个无连接的简单的面向数据报的传输层协议。UDP不提供可靠性,它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快
  • UDP⼀般⽤于多点通信和实时的数据业务, ⽐如:
    • 语音广播
    • 视频
    • QQ
    • TFTP(简单文件传输)

#Socket编程-TCP

  • TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接

  • 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,只简单
    的描述下这三次对话的简单过程

    • 主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话
    • 主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话
    • 主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”
      ,这是第三次对话
    • 三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据

#TCP与UDP之间的区别

  • 基于连接与无连接
  • 对系统资源的要求(TCP较多,UDP较少)
  • UDP程序结构较简单
  • 流模式与数据报模式
  • TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证
  • UDP理解为写信(只有收件人地址),TCP理解为打电话(先拨号建立通路,需要通路稳定)

#Socket-UDP编程

#UDP协议

  • UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

#发送数据

from socket import *
Send_s1 = socket(AF_INET,SOCK_DGRAM)   #创建套接字
Send_addr = ("192.168.34.131",54321)   #准备接收方地址
Send_content = input("")      
Send_s1.sendto(Send_content.encode("GB2312"),Send_addr)
###发送数据时,Python3需要将字符串转换成bytes类型
###encode("GB2312") 用GB2312进行编码,获取到Bytes类型对象
###decode()反过来
Send_s1.close()

#发送数据给飞秋

  • 需要确定飞秋的端口号

  • 发送普通数据,飞秋不会响应,必须发送特殊格式的内容

    1:123123:李晨浩:李晨浩_PC:32:我是吴彦祖

    因为飞秋有自己的应用层协议

    1代表版本

    1:后面的数字随便写,表示发送的时间

    32代表发送消息的指令

  • 使用while循环不延时发送可能会造成卡死

  • 注意:P和端口在网络通信中缺一不可,用到的协议也要匹配,例如飞秋用的是udp协议,使用TCP协议发数据是无效的

from socket import *
s3 = socket(AF_INET,SOCK_DGRAM)  #指定套接字
addr = ("192.168.34.45",2425)  #绑定接收端的IP和端口
for i in range(101):
    s3.sendto("1:123123:刘炳良:刘炳良_Com:32:你愁啥".encode("GB2312"),addr)

#接收数据

from socket import *
s = socket(AF_INET,SOCK_DGRAM)  #创建套接字
s.bind(("",54323))      #绑定发送端的IP和端口   IP一般不用写
redata = s.recvfrom(1024)  #1024表示接受的最大字节
print(redata[0].decode("GB2312"))  #decode需要解码
s.close()
  • 绑定信息:让一个进程可以使用固定的端口
  • 一般情况下,发送端不绑定端口,接收端会绑定

#Echo服务器

  • 用于调试和检测的工具,接收什么就原封发回什么
from socket import *
s1 = socket(AF_INET,SOCK_DGRAM)
addr = ("",54323)
s1.bind(addr)
num = 0
while 1:
    data1 = s1.recvfrom(1024)
    # print(data1)
    num += 1
    s1.sendto(data1[0],data1[1])
    print(num,data1[0].decode("GB2312"))

#UDP网络通信过程

  • 类似于发快递

    1、应用层编写数据(你好),然后向下次传递

    2、传输层在数据前面加上端口号(包括发送端口和目的端口)

    3、网络层继续在前面加上IP地址(包括原IP和目的IP)

    4、链路层再在前面加上Mac地址(硬件地址,用来定义网络设备的位置)

    此时的数据变成了:mac地址 IP地址 端口号 数据内容

    之后通过网络传输给另一台计算机的链路层开始逐步解析判断

#全双⼯的聊天程序

  • 全双工(Full Duplex)是通讯传输的一个术语。通信允许数据在两个方向上同时传输(电话)
  • 单工是只允许甲方向乙方传送信息,而乙方不能向甲方传送(收音机)
  • 半双工:甲方发消息时乙方只能收不能发(对讲机)
import threading
from socket import *
from datetime import datetime
Now_time = datetime.strftime(datetime.now(),"%Y-%m-%d %H:%M:%S")
def send():
    """
    发送端
    :return:
    """
    while 1:
        Send_s1 = socket(AF_INET,SOCK_DGRAM)
        Send_addr = ("192.168.34.131",54321)
        Send_content = input("")
        Send_s1.sendto(Send_content.encode("GB2312"),Send_addr)
def receive():
    """
    接收端
    :return:
    """
    while 1:
        Receive_s1 = socket(AF_INET,SOCK_DGRAM)
        Receive_s1.bind(("",54322))
        Receive_data = Receive_s1.recvfrom(2048)
        print(f"【{Now_time}】:",Receive_data[0].decode("GB2312"))

th_send = threading.Thread(target=send)
th_receive = threading.Thread(target=receive)
th_send.start()
th_receive.start()

#编码

编码

#UDP广播

  • 当前网络上的所有电脑的某个进程都收到同一个数据(TCP没有广播)
    UDP广播
from socket import *
import threading
def send_gb():
    s1 = socket(AF_INET,SOCK_DGRAM)
    #创建套接字
    s1.setsockopt(SOL_SOCKET,SO_BROADCAST,1)
    #设置套接字选项,以广播的形式发送数据
    while 1:
        addr = ("192.168.34.255", 6677)
        #设置IP及端口
        s1.sendto("这是爸爸的广播".encode(),addr)

def show_gb():
    s = socket(AF_INET,SOCK_DGRAM)
    s.bind(("",6677))
    data = s.recvfrom(2048)
    while 1:
        print(data[0].decode())
send_t1 = threading.Thread(target=send_gb)
show_tw = threading.Thread(target=show_gb)
send_t1.start()
show_tw.start()

#Packet_Tracer

  • Packet Tracer 是由Cisco(思科)公司发布的⼀个辅助学习⼯具,为学习思科⽹络课
    程的初学者去设计、 配置、 排除⽹络故障提供了⽹络模拟环境(不用买硬件)
  • 可以提供数据包在网络中行进的详细处理过程,观察网络实时运行情况
    (辅助学习网络通信过程)

#2台电脑相连

Packet_Tracer电脑相连

Packet_Tracer电脑连网

Packet_Tracer电脑连网2

#集线器相连

Packet_Tracer集线器连网

#交换机相连

Packet_Tracer交换机相连

Packet_Tracer交换机相连2

#路由器相连

Packet_Tracer路由器相连1
Packet_Tracer路由器相连2
Packet_Tracer路由器相连3
Packet_Tracer路由器相连4

路由器配置

#基本图形

基本图形1

#TFTP

  • TFTP((Trivial File Transfer Protocol,简单⽂件传输协议),是TCP/IP协议簇中一个用来在客户端与服务器之间进行简单文件传输的协议
  • 使用TFTP协议,就可以使用简单文件的下载
  • 特点:
    • 简单
    • 占用资源小
    • 适合传递小文件
    • 适合在局域网进行传递
    • 端口号为69
    • 基于UDP实现

#Tftpd32

  • Tftpd32:共享服务器(可以从本机共享文件)
    Tftpd32

#TFTP下载

  • 有了服务器,还需要一个下载器
  • 实现TFTP下载器:
    • 下载:从服务器上将文件复制到本机上
    • 下载的过程:
      • 在本地创建一个空文件
      • 向里面写数据(接收到一点就向空文件里写一点)
      • 关闭(接受完所有的数据关闭文件)

#TFTP下载流程

TFTP下载流程

#TFTP格式要求

![TFTP下载格式要求]](https://img2018.cnblogs.com/blog/1776444/201912/1776444-20191227131102935-105577159.png)

#struct模块

  • 当客户端接收到数据小于516(2字节操作码+2字节块编号+512字节数据)时,就意味着服务器发送完毕了(如果恰好最后一个数据长度为516,会再发一个长度为0的数据包)

  • 构造下载请求数据:“1test.jpg0octet0”

    import struct
    cmb_buf = struct.pack(“!H8sb5sb”,1,b“test.jpg”,0,b“octet”,0)
    ##!H8sb5sb :!(感叹号)表示按照网络传输数据要求的形式来组织数据(占位的格式)
      H 表示将后面的1替换成两个字节
      8s 相当于8个s(ssssssss)占8个字节,也就是对应test.jpg  占8个字节
      b占一个字节
    
  • struct模块可以按照指定格式将Python数据转换为字符串,该字符串为字节流

  • struct模块中最重要的三个函数是pack(),unpack(),calcsize()

    • pack() 按照指定的格式(fmt),把数据封装成字符串(实际上类似c结构体的字节流)

      pack(fmt, v1, v2, ...)

    • unpack() 按照给定的格式(fmt)解析字节流string,返回解析出来的元组unpack(fmt,string)

    • calcsize() 计算给定的格式(fmt)占用多少字节的内存

      calcsize(fmt)

  • struct.pack(“!H8sb5sb",1,"test.jpg",0,"octet",0)
    --这是一个下载请求格式,发送的下载包
    

    TFTP下载请求

  • struct.pack("!HH",4,pnum)
    --这是一个确认包格式,发送的是确认包
    

TFTP确认包

  • Data_Bag = s.recvfrom(1024)
    --最大接受套接字,接收数据包
    Opcode,num = struct.unpack("!HH",Data_Bag[0][:4])
    --这是从数据包里获取操作码和块编号,Opcode代表操作码,num代表块编号
    

TFTP获取数据包及操作码和块编号

#C/Python对比

C——Python对比

#从服务器下载文件

import struct
from socket import *
File_name = "qwe123.jpg"
#要下载的文件名
Dold_File = struct.pack("!H{}sb5sb".format(len(File_name.encode("GB2312"))),1,File_name.encode("GB2312"),0,"octet".encode("GB2312"),0)
#打包,构造下载请求数据,根据实际编码对编码格式进行更改
s = socket(AF_INET,SOCK_DGRAM)
#创建套接字
Addr = ("127.0.0.1",69)
# 服务器IP及地址
s.sendto(Dowlod_Re,Addr)  
#第一次发送, 连接服务器69端口
file = open(File_name,"ab")
本地创建一个文件,可以与下载的文件名不一样,但是尽量一样,得是二进制
while 1:
    Data_Bag = s.recvfrom(1024)
	#s.recvfrom配置最大接受套接字,接收数据包
    Opcode,num = struct.unpack("!HH",Data_Bag[0][:4])
    #解包,从数据包里获取操作码和块编号,Opcode代表操作码,num代表块编号
    print(Opcode,num)
    if Opcode == 5:
    #操作码为5代表有报错
        print("Error")
        break
    Data = Data_Bag[0][4:]
    #从数据包里拿出数据
    file.write(Data)
    #将数据写入到新的文件里
    if len(Data) < 512:
    #当数据字节小于512时,代表最后一个数据包里的数据不足512,表示传输完成
        break
    Ack = struct.pack("!HH",4,num)
    #打包,重新构造下载请求数据,4是固定操作码,num代表从上一个数据包获取到的数据包的块编号
    s.sendto(Ack,Data_Bag[1])
    因为服务器第一次连接之后的端口号会随机改变,所以使用获取到的数据包里的IP和端口

#上传文件到服务器

from socket import *
import struct
s = socket(AF_INET,SOCK_DGRAM)
#创建套接字
File = "haotianqi.jpg"
#要上传的文件
File_name = struct.pack("!H{}sb5sb".format(len(File.encode("GB2312"))),2,File.encode("GB2312"),0,"octet".encode("GB2312"),0)
#构造上传请求数据,根据实际编码对编码格式进行更改
Addr = ("192.168.34.112",69)
#服务器的IP及端口
s.sendto(File_name,Addr)
#第一次发生,连接服务器及端口
file = open(File, "rb")
#以读字节的方式打开文件
while 1:
    Data, Addr2 = s.recvfrom(1024)   
    #创建最大套接字,接收返回包里的返回数据和ip及端口,DATA代表数据,Addr2代表IP及端口
    print(Data, Addr2)
    #第一次连接时的端口是固定的,当开始传文件时,服务器会随机产生端口
    Opcode, num = struct.unpack("!HH",Data[:4])  
    #解包,从数据包获取返回包的操作码和块编号
    print(Opcode, num)
    if Opcode == 4:
    #获取到的操作码是4说明返回确认包ACK
        file_data = file.read(512)
        #需要上传的文件,上传数据最大为512字节
        File_name2 = struct.pack("!HH",3,num+1) + file_data
        #构造上传数据,3是固定操作码,num是上一次向服务器写的块编号,再次上传的时候需要将块编号进行+1
        s.sendto(File_name2,Addr2)
        #发送上传的数据
        if len(file_data) < 512:
        #如果发送的数据字节小于512,表示上传完毕
            print("上传完毕")
    elif Opcode == 5:
    #如果操作码等于5,表示有错误产生
        print("Error")

#Socket-TCP编程

#TCP

  • 传输控制协议(使用情况多余udp)
  • 稳定:保障数据一定能收到
  • 相对UDP会慢一点
  • web服务器一般都是使用TCP(银行转账,稳定比快要重要)

#TCP协议及通信模型

  • 协议:TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

  • 在通信之前,必须先等待建立链接
    TCP通信模型

#TCP三次握手

  • 第一次握手:建立连接时,客户端发送SYN(请求同步)包(SYN=x)到服务器,并进入SYN_SENT(请求连接)状态,等待服务器确认
  • 第二次握手:服务器收到SYN包,必须确认客户的SYN,然后在客户的SYN值上+1,得到一个ACK的包,同时自己也发送一个SYN包(SYN=y),即SYN+ACK包,此时服务器进入SYN_RECV(SYN派遣)状态
  • 第三次握手:客户端收到服务器的SYN+ACK包,然后在服务器的SYN包的值上+1,得到一个ACK的包,并向服务器将ACK确认包发送过去,此包发送完毕后,客户端和服务端进入ESTABLISHED(TCP连接成功)状态,完成三次握手,客户端与服务器才正式开始传送数据。

​ 理想状态下,TCP连接一旦建立,在通信双方的任何一方主动关闭连接之前,TCP连接都将被一直保持下去。
![三次握手]](https://img2018.cnblogs.com/blog/1776444/201912/1776444-20191227131310533-2037806882.png)

  • 在TCP传输过程中,如果有一方如果收到对方的数据,一定会发生一个ACK确认包给发送方

#TCP四次挥手

  • 第一次挥手:主动关闭方调用close,会发送一个长度为0的数据包以及FIN(结束标志),用来关闭 主动关闭方 到 被动关闭方 之间的数据传送

  • 第二次挥手:被动关闭方收到FIN包之后,会发送一个ACK给对方,确认序号为收到的序号+1

  • 第三次挥手:被动关闭方发送一个FIN后,用来关闭 被动关闭方 到 主动关闭方之间的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。

  • 第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到的序号+1,到此时,完成四次挥手

    四次挥手

#TCP服务器

  • 长连接:三次握手四次挥手之间分多次传递完所有数据(优酷看视频、在线游戏),
    长时间占用某个套接字
  • 短连接:三次握手四次挥手之间传递少部分数据,多次握手挥手才传递完所有数据
    (浏览器),短时间占用

#TCP服务器流程

  • socket绑定一个套接字
  • bind绑定IP和端口
  • listen设置最大连接数,收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求
  • accept等待客户端的链接及连接和接收请求
  • recv收数据、send发数据

#TCP单进程服务器

from socket import *
one_socket = socket(AF_INET,SOCK_STREAM)#创建一个套接字
addr = ("192.168.34.131",54322) #IP和端口
one_socket.bind(addr) #给套接字绑定IP和端口
one_socket.listen(5) #设置最大排队连接
while 1:
	new_tockte,new_addr = one_sockte.accept()
	#new_tockte为客户端返回的新的套接字,每当一个客户连接时,就会产生一个新的套接字,因为原套接字需要等待其余用户的连接
	new_tocket.send("收到".encode())
	#给客户端发送消息
	data = new_tocket.recv(1024)
	print(data.decode())
	new_tocket.close()

#TCP客户端

from socket import *
s = socket(AF_INET,SOCK_STREAM)  #创建套接字
addr = ("192.168.34.131",54322)  #设定服务器IP和端口
s.connect(addr)  #连接服务器
while 1:
    name = input(":")
    if name == "886":
        break
    s.send(name.encode())
    data = s.recv(1024)
    print(data.decode())

#TCP多进程服务器

  • one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    • 重设套接字,重复使用绑定的信息
    • 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到SO_REUSEADDR选项。
from socket import *
from multiprocessing import *
num = 0
def sild_child(two_socket,two_addr):
    try:
        while 1:
            data = two_socket.recv(1024)   #接收新的套接字返回的数据
            #当传输的数据为0字节的时候,服务器会一直在等待套接字返回的数据。直到客户端进程退出
            if len(data) > 0:
                print(f"IP:{two_addr[0]},PORT:{two_addr[1]}:{data.decode()}")
                two_socket.send("爸爸收到了".encode())
            else:
                print(f"IP:{two_addr[1]}已经关闭")
                break
    except Exception as e:
        print("ERROR")
    finally:
        two_socket.close()   #将该套接字关闭
def main():
    one_socket = socket(AF_INET,SOCK_STREAM)#创建套接字
    one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #重设套接字,重复使用绑定的信息
    addr = ("192.168.34.131",54322)
    one_socket.bind(addr)#给套接字绑定地址
    one_socket.listen(5)#设置最大排练连接
    try:
        while 1:
            two_socket,two_addr = one_socket.accept()#当有客户访问时,会产生新的套接字,三次握手也在此解决
            global num
            num += 1
            print(f"进程{num}---IP:{two_addr[0]}已连接")
            child = Process(target=sild_child,args=(two_socket,two_addr))
            child.start()
            two_socket.close()    #将该套接字关闭
    except Exception as e:
        print("ERROR")
    finally:
    	one_socket.close()   #关闭套接字
if __name__ == "__main__":
    main()

#TCP多线程服务器

  • 耗费资源比多进程小
from socket import *
import threading
def sild_child(two_socket,two_addr):
    try:
        while 1:
            data = two_socket.recv(1024)
            if len(data) > 0:
                print(f"IP:{two_addr[0]},PORT:{two_addr[1]}:{data.decode()}")
                two_socket.send("爸爸收到了".encode())
            else:
                print(f"IP:{two_addr[1]}已经关闭")
                break
    except Exception as e:
        print("ERROR")
    finally:
        two_socket.close()
def main():
    one_socket = socket(AF_INET,SOCK_STREAM)
    one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    addr = ("192.168.34.131",54323)
    one_socket.bind(addr)
    one_socket.listen(5)
    try:
        while 1:
            two_socket,two_addr = one_socket.accept()
            print("新的套接字已返回")
            print(f"IP:{two_addr[0]}已连接")
            child = threading.Thread(target=sild_child,args=(two_socket,two_addr))
            child.start()
            #用进程的话,此处的套接字需要close关闭,但是此处是线程,不用关闭
            #因为线程中共享这个套接字, 如果关闭了会导致这个套接字不可⽤
            #但是此时在线程中这个套接字可能还在收数据, 因此不能关闭
    except Exception as e:
        print("ERROR")
    finally:
        one_socket.close()
if __name__ == "__main__":
    main()

#Socketserver

#进阶版TCP并发服务器

  • 可以使用socketserver来创建socket用来简化并发服务器
  • socketserver可以实现和多个客户端通信(实现并发处理多个客户端请求的Socket服务端),他是在socket的基础上进行了一层封装,也就是说底层还是调用socket
  • 服务器接收客户端连接请求>实例化一个请求处理程序>根据服务器类和请求处理程序类,调用处理方法。
    • 例如:基本请求程序类(BaseRequesHandler)调用方法handle。此方法通过属性self.request来访问客户端套接字

#创建服务端

import socketserver
class Myserver(socketserver.BaseRequestHandler):
#创建了一个请求处理的类,必须继承BaseRequestHandler,来实现通信循环,并且得重写handle方法(),
    def handle(self):
        #在handle()中处理和客户端所有的交互,当建立连接后会自动执行handle()方法
        while 1:
            one_data = self.request.recv(1024)
            #self.request相当于客户端返回的套接字
            #handle通过属性self.request来访问客户端套接字,
            print(one_data.decode())
            self.request.send("爸爸收到了".encode())

socketserver.TCPServer.allow_reuse_address = True
允许地址端口重用
addr = ("192.168.34.131",54325)
server = socketserver.ThreadingTCPServer(addr,Myserver)
#实例化对象,将IP和端口,以及自己定义的类传入,
server.serve_forever()
#对象执行server.forever()方法后,开启服务端,
#server.forever()能够处理多个请求
#handle_request()只处理一个请求

#客户端

from socket import *
import time
s = socket(AF_INET,SOCK_STREAM)
addr = ("192.168.34.131",54325)
s.connect(addr)
while 1:
    name = input(":")
    if name == "886":
        break
    s.send(name.encode())
    data = s.recv(1024)
    print(data.decode())

#subprocess

#subprocess

  • Python可以使用subprocess下的Popen类中的封装的方法来执行命令
  • 构造方法 popen() 创建popen类的实例化对象
obj = Subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
data 表示命令内容
shell = Ture  表示命令解释器,相当于在终端执行命令
stdout=subprocess.PIPE 表示正确的内容
stderr=subprocess.PIPE 表示错误的内容
PIPE是将结构转移到当前进程

  • 通过stdout.read()获取命令执行的结果
  • 通过stderr.read()获取报错的内容
  • 指定结果后会将执行结果封装到指定的对象中
  • 然后通过对象.stdout.read()获取执行命令的结果,如果不定义stdout会将结果进行标准输出

#subprocess格式

import subprocess
obj = subprocess.Popen(
    "dir", #命令内容
    shell=True, #命令解释器,就是终端窗口
    stdout = subprocess.PIPE, #正确结果
    stderr = subprocess.PIPE  #错误结果
)
cor = obj.stdout.read().decode("gbk") #获得正确结果的内容
err = obj.stdout.read().decode("gbk") #获得错误结果的内容
print(cor + err)

#实现远程执行服务器

from socket import *
from multiprocessing import *
import struct
import subprocess
def sild_child(two_socket,two_addr):
    try:
        while 1:
            data = two_socket.recv(1024)
            if len(data) > 0:
                print(f"{two_addr[0]}-命令为:{data.decode()}")
                obj = subprocess.Popen(
                    data.decode("gbk"),
                    shell = True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                cor = obj.stdout.read()
                err = obj.stderr.read()
                allrr = cor+err
                two_socket.send(allrr)  #发送数据
            else:
                print(f"IP:{two_addr[1]}已经关闭")
                break
    except ConnectionResetError:
        print("服务异常")
        two_socket.close()

def main():
    one_socket = socket(AF_INET,SOCK_STREAM)
    one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    addr = ("192.168.34.131",54323)
    one_socket.bind(addr)
    one_socket.listen(5)
    num = 0
    try:
        while 1:
            num += 1
            two_socket,two_addr = one_socket.accept()
            print(f"进程{num}---IP:{two_addr[0]}已连接")
            child = Process(target=sild_child,args=(two_socket,two_addr))
            child.start()
            two_socket.close()
    except Exception as e:
        print("连接异常")
    one_socket.close()
if __name__ == "__main__":
    main()

#客户端

from socket import *
import struct
client = socket(AF_INET,SOCK_STREAM)
addr = ("192.168.34.131",54323)
client.connect(addr)
while 1:
    name = input(":")
    if name == "886":
        break
    client.send(name.encode())
    data = client.recv(1024)
    print(data.decode("gbk"))

#沾包

#TCP和UDP协议

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

#沾包现象

只有TCP会出现沾包现象,UDP永远不会出现沾包

​ 应用程序看到的数据是一个整体,或者说是一个流,一条消息有多少字节对应程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现沾包的原因

​ 而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位对数据进行提取,不能一次提取任意字节的数据,这一点和TCP有很大的不同。那一条消息又该如何定义?

​ 消息可以认为是为对方一次性write/send的数据为一个消息,需要知道的是当对方send一条消息的时候,无论底层怎么进行分段分片,TCP协议会把整条消息的数据段排序完成后才呈现在内核缓冲区

​ 当TCP套接字客户端往服务端上传文件时,发送文件内容都是按照一段一段的字节流进行发送的,接收方看了,根本不知道文件的字节流从何处开始,在何处结束。所谓的沾包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

__沾包不一定会发生,但是发生了,就会有两种情况:__71、在客户端发生了沾包

  • 发送端需要等待缓冲区满了才发送出去,造成沾包(发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包)

  • 2、在服务端发生了沾包

    接收方不及时接受缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

#沾包解决

#第一种:

  • send的时候加上sleep

    虽然可以解决,但是会影响效率,不可取

#第二种

问题的根源在与:接收端不知道发送端要传送的字节流的长度

  • 所以解决沾包的方法就是在发送端在发送数据之前,发送一个头文件,告诉发送的字节流总大小,然后接收端来一个死循环接收完所以数据
  • 使用struck模块可以将Python的值根据格式符,转换为固定长度的字符串(bytes类型)
####服务端###
from socket import *
import threading
import struct
import subprocess
def sild_child(two_socket,two_addr):
    try:
        while 1:
            data = two_socket.recv(1024)
            if len(data) > 0:
                print(f"{two_addr[0]}-命令为:{data.decode()}")
                #远程操作服务器
                obj = subprocess.Popen(
                    data.decode("GBK"),
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                out = obj.stdout.read()
                err = obj.stderr.read()
                allread = out + err
                ###沾包处理
                allsize = len(allread)   #获取包数据的总长度
                headsize = struct.pack("i",allsize)   #制作数据包头
                two_socket.send(headsize)  ##将固定长度的包头发送
                two_socket.send(allread) #将数据发送
    except ConnectionResetError:
        print("服务异常")
    finally:
        two_socket.cloes()
def main():
    one_socket = socket(AF_INET,SOCK_STREAM)
    one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    addr = ("192.168.34.131",54325)
    one_socket.bind(addr)
    one_socket.listen(5)
    num = 0
    try:
        while 1:
            num += 1
            two_socket,two_addr = one_socket.accept()
            print(f"进程{num}---IP:{two_addr[0]}已连接")
            child = threading.Thread(target=sild_child,args=(two_socket,two_addr))
            child.start()
    except Exception as e:
        print("Connection exception")
    one_socket.close()

if __name__ == "__main__":
    main()

####客户端
from socket import *
import struct
sliend = socket(AF_INET,SOCK_STREAM)
addr = ("192.168.34.131",54325)
sliend.connect(addr)
while 1:
    senddata = input(":")
    if senddata == "886":
        break
    sliend.send(senddata.encode())
    headsize = sliend.recv(4)  #接受服务端发送的包头(只接受4字节)
    allsize = struct.unpack("i",headsize)[0] #获取总数据字节大小
    data = b""
    while len(data) < allsize:
        data += sliend.recv(1024)
    print(data.decode("GBK"))

posted @ 2019-01-25 07:53  Na_years  阅读(229)  评论(0编辑  收藏  举报