网络编程

互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层

每层运行常见物理设备

为啥要分层?

网络通信的过程很复杂,为了降低复杂性

osi7层参考模型

osi

同层使用相同协议

底层为上层服务

osi 参考模型将整个通信功能分为了7个分层.
具体有:

应用层

为应用程序提供相关的的服务,并规定应用程序中相关的通信细节.包括文件传输,电子邮件,远程登录等协议.
作用:为应用软件提供接口
 
http协议   FTP协议
端口号:区分不同的应用程序

表示层

主要负责数据格式的转换,将应用程序处理的信息转换网络能够传输的格式,或者将来自下一层的数据转换为上一层能给处理的格式.

会话层

负责建立和断开通信连接,以及数据的分割等数据传输相关的管理

传输层

确保数据能够可靠的传输到目标地址

作用:

  1. 建立端到端的连接,负责端到端的传输
  2. 传输层通过端口号区分上层服务

tcp协议

udp协议

网路层

寻ip地址和路由器的选择

作用:

  1.为网络设备提供逻辑地址

  2.负责将数据从源端发到目标端

  3.负责数据传输的寻径和转发。

IP地址对应的是因特网(广域网)

这层设备:路由器

路由器:寻找最快最优的网略传输路径,将数据转发出去

数据链路层

将源计算机网络层来的数据可靠的传输到相邻节点的目标计算机的网络层

交换机通过管理Mac地址(网卡),Mac地址是硬件设备的唯一标识,有了Mac地址,你的这个设备才叫网络设备,才具备接收和发送数据的能力。

这层的设备:交换机

Mac地址对应的是以太网(局域网)

物理层

负责比特流和电子信号的转换

作用:产生并检测电压,发射和接收具有数据的电器信号

这层设备:双绞线,光纤

tcp/ip五层协议簇

我们主要研究tcpip5层协议。它把应用层、表示层、会话层都统称为应用层

互联网协议:定义计算机如何介入Internet,以及接入Internet的计算机的通信标准。互联网的本质就是一系列的协议。

数据的封装与解封装过程

物理层

物理层解决了如何在连接各种计算机的传输媒体上传输数据比特流.

物理层功能:定义了与传输媒体接口有关的一些特性.

      1. 机械特性.规定物理连接是所采用的规格.
      2. 电器特点:规定传输二进制时电压的传输范围.
      3. 功能特定
      4. 规程特性

            

数据链路层

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

数据链路层的传输单位: 帧

数据链路层的功能

  1. 组帧.发送方把网络层的数据报添加帧首部和帧尾部形成帧,接收方根据帧的首部和尾部识别出帧的开始和结束
  2. 流量控制,当网络发成拥塞的时候,限制发送方的发送速度.
  3. 差错控制
  4. 链路管理,即连接的建立,维持和释放,主要面向tcp协议

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

ethernet规定

  • 一组电信号构成一个数据包,叫做‘帧’
  • 每一数据帧分成:报头head和数据data两部分
       head                        data                             

head包含:(固定18个字节)

  • 发送者/源地址,6个字节
  • 接收者/目标地址,6个字节
  • 数据类型,6个字节

data包含:(最短46字节,最长1500字节)约1.5k

  • 数据包的具体内容

head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送

mac地址:

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

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

mac地址标识着目标计算机是局域网的哪台计算器

广播:

有了mac地址,同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址),这种广播的形式只能在一个局域网内,出了局域网就不管用了。

ethernet采用最原始的方式,广播的方式进行通信,即计算机通信基本靠吼

工作在数据链路层的设备:

1.网卡 # 生成帧
2.交换机 #

网络层

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

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

网络层功能

  1. 提供主机和主机之间的通信服务(完成点到点的服务)
  2. 路由选择与分组转发
  3. 异构网络的互联,比如手机网和电脑网
  4. 拥塞控制

网络层传输单元是数据报

IP协议:

  • 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  • 范围0.0.0.0-255.255.255.255
  • 一个ip地址通常写成四段十进制数,例:172.16.10.1

ip地址分成两部分

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

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

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

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

 ARP协议

arp协议即地址解析协议

在现实生活中我们知道了一台机器的主机ip地址需要找到它的Mac地址,也就用到了arp协议

arp协议的用途:将目标IP地址解析为目标Mac地址

请注意:arp协议是解决同一个局域网上的主机或路由器的ip地址和硬件地址的映射问题

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

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

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

传输层

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

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

传输层是只有主机才有的层.

传输层功能:

  1. 提供进程和进程之间的逻辑通信.
  2. 提供复用和分用功能.复用指的是各个应用程序都可以通过传输层来到达网络层,分用指的是:传输层可以网络层传过来的不同的数据分发给对应的程序.
  3. 对应用层传过来的报文进行差错检测.

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

里边有:tcp协议

    udp协议

 应用层

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

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

主要协议:http协议和ftp协议等  

各个层的首部

首先应用层把数据传给传输层,传输层在首部加上源端口和目标端口,接着传给网络层,网络层在首部添加原地址和目标地址,数据链路层在首部添加源mac和目标Mac

数据                             #应用层
port+数据=数据端                  #传输层 

ip+port+数据=数据包 (报文)       # 网络层

Mac+ip+port+数据=数据帧(frame)  # 数据链路层 交换机工作在这里,这里要添加FCS校验数据的完整性

交换机转发数据帧 #  数据链路层

路由器转发数据包  # 网络层
防火墙分为:应用层、传输层、网络层

socket

套接字标识了一个主机和主机上的一个网络进程

socket=(主机IP地址:端口号)

而我们在python中的socket指的是套接字对象也就是一个接口

socket中文为“插座”,它本身不是一个协议而是一个套接字,socket套接字是操作系统为了方便大家使用tcp/ip协议而存在的一个接口,是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。

学习了很长时间我对套接字的概念很模糊,究竟什么是套接字呢?应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字 (Socket)的接口。

socket 和file 的区别:

file 是对某个件的打开-读写-关闭

socket 是对服务器和客户端进行打开读写关闭。

TCP协议

TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,只简单的描述下这三次对话的简单过程:主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据

TCP的特点

1.tcp是面向连接的.
2.每一条tcp连接只有两个端点.端点就是套接字
3.tcp提供可靠交付
4.tcp是面向字节流
5.tcp提供全双工通信

###具体见计算机网络课本

基于TCP协议的套接字

TCP编程的服务器端一般步骤是: 
  1、创建一个socket,用函数socket(); 
  2、设置socket属性,用函数setsockopt(); * 可选 
  3、绑定IP地址、端口等信息到socket上,用函数bind(); 
  4、开启监听,用函数listen(); 
  5、接收客户端上来的连接,用函数accept(); 
  6、收发数据,用函数send()和recv();
  7、关闭网络连接; 
  8、关闭socket; 

TCP编程的客户端一般步骤是: 
  1、创建一个socket,用函数socket(); 
  2、设置socket属性,用函数setsockopt();* 可选  这步一般不写
  3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选  这步一般不写
  4、设置要连接的对方的IP地址和端口等属性; 
  5、连接服务器,用函数connect(); 
  6、收发数据,用函数send()和recv(),或者read()和write(); 
  7、关闭socket;

范例一:

服务端:

import socket
phone=socket.socket(socket.AF_INET,SOCKET.SOCK_STREAM) # socket.AF_INET指的是基于网络通信,socket.SOCK_STREAM指的是TCP协议
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#加上它可以重复使用一个端口
phone.bind(("192.168.21.37",8085)) #客户端的端口是随机的,服务端的端口是固定的,注意bind的里是一个元组,其中IP地址是一个字符串,端口范围是0-65535,其中端口1-1024被系统占用。

phone.listen(5) #写5,最多能挂起5个链接
print("starting....")
while True:#链接循环
    conn,addr=phone.accept()#accept接收客户端的链接和IP地址,得到的是一个元组。
    print(conn)
    print(addr)
    while True:
        try:
            date=conn.recv(1024) #表示最大收多少个字节
            conn.send(date.upper())
        except ConnectionResetError:
            break
    conn.close()
phone.close()

客户端:

import socket
phone=socket.socket() # socket.AF_INET指的是基于网络通信,socket.STREAM值得是TCP协议

phone.connect(("192.168.21.37 ",8085)) #链接服务端IP地址,#端口范围0-65535 ,其中1-1024被系统占用了,不能用,注意这里是一个元组

while True:
        msg=input(">>>").strip()
        if not msg:
            continue
        phone.send(msg.encode("utf-8"))#必须要转化为二进制
        date=phone.recv(1024)
        print(date.decode("utf-8"))
phone.close()

客户端只有一个套接字,服务端有两个套接字

范例二

使用cmd命令操作系统

服务器端:

import subprocess
import socket
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8081))
server.listen(5)
print("starting.....")
while True:
    conn,addr=server.accept()
    print(addr)
    while True:
        try:                         #加上异常处理的原因是如果客户端单方面的停止程序,服务器会捕获到,然后就从该循环中跳出来,避免一直等待接收数据
            cmd=conn.recv(8096)
            if not cmd:break
            cmd=cmd.decode("utf-8")#把二进制解码为utf-8字符
            obj=subprocess.Popen(cmd,shell=True,               #这里得到一个对象
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)   #如果把shell为True,指定的命令会在shell里执行,subprocess.popen返回的是一个对象
            stdout=obj.stdout.read()#这里是正确的命令得到的bytes类型
            stderr=obj.stderr.read()#错误的命令
            conn.send(stdout+stderr)  #发送bytes类型
        except ConnectionResetError:
            break
    conn.close()
sever.close()

subprocess 模块 :在主进程下开启了一个子进程。这是一个强大的模块,它可以替代os模块,基本上包含了os模块,打开程序命令时,我们为什么不用os 模块中的system,而是用的是os.popen()? 原因在于system没有返回值,它直接把结果打印到屏幕上,我们无法获取,赋值也不行,只能得到0或1.,但是os。popen(),可以读出执行命令的内容。注意popen的到的是一个对象,对其进行读取使用read()。

客户端:

from socket import *   #虽然这个方法,不推荐使用,但是在socket模块中可以使用这种方法
client=socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8081))
while True:
    cmd=input(">>>:").strip()
    if not cmd:     #避免发空字符,空字符操作系统不会执行
        continue
    client.send(cmd.encode("utf-8"))
    data=client.recv(1024)
    print(data.decode("gbk")) #Windows系统下执行命令使用的是GBK编码方式
client.close()

 粘包

粘包问题:TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾,这个问题只会发生在TCP协议下。

为什么会发生粘包问题?

发送方原因:TCP协议使用nagle算法,会把数量较少并且发送时间间隔短的包合成一个包发送给客户端,客户端分辨不出来。

接收方原因: TCP 接收到数据时,并不会立即把数据传给物理层而是会在操作系统中缓存,如果TCP接受的速度大于应用程序读取的速度,那么多个包就会在操作系统中发生粘包现象。接收方不及时接收缓冲区的包,造成一次接收多个包。

分析的解决这个问题方法?

发送方:我们可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭Nagle算法。这个我是从某个大神博客中看的,具体怎么操作不晓得。

接收方:我们只能在应用层解决问题。

解决方法就是循环处理,应用程序在处理从缓存读来的分组时,读完一条数据时,就应该循环读下一条数据,直到所有的数据都被处理,才能再处理下一个包。

针对这个问题,一般有2种解决方案:

      (1)发送固定长度的消息

      (2)把消息的尺寸与消息一块发送

服务端:

import subprocess
import struct
import json
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8081))
# print(server)
server.listen(5)
while True:
    conn,addr=server.accept()
    # print(conn)
    print(addr)
    while True:
        try:
            cmd=conn.recv(8096)
            if not cmd:break #针对linux

            #执行命令
            cmd=cmd.decode('utf-8')
            #调用模块,执行命令,并且收集命令的执行结果,而不是打印
            obj = subprocess.Popen(cmd, shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE,
                                   )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()


            # 1:先制作报头,报头里放:数据大小, md5, 文件
            header_dic = {
                'total_size':len(stdout)+len(stderr),
                'md5': 'xxxxxxxxxxxxxxxxxxx',
                'filename': 'xxxxx',
                'xxxxx':'123123'
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('utf-8')
            header_size = struct.pack('i', len(header_bytes))

            # 2: 先发报头的长度
            conn.send(header_size)

            # 3:先发报头
            conn.send(header_bytes)

            # 4:再发送真实数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()

server.close()
服务端

struct 模块   一种机制将某些特定的结构体类型打包成二进制流的字符串然后再网络传输。按照给定的格式(fmt)把数据封装成二进制。

关于特定的fmt格式如下图所示:

struct模块有两种函数,struct.pack(),把任意数据类型转换为bytes。unpack 把bytes类型转换为任意数据类型。

客户端:

import struct
import json
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8081))

while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))

    # 1:先收报头长度
    obj = client.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 2:先收报头,解出报头内容
    header_bytes = client.recv(header_size)
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)

    print(header_dic)
    total_size = header_dic['total_size']

    # 3:循环收完整数据
    recv_size=0
    res=b''
    while recv_size < total_size:
        recv_data=client.recv(1024)
        res+=recv_data
        recv_size+=len(recv_data)
    print(res.decode('gbk'))

client.close()

 tcp的连接建立和连接释放

三次握手

tcp建立连接的过程叫握手

1.客户端向服务端发送请求连接报文.第一次握手
2.服务端发送确认报文,第二次握手
3.客户端发送确认报文,并携带数据 .第三次握手.客户端发送确认报文的原因是防止已失效的连接请求报文再次传送到服务端,因而产生错误.

四次挥手

1.客户端向服务端发送连接释放报文.第一次挥手
2.服务端向客户端发送确认报文,第二次挥手.
3.服务端向客户端发送连接释放报文,第三次挥手
4.客户端发送确认报文,第四次挥手.

UDP协议

UDP(User Data Protocol,用户数据报协议)
(1) UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
(2) 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。
(3) UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
(4) 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
(5)UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表(这里面有许多参数)。
(6)UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。

 UDP协议称为数据报协议SOCK_DGGAM.,

udp的特点

1UDP是无连接的,即传输数据前不需要建立连接
2.udp提供尽最大努力交付
3.udp是面向报文的,而tcp是面向字节流的.
4.udp提供1对1,一对多,多对多通信.由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。
5.udp首部比tcp短,udp8个字节,tcp20个
6.udp没有拥塞控制

为什么基于UDP协议的套接字没有粘包现象?

什么是保护消息边界和流呢?

保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息.也就是说存在保护消息边界,接收 端一次只能接收发送端发出的一个数据包。

TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.
对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据。
不会少接收也不会多接收.

 基于UDP协议的套接字

由于udp协议是无连接的协议,因为不使用listen和recv系统调用

与之对应的UDP编程步骤要简单许多,分别如下: 
  UDP编程的服务器端一般步骤是: 
  1、创建一个socket,用函数socket(AF_INET,SOCK_DGRAM); 
  2、设置socket属性,用函数setsockopt();* 可选 
  3、绑定IP地址、端口等信息到socket上,用函数bind(); 
  4、循环接收,发送数据,用函数recvfrom(),sendto(); 
  5、关闭socket连接; 

UDP编程的客户端一般步骤是: 
  1、创建一个socket,用函数socket(AF_INET,SOCK_DGRAM); 
  2、循环接收,发送数据,用函数recvfrom(),sendto()注意括号中写上发送的bytes类型的内容和一个包括服务器端的IP和端口; 
  3、关闭socket;

服务端:

from socket import *
server=socket(AF_INET,SOCK_DGRAM)  #注意基于UDP协议的套接字,是SOCK_DGRAM
server.bind(("127.0.0.1",8080))  #需要绑定IP地址
while True:
    data, client_addr = server.recvfrom(1024)
    print(data.decode("utf-8"),client_addr)
    server.sendto(data.upper(),client_addr)
server.close()

客户端:

from socket import *
client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input(">>>").strip()
    client.sendto(msg.encode("utf-8"),("127.0.0.1",8080))
    data,addr=client.recvfrom(1024)
    print(data.decode("utf-8"))
    print(addr)
client.close()
原文地址:https://www.cnblogs.com/sticker0726/p/7905796.html