网络编程

网络架构:

C/S B/S :

C:客户端,B:游览器(rowser),S:服务端

  • C/S:客户端与服务器之间的架构:QQ,微信,游戏,App都属于cs架构

    • 优点:安全,响应速度快,个性化功能多

    • 缺点:开发和维护成本高,面向客户固定

  • B/S:游览器与服务器之间的架构:

    • 优点:开发维护成本低,维护成本低,面向的用户广泛
    • 缺点:相对不安全,响应速度相对慢,个性化设计单一

互联网通讯原理:

  • 打电话示例:
  • 互联网通信:
    • 1,一堆物理介质将两个电话连接起来
    • 2,拨号
    • 3,统一的通信标准,一揽子协议,这些互联网协议就是一个个标准,最终可以通信

网络五层:

  • osi五层:

    • OSI(Open System Interconnect)开放式系统互联

      • osi是一个标准,是一个规范,五层协议都是操作系统帮我们封装各种head
    • 应用层 python (应用层包括会话层,表示层) 会话:保证多次网络传输准确性,表示:保证数据安全

      ​ 应用层常见协议:HTTP、HTTPS、FTP、SMTP、TELNET、POP3、DNS、DHCP

    • 表示层 数据的表示,安全,压缩,格式有JPEG,ASCII 加密格式等

    • 会话成 建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话

    • 传输层 tcp udp 四层路由器 四层交换机

    • 网络层 路由器 三层交换机 ipv4 ipv6 (国际协议)

    • 数据链路层 二层傻瓜交换机,以太网协议,mac arp 网卡

    • 物理层 物理连接介质,网线,光纤,发送的是010101比特数据流

  • 1.物理层:

    • 物理连接介质:网线,光纤:发送的数据就是0101010 1比特数据流

  • 2.数据链路层:

    • 交换机

    • 以太网协议:

      • 以太网协议是局域网中的一种协议

      • 将比特流数据分组,一组叫做一帧,每一数据帧分成:报头head和数据data两部分

    • 数据头(head)|data数据:

    • 数据头:固定长度18个字节(标准)(源地址,目的地址,端口,数据类型)

    • data数据:最短46字节,<=data <=1500字节

    • 以太网问题:
      1,数据头为什么要固定?
         固定就是一个标准,为了提取源地址以及目标地址
         
      2,以太网协议中源目标地址如何设置唯一?
      	网线直接接触的硬件就是网卡,网卡上有一个mac地址,确定计算机的唯一性的物理地址
          
      网卡mac地址:12位,16进制组成的一串数字,前六位厂商编号:后六位:流水线号
      

      head(源地址,目的地址,端口,数据类型)|data("俊丽")

    • 广播/单播:

      • 计算机最原始的通信方式

      • 数据经过以太网协议封装后(head)|data,先要从局域网内广播,

        • 广播:

          • 一对多连接,有了mac地址,两台主机通信,一台主机通过arp协议获取到另一台主机的mac地址
          • 广播只存在局域网内,广播通信基本靠吼
        • 单播:

          • 一对一连接(第一次是广播访问,第二次交换机有学习功能,将mac写入表中,下次进行单播)

          • 优点:提升效率

      • 交换机学习功能:

      • 交换机对照(网口与MAC地址的)表:
        1:    1C-1B-0D-DA-E8-8F
        2:    FF-FF-FF-FF-FF-FF
        3:    FF-FF-FF-FF-FF-FF
        4:    FF-FF-FF-FF-FF-FF
        5:    1C-1B-0F-4A-E8-8F
        
        网口1: 出来一条信息:
        # 第一次广播的形式发出去
        网口1:源地址: 1C-1B-0D-DA-E8-8F 目标地址: 1C-1B-0F-4A-E8-8F  |   明天放假
        
        2,3,4,5口接收到次消息,查看目标mac地址是否是自己的,5口确定是自己的.
        
        每个网口都广播发送消息一遍之后,对照表就构建好,下次在任意的网口在发消息,就直接以单播的形式发送.
        目的: 避免局域网内每一次都广播的形式通信.以后就可以单播,提升效率.
            
        mac地址:head报头中的mac地址 ,都会替换,了解
        
    • arp协议:

      • 通过ip获取到mac地址

      • ARP协议:就是将对方的ip地址获取到对方的MAC地址
        如果两个用户进行第一次通信的时候,你不可能知道对象的mac地址么? 但是你必须要知道对方的IP地址.
        
        IP + ARP协议 = 获取对方的MAC地址
        
        源mac                           目标mac               源ip               目标ip                     数据部分
        发送端mac: 1C-1B-0D-DA-E8-8F      FF:FF:FF:FF:FF:FF  172.16.10.10/24    172.16.10.11/34         今晚一起吃饭
        
        目标计算机
        (源mac:1C-1B-0F-4A-E8-8F  目标mac 1C-1B-0D-DA-E8-8F 源ip: 172.16.10.11/34 目标ip:172.16.10.10/24)
        

  • 3.网络层:

    • 路由器

    • 网络层的作用是解决客户端到服务端连接中的各个节点问题

    • IP协议:

      • ipv4 ipv6 国际互联网协议第四版

      • 广播,mac地址+IP == 可以找到世界上任意一台计算机

      • IP:

        • IP地址:
        • ip地址+子网掩码确定计算机所在网段,子网,局域网位置
          • ipv4地址:四点分十进制,范围 0-255.0-255.0-255.0-255
        • 子网掩码:
          • 一般都是c类
          • 255.255.255.0
          • ip地址+子网掩码可以计算出IP网段,可以分254个ip地址,同一局域网内的ip地址不能重复

传输层:(tcp,udp)

  • 端口协议:传输层建立了点到点的连接

    • 端口号:0-65535 系统占用端口:1-1023 软件端口:1024-8000

    • 广播,mac地址+IP +端口 == 可以找到世界上任意一台计算机对应的软件

  • TCP:

    • tcp是一种面向连接,可靠的,基于IP的传输层协议

    • 使用的tcp的应用:Web浏览器,文件传输

    • 优点:

      • 可靠的面向连接协议,稳定安全 (面向连接就是双方通信)
    • 缺点

      • 效率相对低,传输速度慢些
  • UDP:

    • 面向数据报协议,无连接协议

    • 使用UDP应用:域名系统,视频流,IP语言,微信,QQ

    • 优点:

      • 效率高,速度快,发送数据前不需要建立连接
    • 缺点:

      • 不安全,不可靠,容易丢包

应用层:

  • 应用层存储的都是自己开发的程序,HTTP

  • 软件自己定义的协议:

    • qq:发送数据:“海洋“ ------> [”海洋“]

    • 将数据按照自己定义的协议进行封装(http ftp协议等等)

  • 物理层 --> 数据链路层(以太网协议,mac) --> 网络层 --> 传输层( tcp,udp) --> 应用层

  • mac地址 + 广播形式 +ip地址 + 端口 ==可以找到任意一台机器

握手状态:

  • seq 序号 用来表示tcp源端像目标端发送字节流,发送方发送数据对此进行标记 32位
  • ACK 确认请求 只有ACK标志位等于1事,确认序号才有效,ack = seq + 1
  • SYN 发送请求
  • FIN 释放一个连接
  • 注意:确认方=发起方req + 1 ,两端配对

三次握手:

  • 客户端与服务端第一次需要建立通信联系,需要三次握手:

  • 第一次握手:

    • 客户端向服务端发送syn=1的请求和一个随机seq等于x的报文,进入发送状态
  • 第二次握手:

    • 两条请求合并成一条请求
    • 服务端收到客户端发送的syn报文,给客户端回应一个ack=x+1(我收到你的报文,进行+1)的确认请求和同样还会返回syn=1的请求一个随机seq等于y的报文 ,进入接收状态
  • 第三次握手:

    • 客户端收到服务器syn报文和确认请求,在回应服务器一个ack = y+1报文,客户端和服务端进入建立连接状态,完成三次握手
  • 三次握手意义:

    • 简述:主要防止客户端已经失效的连接请求报文突然传到了服务端,而产生的错误
  • 客户端向服务端发送一个连接请求,这个请求因为网络节点问题滞留了,请求在到达服务器已经是一个失效的连接,如果没有三次握手,服务器确认请求,建立连接,但是请求是失效的,客户端不会理会服务器的确认信息,也不会发送消息,服务端一直等待客户端发送数据,这样很多资源就浪费了,这就是三次握手的作用

  • 为什么要进行三次握手:

    • TCP协议在建立连接时,需要确认通信的双方收发信息的功能都是正常的,因此要进行三次握手

四次挥手:

  • 建立的连接不能一直连接着

  • 第一次挥手:

    • 客户端向服务器发送FIN=1,seq序号=x 客户端停止发送数据
  • 第二次挥手:

    • 服务端收到FIN,回复确认请求 ack=x+1

      客户端到服务端这条路断开

  • 第三次挥手:

    • 服务端向客户端发送FIN=1 ,seq=y
  • 第四次挥手:

    • 客户端发送ack=1 + y

      服务端到客户端这条路断开

  • 为什么要进行四次挥手:

    • tcp是一个面向连接,全双工模式,两条通道要进行两次确认才断开

    • 客户端向服务端发送Fin关闭连接,服务端确认后,服务端确认后就表示服务端没有数据在发送给客户端,

    • 客户端到服务端通道关闭

    • 但是服务器的数据不一定都完整传给了客户端,服务器不会直接关闭,他可能还会发送数据给客户端,

    • 之后,再次发送FIN报文,给客户端,客户端在同意后在关闭连接

    • 服务端到客户端通道关闭

Socket:

  • socket 套接字就是一个网络通信的工具,建立客户端与服务端的通信 ,他存在于应用层和传输层之间的抽象层,这个通信工具封装好了所有的底层接口,可以直接调用这些接口,提高开发效率

TCP_Socket:

  • #服务端
    import socket
    
    phone = socket.socket()                 #创建socket对象,默认tcp协议
    phone.bind(("127.0.0.1",666))           #绑定IP地址和端口
    phone.listen(5)                         #监听,开机状态,5同时可以有多少个请求连接
    
    conn,addr = phone.accept()              #建立连接,等待客户端,conn两条管道,addr客户端地址
    
    while 1:
        from_client_data = conn.recv(1024)   #接受消息和多少字节
        print(f'来自客户端的{addr}消息{from_client_data.decode("utf-8")}')
    
        to_server = input(">>>>")
        conn.send(to_server.encode("utf-8"))  #发送
    
    conn.close()    #关闭通道连接
    phone.close()  #关闭socket套接字
    
    #客户端
    import socket
    
    phone = socket.socket()
    phone.connect(("127.0.0.1",666))            #连接server服务端信息
    
    while 1:
        to_client = input(">>>")
        phone.send(to_client.encode("utf-8"))    #send发送
    
        from_server_data = phone.recv(1024)      #接受消息和多少字节
        print(from_server_data.decode("utf-8"))
    
    phone.close()
    

  • 通讯循环-一个对多个客户端,客户端需要排队
  • import socket
    
    phone = socket.socket()                 #创建socket对象,默认tcp协议
    phone.bind(("127.0.0.1",666))           #绑定IP地址和端口
    phone.listen(5)                         #监听,开机状态,5同时可以有多少个请求连接
    
    while 1:
        print("开启")
        conn,addr = phone.accept()          #建立连接,等待客户端,conn连接管道,addr客户端地址
        print(conn,addr)
    
        while 1:
            try:  #客户端异常退出报错进行异常处理
                from_client_data = conn.recv(1024)    #recv:接受消息和多少字节,夯住
                if from_client_data == "q".encode("utf-8"):
                    break
                print(f'来自客户端的{addr}消息{from_client_data.decode("utf-8")}')
    
                to_server = input(">>>>")
                conn.send(to_server.encode("utf-8"))  #send:发送
            except ConnectionResetError:
                break
    
        conn.close()    #关闭通道连接
    phone.close()  	    #关闭socket套接字
    
    #客户端还引用上面那个
    
  • 执行win端命令:
  • #服务端:
    import subprocess
    import socket
    
    phone = socket.socket()                  #创建socket对象,默认tcp协议
    phone.bind(("127.0.0.1",1666))          #绑定IP地址和端口
    phone.listen(5)                          #监听,开机状态,5同时可以有多少个请求连接
    
    while 1:
        print("开启")
        conn,addr = phone.accept()           #建立连接,等待客户端,conn连接管道,addr客户端地址
        while 1:
            try:
                cmd = conn.recv(1024)        #接受消息和多少字节,夯住
                obj = subprocess.Popen(cmd.decode("utf-8"),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       )
                result = obj.stdout.read() + obj.stderr.read()
                conn.send(result)
    
            except ConnectionResetError:
                break
    
        conn.close()    #关闭通道连接
    phone.close()       #关闭socket套接字
    
    #客户端:
    import socket
    phone = socket.socket()
    phone.connect(("127.0.0.1",1666))           #连接server服务端信息
    
    while 1:
        cmd = input(">>>").strip()
        phone.send(cmd.encode("utf-8"))   #将命令转为utf-8发送
    
        result = phone.recv(1024)          #接受的消息为windows系统编码转为gbk
        print(result.decode("gbk"))
    
    phone.close()
    

粘包:

  • 缓冲区作用:

    • Socket进行send和recv的时候,都要通过输出缓冲区和输入缓冲区,缓冲区可以暂时解决网络不稳定问题,提高稳定性

    • 缓存区一般为8K

  • 产生粘包的现象(只有TCP):

    • 粘包发生在tcp协议中
  • 产生粘包的现象有两种recv和send:

    • recv产生粘包:

      • 如果你的recv最大接受值为1024字节,当你接受一个1500的字节,recv只会接受1024剩下的476个字节会停留在输入缓存区,当你第二次在发送数据,造成了数据的粘包。
    • send产生粘包:

      • 当你连续send发送少量的数据,tcp会根据优化算法将数据合并成一个TCP段发送出去

  • 解决粘包方式:

    • 1、可以扩大recv的上限,但是会对服务器内存占用

    • 2、客户端sleep睡眠,这样会非常影响效率

  • 正确思路:

    • 当第二次接受数据前,循环recv将所有的数据取完

  • recv的工作原理:

    • send多次,recv一次(不是一发一收制)
      
      当缓存区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭,关闭远程端并读取所有数据后,返回空字符
      

low版:

  • 问题1:非常大的数据使用struct会报错
    问题2:报头信息不可能只包含数据总大小
    
    # 服务端
    import subprocess
    import socket
    import struct
    
    phone = socket.socket()  		 # 创建socket对象,默认tcp协议
    phone.bind(("127.0.0.1", 1666))  # 绑定IP地址和端口
    phone.listen(5)  			     # 监听,开机状态,5同时可以有多少个请求连接
    
    while 1:
        print("开启")
        conn, addr = phone.accept()    # 建立连接,等待客户端,conn连接管道,addr客户端地址
        while 1:
            try:
                cmd = conn.recv(1024)  # 接受消息和多少字节,夯住
                obj = subprocess.Popen(cmd.decode("utf-8"),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       )
                result = obj.stdout.read() + obj.stderr.read()
    
                total_size = len(result)  # 制作报头(371)
                # 将不固定长度的int类型报头,转化成固定4字节(b'sx01x00x00')
                total_size_bytes = struct.pack("i", total_size)
    
                conn.send(total_size_bytes)  # 发送字节长度和数据信息
                conn.send(result)
    
            except ConnectionResetError:
                break
    
        conn.close()
    phone.close()
    
    # 客户端:
    import socket
    import struct
    
    phone = socket.socket()
    phone.connect(("127.0.0.1", 1666))  # 连接server服务端信息
    
    while 1:
        cmd = input(">>>").strip()
        phone.send(cmd.encode("utf-8"))
    
        head_bytes = phone.recv(4)  # 接受报头四个字节(b'sx01x00x00')
        total_size = struct.unpack("i", head_bytes)[0]  # 将报头反解回int类型(371)
    
        total_data = b''  # 循环接受原数据
        while len(total_data) < total_size:
            total_data += phone.recv(1024)
    
        print(total_data.decode("gbk"))
    
    phone.close()
    

旗舰版-自定义报头版:

  • 解决粘包思路:

    • 1,当第二次接受数据前,循环recv将所有的数据取完

      • result 3000bytes recv 3次

        result 5000bytes recv 5次

        result 30000bytes recv ?次 ---> 循环次数相关

    • 2,接受总数据bytes,进行while循环,当接受数据大于总数据,循环结束

    • 3,怎么获取到总bytes个数:数据转成bytes之后len() =总bytes数

      • dict报头-->“dict报头”---->json("dict报头") = 字典数据
      • len(json("dict报头")) --->len()转成四个字节 = 字典报头长度
      • 先发送字典报头长度,在发送字典数据
  • #服务端:
    import socket
    import subprocess
    import struct
    import json
    
    phone = socket.socket()
    phone.bind(('127.0.0.1', 8888))
    phone.listen(5)
    
    while 1:
        print('start')
        conn, addr = phone.accept()
        while 1:
            try:
                cmd = conn.recv(1024)
                obj = subprocess.Popen(cmd.decode('utf-8'),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       )
    
                result = obj.stdout.read() + obj.stderr.read()
                result = result.decode('gbk').encode('utf-8')
    
                head_dict = {
                    'MD5': 'fdsaf2345544324dfs',
                    'file_name': '视频文件',
                    'file_size': len(result),
                }                                                 #制作报头
    
                head_dict_json = json.dumps(head_dict)            #将报头字典转化成json序列化
                head_dict_json_bytes = head_dict_json.encode('utf-8')   #将json字符串 转化成bytes
    
                head_len = len(head_dict_json_bytes)         #获取字典的长度(88)
                head_len_bytes = struct.pack('i',head_len)  #将长度转化成固定的4个字节 (b'Xx00x00x00')
    
    
                conn.send(head_len_bytes)      #字典报头长度--->4个字节(b'Xx00x00x00')
                conn.send(head_dict_json_bytes)  # 字典数据bytes
                conn.send(result)                #发送原数据bytes
    
            except ConnectionResetError:
                break
        conn.close()
    phone.close()
    
    #客户端:
    import socket
    import struct
    import json
    
    phone = socket.socket()
    phone.connect(('127.0.0.1', 8888))
    
    while 1:
        cmd = input('>>>').strip()
        phone.send(cmd.encode('utf-8'))
    
        jie_bytes = phone.recv(4)
        total_size = struct.unpack('i', jie_bytes)[0]     #接收字典报头长度,struct转化为int
    
        head_bytes = phone.recv(total_size)               #接受字典数据,total_size字典报头长度
        head_dict  = json.loads(head_bytes.decode("utf-8"))  #将json字典转为字典
    
        total_size = head_dict["file_size"]         #通过字典取出要发送过来数据的总长度
    
        recv_data = b''                             #循环累加接受原数据
        while len(recv_data) < total_size:          #不等于,当等于还会进去取值,这时值已经没有了
            recv_data += phone.recv(1024)
    
        print(recv_data.decode('utf-8'))
    
    phone.close()
    

UDP_Socket:

  • udp协议:不可靠,相对来说不安全的协议,面向数据报无连接协议(可靠不可靠是指数据是否能到达)效率高,速度快

  • 基础通信:
    • UDP通信没有tcp连接通道,服务端和客户端每次发送都需要带着IP地址和端口号

    • #服务端:
      import socket
      
      udp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建udp协议对象
      udp_server.bind(("127.0.0.1",666))
      
      while 1:
          from_client_data = udp_server.recvfrom(1024)  #(b'niha', ('127.0.0.1', 64180))
          print(f'来自{from_client_data[1]}{from_client_data[0].decode("utf-8")}')
      
          to_client_data = input("输入").strip()
          udp_server.sendto(to_client_data.encode("utf-8"),from_client_data[1])  
          
      #客户端:
      import socket
      
      udp_client= socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建udp协议对象
      
      while 1:
          to_client_data = input("输入").strip()
          udp_client.sendto(to_client_data.encode("utf-8"),("127.0.0.1",666))
      
          from_server_data = udp_client.recvfrom(1024)
          print(f'来自{from_server_data[1]}的消息:{from_server_data[0].decode("utf-8")}')
      

Socket_Server多线程通信:

  • #服务端:
    import socketserver
    
    class MyServer(socketserver.BaseRequestHandler): # 继承的类固定的
    
        def handle(self):  # 必须是这个handle名字,类的约束
            while 1:
                from_client_data = self.request.recv(1024).decode('utf-8') #self.request == conn管道
                print(from_client_data)
    
                to_client_data = input('>>>').strip()
                self.request.send(to_client_data.encode('utf-8'))
    
    if __name__ == '__main__':
    
        ip_port = ('127.0.0.1',8848)
        server = socketserver.ThreadingTCPServer(ip_port,MyServer)
        # server.allow_reuse_address = True
        # print(socketserver.ThreadingTCPServer.mro())
        # [ThreadingTCPServer, ThreadingMixIn,TCPServer, BaseServer]
        server.serve_forever()
        
    #客户端:
    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1', 8848))
    
    while 1:
        content = input('>>>').strip()
        client.send(f'海洋:{content}'.encode('utf-8'))
    
        from_server_data = client.recv(1024)  # 夯住,等待服务端的数据传过来
        print(f'来自服务端消息:{from_server_data.decode("utf-8")}')
    
    phone.close()
    
原文地址:https://www.cnblogs.com/haiyang11/p/11172204.html