基于TCP协议的网络通信

第二节:基于TCP协议的网络通信

 

本节具体内容如下:

  1. 对上一节内容补充总结

  2. 单个客户端与服务端通信

  3. 通信循环

  4. 通信,连接循环

  5. 远程执行命令示例

  6. 提出粘包现象

     

    1.对上一节内容补充总结

    上一节我们通篇讲的是网络相关的知识,接触了很多专业名词以及各种协议,在本节开篇先对上一届内容进行简单的汇总梳理。

    • 相关名词解释

      • 互联网协议:就是制定一系列全世界范围内都公认的通信标准,让全世界各地的人通过计算机都可以通信。(比如英语)

      • mac地址:网卡烧制的48位二进制一串数字,计算机上的唯一标识,全世界的每台计算机的mac都是不同的,唯一的,由12位16进制数表示,根据它可以查找局域网内计算机的位置。

      • 以太网协议:数据链路层的协议,可以将bit流分组,分成固定的头18个字节(源mac地址,目标mac地址,数据类型)和数据两部分 ,用于查找局域网内的目标计算机。

      • 广播:计算机在局域网内通信方式,一个计算机发出数据,同一局域网内的其他计算机都可以接受到数据。

      • 单播:数据可以单独发送,比如交换机接受到一个计算机来的数据之后,可以通过mac表查找目标mac地址对应的网口,然后单独发送。

      • 局域网内的通信是通过广播+以太网协议完成的。

      • 局域网,网段,子网:都是一个概念,都是局域网的意思。什么是局域网呢?

        局域网将一定区域内的各种计算机、外部设备和数据库连接起来形成计算机通信网,通过专用数据线路与其他地方的局域网或数据库连接,形成更大范围的信息处理系统。局域网可以实现文件管理、应用软件共享、打印机共享等功能,在使用过程当中,通过维护局域网网络安全,能够有效地保护资料安全,保证局域网网络能够正常稳定的运行。 局域网自身的组成大体由计算机设备、网络连接设备、网络传输介质3大部分构成,其中,计算机设备又包括服务器,工作站,网络连接设备则包含了网卡、集线器、交换机,网络传输介质简单来说就是网线,由同轴电缆、双绞线及光缆3大原件构成。

         

      • 集线器:存在局域网中,就是扩充网线端口,它没有mac地址学习功能,只能广播的形式进行通信。也就是low版的"交换机"。

         

         

      • 交换机:存在局域网中,也是扩充网线端口,但是能够利用mac地址学习功能绘制mac地址~网线口表,可以通过单播的形式收发数据。常见的标准的固定端口数量有8、12、16、24、48等几种。

        交换机与集线器的区别也就是集线器必须通过广播的形式发送数据,而交换机存在mac地址~网口表,可以单播的形式发送数据。

         

         

      • 交换机mac地址学习功能:交换机存储一张mac地址~端口对照表,作用是可以对照表快速定位目标mac的端口通过单播的形式传递数据。

      • ARP协议:通过计算机的IP地址获取其mac地址。我们上一节只讲了同一个局域网内ARP协议是如何工作的,本节会讲到不同的局域网内ARP协议如何工作。

      • 路由器:简单描述一下,路由器又可以称为网关设备,它就是连接为外网与不同的子网传递数据,他的包含了很多协议,其中有几个技术点需要我们清楚:

        1. DHCP协议,给局域网内的计算机自动分配IP地址。

        2. 路由器也有mac地址(下面会说到他的作用)。

        3. 路由协议,包含多个协议,主要目的就是选取到达目的路由的最优路径。

        4. 默认网关:计算机A以广播的形式发数据,当发现子网内都没有找到目标mac时,就会将数据发送到路由器上的默认网关,然后由默认网关再将数据发送出去。一般默认网关的ip地址为xxx.xxx.xxx.1。

      • IP地址:标示的一个计算机的网络地址一般都是四段十进制。

        • 公网IP:也可以直接称为外网IP,可以直接访问因特网,公网IP时唯一的。

        • 私网IP:就是路由器给你自动分配虚拟的IP,同一个局域网内的私网IP唯一,但是不可直接访问因特网。

      • 子网掩码:表示子网络的一个参数,有两个作用:

        • 与IP地址and运算,确定子网网段。

        • 不同种类的子网掩码限定了局域网内ip地址的数量也就是限定了局域网内承载计算机的上限。

          A类子网掩码:255.0.0.0

          B类子网掩码:255.255.0.0

          C类子网掩码:255.255.255.0

          由于我们国家引入计算机技术相对较晚,所以给我们国家分配的大部分都是C类子网掩码,这就意味我们如果创建一个局域网,ip地址取值范围0~255(0,255不能使用).

      • 端口协议:简单说就是数据到传输层需要封装客户端与服务端的端口号,有两种协议分别是UDP与TCP。

         

    • 同一局域网,计算机通信流程:

      https://www.processon.com/view/link/5d784083e4b04a19501d5ddb

      提取密码:taibai

    • 不同局域网,计算机通信流程:

      https://www.processon.com/view/link/5d78ab9ae4b03461a3a4d184

      提取密码:taibai

       

    • 额外解疑

      1. 有人常说数据链路层对应的设备除了计算机之外还有以太网(二层)交换机(就是我们讲的交换机)为什么?

        因为以太网交换机会将计算机发出的数据进行拆包,但是只能拆到数据链路层,他需要查看源mac地址以及目标mac地址。

      2. 网络层对应的设备有路由器,为什么?

        网络层除了计算机在拆封数据时需要封装或者解析IP地址,mac地址,路由器也是需要拆封数据直至网络层,因为它需要查看源ip 目标ip,源mac,目标mac。

      3. 家用路由器与企业路由器的区别?

        家用路由器与企业路由器区别很多,对我们有帮助的区别就是:

        家用路由器(比如大学宿舍中一个路由器可以连接几台电脑)只有一个外网IP,只能设置一个网段,每个连接电脑的端口都是内网IP,它既有路由的功能,也具有交换机的功能,因为电脑数量很少,不需要用交换机分端口。

        企业级路由器可以连接多个外网IP,可以设置多个网段,每个端口对应一个网段,每个端口都可以连接一个交换机,然后交换机在连接其他交换机...... 理论上,一个端口就可以分流出去255左右个IP地址。

      4. 路由器都必须有路由表(每个端口对应那个网段),路由协议(计算机给另一个网段发消息,计算最优路径),ARP协议(存储周围获着经常发送的路由器的mac地址与ip),可以具有交换机的功能(比如家用路由一般都有交换机的功能即有mac地址与端口的对照表)。

      5. 交换机主要就是分多个端口,而且可以泛洪(广播的功能),具有mac地址表。

      6. ARP请求以广播发送、以单播回应

      7. 路由器隔离广播。每一个网段都是独立的广播域。

      8. 跨越网段通信必须使用网关的mac地址。

      9. 上面不同网段通信时,源IP与目的IP始终不变,但是只要经过路由就需要mac地址置换。

        如想深入分析,建议模拟环境,进行抓包操作,可以看到具体经历了哪些过程,有助于深入了解。

       

      看socket之前,先来回顾一下五层通讯流程:

      img

      但实际上从传输层开始以及以下,都是操作系统以及各个硬件设备帮咱们完成的,下面的各种包头封装的过程,用咱们去一个一个做么?NO!

      img

        Socket又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展(就像我们开发一套公司管理系统一样,报账、会议预定、请假等功能不需要单独写系统,而是一个系统上多个功能接口,不需要知道每个功能如何去实现的)。于是UNIX BSD就发明了socket这种东西,socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而无需自己去进行磁盘读写,内存管理。socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。

        其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。

      套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

      基于文件类型的套接字家族

      套接字家族的名字:AF_UNIX

      unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

      基于网络类型的套接字家族

      套接字家族的名字:AF_INET

      (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

       

      2.单个客户端与服务端通信

        • 服务端代码示例:

          import socket
          ​
          # 1. 创建socket对象(买电话)
          phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 参数可以默认不写
          # 2. 绑定IP地址和端口  # 安装电话卡
          phone.bind(('127.0.0.1',8848))
          ​
          # 3. 监听(开机)
          phone.listen(5)
          ​
          # 4. 等待电话连接(等别人给你打电话)
          phone.accept()
          # 此时运行就会一直阻塞住。
          import socket
          

          参数详解:

          socket.AF_INET:基于网络的socket套接字。

          socket.SOCK_STREAM:基于TCP协议的socket套接字。

          phone.listen:这个知识点有一些不容易理解,服务端开启之后,等待客户端连接,listen做了一个客户端数量的限定,listen(n)只有n+1的客户端可以连接上我的服务端,但是连接上之后,只有第一个客户端可以与服务端进行互相通信,其他的n个客户端已经成功建立链接但是需要等待第一个客户端结束之后,逐一进行通信,通信之前的状态都是阻塞状态;n+1以外的客户端虽然也是阻塞,但是是连链接都建立不成的,就是单纯的阻塞。只有第一客户端结束之后,剩余的才可以逐一建立链接等待。这个其实与服务端开启的半链接池相关,什么叫半链接池?服务端开启之后,只要有客户端链接我,理论上来说都可以与我建立链接的,但是只要建立链接,在我服务端就会占有一定的内存,暂存这些链接数据,试想一下,如果1000万个链接进入我的服务端的内存,这样会极大的浪费内存资源,所以服务端设置一个半链接池,只允许n+1个客户端与我服务端建立链接,剩下的客户端也是处于阻塞状态,但是不会进入我的内存,这样可以控制客户端的数量,节省内存。

          accept:服务端会处于阻塞状态,直至有客户端链接我,服务端代码才会向下执行。

          服务端代码示例:此时运行一下就会一直阻塞住,我们继续完善。

        • # 1. 创建socket对象(买电话)
          phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 参数可以默认不写
          # 2. 绑定IP地址和端口  # 安装电话卡
          phone.bind(('127.0.0.1',8848))
          ​
          # 3. 监听(开机)
          phone.listen(5)
          ​
          # 4. 等待电话连接(等别人给你打电话)
          print('start...')
          conn,client_addr = phone.accept()  # 此时运行就会一直阻塞住。
          print('连接来了:',conn,client_addr)
          ​
          # 5. 接受消息
          msg = conn.recv(1024)  # 每次至多读取1024个字节
          print('客户端的消息:',msg)
          conn.send(msg.upper())
          ​
          # 6. 关闭连接
          conn.close()
          ​
          # 7. 关机
          phone.close()

           

        • 客户端代码示例:

          import socket
          ​
          # 1. 创建socket对象(买电话)
          phone = socket.socket()
          ​
          # 2. 与服务端建立链接
          phone.connect(('127.0.0.1',8848))
          ​
          # 3. 发消息
          phone.send('hello'.encode('utf-8'))
          from_server_data = phone.recv(1024)
          print(from_server_data)
          ​
          phone.close()
          ​

          同学们可以简单练习一下。

          先开启客户端就会报错,应该是先启动服务端,然后在开启客户端。进行一个错误演示,先开启客户端,就会报错: 

           

      3.通信循环

      真实场景是互相沟通,有收有发,通讯循环。

      • server端代码示例:

        import socket
        ​
        # 1. 创建socket对象(买电话)
        phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 参数可以默认不写
        # 2. 绑定IP地址和端口  # 安装电话卡
        phone.bind(('127.0.0.1',8848))
        ​
        # 3. 监听(开机)
        phone.listen(5)
        ​
        # 4. 等待电话连接(等别人给你打电话)
        print('start...')
        conn,client_addr = phone.accept()  # 此时运行就会一直阻塞住。
        print('连接来了:',conn,client_addr)
        ​
        # 5. 循环收发接受消息
        while 1:
            msg = conn.recv(1024)  # 每次至多读取1024个字节
            print(f'来自客户{client_addr}的消息:{msg.decode("utf-8")}')
            to_client = input('>>>')
            conn.send(to_client.encode('utf-8'))
        ​
        # 6. 关闭连接
        conn.close()
        # 7. 关机
        phone.close()

         

         

        • 客户端代码示例:

          import socket
          ​
          # 1. 创建socket对象(买电话)
          phone = socket.socket()
          ​
          # 2. 与服务端建立链接
          phone.connect(('127.0.0.1',8848))
          ​
          # 3. 循环收发消息
          while 1:
              to_server = input('>>>')
              phone.send(to_server.encode('utf-8'))
              from_server_data = phone.recv(1024)
              print(from_server_data.decode('utf-8'))
          ​
          phone.close()

          此时如果你直接关闭客户端,服务端就会出现如下的错误:

          所以,无论你的客户端是合理关闭,或者强制关闭,你的服务端最起码是正常关闭的。我们应该怎么解决?在服务端加上异常处理!

          服务端:

          import socket
          ​
          # 1. 创建socket对象(买电话)
          phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 参数可以默认不写
          # 2. 绑定IP地址和端口  # 安装电话卡
          phone.bind(('127.0.0.1',8848))
          ​
          # 3. 监听(开机)
          phone.listen(5)
          ​
          # 4. 等待电话连接(等别人给你打电话)
          print('start...')
          conn,client_addr = phone.accept()  # 此时运行就会一直阻塞住。
          print('连接来了:',conn,client_addr)
          ​
          # 5. 循环收发接受消息
          while 1:
              try:
                  msg = conn.recv(1024)  # 每次至多读取1024个字节
                  print(f'来自客户{client_addr}的消息:{msg.decode("utf-8")}')
                  to_client = input('>>>')
                  conn.send(to_client.encode('utf-8'))
              except ConnectionResetError:
                  break# 6. 关闭连接
          conn.close()
          # 7. 关机
          phone.close()

          客户端

          import socket
          ​
          # 1. 创建socket对象(买电话)
          phone = socket.socket()
          ​
          # 2. 与服务端建立链接
          phone.connect(('127.0.0.1',8848))
          ​
          # 3. 循环收发消息
          while 1:
              to_server = input('>>>')
              phone.send(to_server.encode('utf-8'))
              from_server_data = phone.recv(1024)
              print(from_server_data.decode('utf-8'))
          ​
          phone.close()
          但是上面也不是非常合理,因为你的客户端无论是不是正常关闭,服务端一定是一直开启状态,等待其他人链接的。

      4.通信,连接循环

      • 服务端:

        import socket
        ​
        # 1. 创建socket对象(买电话)
        phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 参数可以默认不写
        # 2. 绑定IP地址和端口  # 安装电话卡
        phone.bind(('127.0.0.1',8848))
        ​
        # 3. 监听(开机)
        phone.listen(3)
        ​
        # 4. 等待电话连接(等别人给你打电话)
        while 1:
            conn,client_addr = phone.accept()  # 此时运行就会一直阻塞住。
        # 5. 循环收发接受消息
            while 1:
                try:
                    msg = conn.recv(1024)  # 每次至多读取1024个字节
                    print(f'来自客户{client_addr}的消息:{msg.decode("utf-8")}')
                    to_client = input('>>>')
                    conn.send(to_client.encode('utf-8'))
                except ConnectionResetError:
                    break
            # 6. 关闭连接
            conn.close()
        # 7. 关机
        phone.close()
      • 客户端:
        import socket
        ​
        # 1. 创建socket对象(买电话)
        phone = socket.socket()
        ​
        # 2. 与服务端建立链接
        phone.connect(('127.0.0.1',8848))
        ​
        # 3. 循环收发消息
        while 1:
            to_server = input('>>>')
            phone.send(to_server.encode('utf-8'))
            from_server_data = phone.recv(1024)
            print(from_server_data.decode('utf-8'))
        ​
        phone.close()

          5.远程执行命令示例

      •  

posted @ 2019-12-31 08:28  太白*金星  阅读(2014)  评论(2编辑  收藏  举报