网络编程及socket模块

目录

1.必备基础

1.1 网络架构

 1.1.1 交换机

1.1.2 路由器

1.1.3 三层交换机

1.1.4 小型企业基础网络架构

1.1.5 家庭网络架构

1.1.6 互联网

1.2 网络核心词汇

1.2.1 子网掩码和IP

1.2.2 DHCP

1.2.3 内网和公网IP

1.2.4 云服务器

1.2.5 端口

2. 网络编程 socket模块

案例:智障客服

案例:文件上传

案例:用户登录认证

3. B/S和C/S架构

小总结

1. OSI 7层模型

2. UDP和TCP协议

2.1 UDP和TCP 示例代码

2.2 TCP三次握手和四次挥手

3. 粘包

如何解决粘包的问题?

案例:消息 & 文件上传

4. 阻塞和非阻塞

5. IO多路复用


提前小结

用于快速复习 详情请跳过向下看详细

简述交换机,路由器,三层交换机,子网掩码和IP区分是否同一网段,DHCP,内网ip外网ip。

cmd:ipconfig 百度:ip

seq syn等

send和sendall区别

服务端代码

import os
import json
import socket
import struct


def recv_data(conn, chunk_size=1024):
    # 获取头部信息:数据长度
    has_read_size = 0
    bytes_list = []
    while has_read_size < 4:
        chunk = conn.recv(4 - has_read_size)
        has_read_size += len(chunk)
        bytes_list.append(chunk)
    header = b"".join(bytes_list)
    data_length = struct.unpack('i', header)[0]

    # 获取数据
    data_list = []
    has_read_data_size = 0
    while has_read_data_size < data_length:
        size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
        chunk = conn.recv(size)
        data_list.append(chunk)
        has_read_data_size += len(chunk)

    data = b"".join(data_list)

    return data


def recv_file(conn, save_file_name, chunk_size=1024):
    save_file_path = os.path.join('files', save_file_name)
    # 获取头部信息:数据长度
    has_read_size = 0
    bytes_list = []
    while has_read_size < 4:
        chunk = conn.recv(4 - has_read_size)
        bytes_list.append(chunk)
        has_read_size += len(chunk)
    header = b"".join(bytes_list)
    data_length = struct.unpack('i', header)[0]

    # 获取数据
    file_object = open(save_file_path, mode='wb')
    has_read_data_size = 0
    while has_read_data_size < data_length:
        size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
        chunk = conn.recv(size)
        file_object.write(chunk)
        file_object.flush()
        has_read_data_size += len(chunk)
    file_object.close()


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # IP可复用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
        conn, addr = sock.accept()

        while True:
            # 获取消息类型
            message_type = recv_data(conn).decode('utf-8')
            if message_type == 'close':  # 四次挥手,空内容。
                print("关闭连接")
                break
            # 文件:{'msg_type':'file', 'file_name':"xxxx.xx" }
            # 消息:{'msg_type':'msg'}
            message_type_info = json.loads(message_type)
            if message_type_info['msg_type'] == 'msg':
                data = recv_data(conn)
                print("接收到消息:", data.decode('utf-8'))
            else:
                file_name = message_type_info['file_name']
                print("接收到文件,要保存到:", file_name)
                recv_file(conn, file_name)

        conn.close()
    sock.close()


if __name__ == '__main__':
    run()

客户端代码

import os
import json
import socket
import struct


def send_data(conn, content):
    data = content.encode('utf-8')
    header = struct.pack('i', len(data))
    conn.sendall(header)
    conn.sendall(data)


def send_file(conn, file_path):
    file_size = os.stat(file_path).st_size
    header = struct.pack('i', file_size)
    conn.sendall(header)

    has_send_size = 0
    file_object = open(file_path, mode='rb')
    while has_send_size < file_size:
        chunk = file_object.read(2048)
        conn.sendall(chunk)
        has_send_size += len(chunk)
    file_object.close()


def run():
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))

    while True:
        """
        请发送消息,格式为:
            - 消息:msg|你好呀
            - 文件:file|xxxx.png
        """
        content = input(">>>")  # msg or file
        if content.upper() == 'Q':
            send_data(client, "close")
            break
        input_text_list = content.split('|')
        if len(input_text_list) != 2:
            print("格式错误,请重新输入")
            continue

        message_type, info = input_text_list

        # 发消息
        if message_type == 'msg':

            # 发消息类型
            send_data(client, json.dumps({"msg_type": "msg"}))

            # 发内容
            send_data(client, info)

        # 发文件
        else:
            file_name = info.rsplit(os.sep, maxsplit=1)[-1]

            # 发消息类型
            send_data(client, json.dumps({"msg_type": "file", 'file_name': file_name}))

            # 发内容
            send_file(client, info)

    client.close()


if __name__ == '__main__':
    run()

OSI7层模型:

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

应用层:规定数据的格式。

表示层:对应用层数据的编码、压缩(解压缩)、分块、加密(解密)等任务。

会话层:负责与目标建立、中断连接。

传输层:建立端口到端口的通信,其实就确定双方的端口信息。

网络层:标记目标IP信息(IP协议层)

数据链路层:对数据进行分组并设置源和目标mac地址

物理层:将二进制数据在物理媒体上传输。

UDP、TCP协议

TCP三次握手和四次挥手

粘包

两台电脑在进行收发数据时,其实不是直接将数据传输给对方。

  • 对于发送者,执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。

  • 对于接受者,执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据。

所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。

如何解决粘包问题?

每次发送的消息时,都将消息划分为 头部(固定字节长度) 和 数据 两部分。例如:头部,用4个字节表示后面数据的长度。

  • 发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。

  • 接收数据,先读4个字节就可以知道自己这个数据包中的数据长度,再根据长度读取到数据。

对于头部需要一个数字并固定为4个字节,这个功能可以借助python的struct包来实现。

阻塞和非阻塞

默认情况下我们编写的网络编程的代码都是阻塞的(等待)。如果想要让代码变为非阻塞,需要这样写: sock.setblocking(False) # 加上就变为了非阻塞

如果代码变成了非阻塞,程序运行时一旦遇到 acceptrecvconnect 就会抛出 BlockingIOError 的异常。

这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。

非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。

I/O多路复用及作用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

监测多个 IO对象 是否发生变化(可读/可写)。

  • IO多路复用 + 非阻塞 + socket服务端,可以让服务端同时处理多个客户端的请求。

  • IO多路复用 + 非阻塞 + socket客户端,可以向服务端同时发起多个请求。

网络编程及socket模块

目标:掌握网络相关的基础知识并可以基于Python开发程序(基于网络进行数据传输)。

课程概要:

  • 网络必备基础

  • 网络编程(Python代码)

  • B/S和C/S架构

1.必备基础

你必须了解的网络相关设备和基础概念。

1.1 网络架构

假设 lbw 上了一个野鸡大学买了一台电脑,电脑里存了1部小电影,整宿整宿的在宿舍反复的看。

lbw 如何想要和室友 uzi 进行收发数据,可以通过一根网线来进行连接,并进行数据的传输。

 1.1.1 交换机

其他2位室友如何也想和他们的电脑相互连接然后进行资源的共享,此时就需要一个设备 【二层交换机】组件一个局域网。

当电脑接入交换机之后,我们需要为每台电脑分配一个IP,例如:
    - 电脑1:192.168.10.1
    - 电脑2:192.168.10.2
    - 电脑3:192.168.10.3
    - 电脑4:192.168.10.4
局域网内容个电脑之间是基于ARP协议来进行通信,例如:A电脑向 IP为192.168.10.3的另一个电脑发送消息。
​
第一步:A封装数据包,此时只知道目标IP不知道目标mac地址(未知mac地址时默认会设置为FF)。
第二步:将数据包发送到交换机,交换机通过广播的形式将数据发送给所有电脑。
第三步:目标电脑接收到数据包后,监测自己是否是目标IP。
        - 是,收到数据并回复。
        - 不是,则丢弃包。
​
为防止每次发送消息都是广播形式,每台电脑的内部都为维护了一个ARP表,接受到数据时(无论是否自己的)都会记录自己了解的IP和MAC的对应关系,例如:
    Internet地址                 物理地址
    192.168.10.1            14-9d-da-2a-dd-0a
    192.168.10.3            14-9d-da-2a-dd-0c
    ...
    
以便于下次在发送消息时,就知道了目标的mac地址,直接让交换机转发给指定的电脑(单播)。
​
同时,当有消息发送经过二层交换机时他的内容也会维护记录了交换机接口和连接的电脑的mac地址的对应关系,例如:
    接口(网卡)               mac地址
      接口1              14-9d-da-2a-dd-0A
      接口2              14-9d-da-2a-dd-0B
      接口3              14-9d-da-2a-dd-0C
      ...
这样一来,交换机在进行数据转发时,效率就更高了。
​
注意:每台电脑出厂时在网卡中都设置了唯一的mac地址(不重复),网卡集成在主板上,如果更换了主板则mac地址也会变更。

头部信息:xxx
数据:你好
头部信息:xxx
数据:收到

1.1.2 路由器

多个宿舍之间想想要组建一个相互可以通信网络,此时需要【二层交换机】和【企业路由器】配合组建稍微大一点的局域网(同时也可缓解广播风暴)。

划分好网络结构之后,其实会给各宿舍的电脑分配IP和网关,例如:
   宿舍A:
        - 电脑1:192.168.10.1  网关:192.168.10.254
        - 电脑2:192.168.10.2  网关:192.168.10.254
        - 电脑3:192.168.10.3  网关:192.168.10.254
        - 电脑4:192.168.10.4  网关:192.168.10.254
   宿舍B
        - 电脑1:192.168.20.1  网关:192.168.20.254
        - 电脑2:192.168.20.2  网关:192.168.20.254
        - 电脑3:192.168.20.3  网关:192.168.20.254
        - 电脑4:192.168.20.4  网关:192.168.20.254
​
然后再在路由器中配置路由表(包含网段和路由器上的接口的对应关系),例如:
     接口             IP
     eth0         192.168.10.254(192.168.10网段)
     eth1         192.168.20.254(192.168.20网段)
    
想与外部网络通信,需要配置网关,网关就是路由表中配置的指向此网段的IP。其实就类似于贸易出口都需要经过海关。
数据通信的过程结合了APR协议和IP协议,例如:宿舍A的电脑1向宿舍B的电脑3发送消息(目标IP:192.168.20.3)。
​
简化过程:
    - 宿舍A的电脑1,通过广播或单播将数据发送到网管(路由器)
    - 路由器接收到数据之后,再通过对应的接口把数据通过广播的形式发送到宿舍B。
注意:各自局域网内通过学习并记录相关mac地址后,就可以不再使用广播形式,而是使用单播来发送消息了。

1.1.3 三层交换机

三层交换机集成了 交换机 & 路由器的功能(大部分路由器功能),上述的三个设备其实可以用一个三层交换机就可以搞定。

按照下图,在三层交换机上分别做如下几件事:
​
1. 划分两个vlan,模拟出来路由器的两个接口。
2. 将交换机的接口划分给指定的vlan,例如:
    接口1、2、3划分给一个vlan,相当于交换机连接上了路由器。
    接口4、5、6划分给一个vlan,相当于交换机连接上了路由器。
3. 电脑连接上交换机。
4. 进行相应的配置。
    宿舍A(左边)电脑配置:
        - 电脑1:192.168.10.1  网关:192.168.10.254   对应交换机接口:1
        - 电脑2:192.168.10.2  网关:192.168.10.254   对应交换机接口:2
        - 电脑3:192.168.10.3  网关:192.168.10.254   对应交换机接口:3
    宿舍B(右边)电脑配置:
        - 电脑1:192.168.20.1  网关:192.168.20.254   对应交换机接口:4
        - 电脑2:192.168.20.2  网关:192.168.20.254   对应交换机接口:5
        - 电脑3:192.168.20.3  网关:192.168.20.254   对应交换机接口:6
    
    交换机中的路由配置:
          接口               IP
         左vlan        192.168.10.254(192.168.10网段)
         右vlan        192.168.20.254(192.168.20网段)
        
通过上述的配置之后,就可以实现宿舍A和宿舍B的网络通信了。

1.1.4 小型企业基础网络架构

1.1.5 家庭网络架构

家用路由器集成了是交换机和路由的功能(性能差、价格便宜)。

1.1.6 互联网

1.2 网络核心词汇

1.2.1 子网掩码和IP

之前说过,接入网络设备后,需要一个IP来代指次电脑,例如:192.168.10.1 。

IP其是一个32位的二进制,为了便于记忆就将它分为4组,每组8位,由小数点分开,例如:

二进制表示:00000000.10010111.11111111.00001111
十进制表示:251.151.255.15
​
0~255
192.178.11.211
192.178.11.311

在网络中的每台电脑都会有一个IP与之绑定,这样通过IP就可以找到相应的电脑。

一个IP地址可以划分为两个部分,即:网络地址 + 主机地址。

  • 问题1:如何确定网络地址和主机地址呢?

    通过子网掩码就可以确定IP的网络地址和主机地址。
    
    示例1:
        	IP:192.168.1.199      11000000.10101000.00000001.11000111
    	子网掩码:255.255.255.0     11111111.11111111.11111111.00000000
    此时,网络地址就是前24位 + 主机地址是后8位。你可能见过有些IP这样写 192.168.1.199/24,意思也是前24位是网络地址。
    
    
    示例2:
        	IP:192.168.99.254     11000000.10101000.01100011.11111110
    	子网掩码:255.255.240.0     11111111.11111111.11111100.00000000
    此时,网络地址就是前22位 + 主机地址是后10位。你可能见过有些IP这样写 192.168.99.254/22,意思也是前22位是网络地址。
  • 问题2:划分 网络地址 + 主机地址 的意义是什么?

    网络地址相同的IP,也称为属于同一个网段。
    在局域网内只有同一个网段的IP才能相互通信,不同网段IP想要通信需要借助路由的转发才能通信。
    
    当了解子网掩码之后,其实就可以确定某个网段可以容纳的主机个数,例如:
    【IP: 192.168.10.2  掩码:255.255.255.0】 和 【192.168.10.251 掩码:255.255.255.0】 数据同一个网段。
    
    	示例网段的主机范围:11000000.10101000.00001010. 00000001  ~  11000000.10101000.00001010.  11111110
    	                 --------------------------              --------------------------
    	                          网络地址                                   网络地址
    				           192.168.10.1                 ~           192.168.10.254
                               
    【IP: 192.168.8.1  掩码:255.255.240.0】 和 【192.168.11.254 掩码:255.255.240.0】 数据同一个网段。
    	子网掩码:255.255.240.0
    	示例网段的主机范围:11000000.10101000.000010 00.00000001  ~  11000000.10101000.000010 11.11111110
    	                 11111111.11111111.111111 00.00000000
    	                 ------------------------                 ------------------------
    	                          网络地址                                   网络地址
    				           192.168.8.1                 ~           192.168.11.254
    				           
    【IP: 192.168.96.1  掩码:255.255.240.0】 和 【192.168.99.254  掩码:255.255.240.0】 数据同一个网段。
    	示例网段的主机范围:11000000.10101000.011000 00.00000001  ~  11000000.10101000.011000 11.11111110
    	         
    	                 ------------------------                 ------------------------
    	                          网络地址                                   网络地址
    				           192.168.96.1                 ~           192.168.99.254    
    

1.2.2 DHCP

在一个局域网内想要给某台电脑分配IP有两种方式:

  • 手动设置,打开指定菜单栏在里面输入相应的IP信息。

  • 自动获取

    - 在电脑端,IP地址获取方式设置为自动。
    - 在路由器或三层交换机,开启DHCP服务,并设置IP地址池。(家用路由器上也是基于DHCP服务自动分配的IP)
    
    这样,电脑只要连接只该网络,DHCP服务就会为它自动分配IP、子网掩码、网关。

1.2.3 内网和公网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,基于内网IP可以在一个局域网内进行相互通信(也需要相关的配置)。

如果想要通过互联网进行通信,就必须借助公网IP。例如,右边家庭电脑想访问左边某公司服务器上的部署的网站:

  • 第一步:左边公司,去运营商申请公网的固定IP(办理专线宽带时运营商会分配至少1个固定的IP地址),其实运营商就是将你拉的这个专线和固定IP创建绑定关系。(假设公网IP:123.206.15.88)

  • 第二步:配置公网IP与指定服务器的转发规则。

  • 第二步:右边家庭,如果想要访问某个公司服务器上的网网站,只需要执行指定IP:123.206.15.88,运营商就会根据IP找到与之关联的公司专线,并通过公司路由器、防火墙等设备找到指定的服务器。

按理说,每个从运营商接入网的用户都可以有一个外网IP,但由于全球用户太多而IP根本就不够分配,所以,运营商网络会进行划分,让多个家庭宽带用户共用一个公网IP(动态,可能每次上网公网IP都不一样)。

让家庭用户想要通过网络访问访问其他IP时,先发给运营商由运营商向外转发到其他IP。

注意:外部用户想要访问家庭宽带的IP时,运营商不会把请求转发到我们的电脑。

所以,以后如果你想开发一个网站供全球的用户访问,那你就需要做以下几件事:

  • 拉专线,申请固定公网IP

  • 买一台服务器(就是性能好的电脑)

  • 公网IP绑定至此服务器

  • 将写好的代码放在服务器上并运行起来

这样就可以搞定了...

扩展:IPv4和IPv6

IPv4,长度为 32 位(4 个字节), 格式:A.B.C.D
IPv6,长度为 128 位(16 个字节),用":"分成8段,格式:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX(每个X是一个16进制数)。

1.2.4 云服务器

大家可能之前听说过:阿里云、腾讯云、亚马逊aws等之类的平台都在搞云服务器,那是个啥?

简单的说:他们造了一个机房(网吧),买了很多很多的服务器(高性能电脑),然后将他们放在机房,然后通电+通网,主要对外去租赁这些服务器资源,让用户不必再自己 拉专线+配置网络+买服务器。

假设,你想要在腾讯云租一台服务器,就可以根据自己的需求去选择配置,腾讯云会根据配置在他的物理机上虚拟出一个服务器,并进行相应的环境初始化并绑定公网固定IP,这样你就可以快速拥有一台可以被大家访问的服务器了。

注意:一台性能非常高的物理机虚拟出很多虚拟机,类似于你在自己电脑上通过vmware、parallel等搞出多个虚拟机。

1.2.5 端口

假设,你在腾讯租了一台云服务器(外网IP:123.206.15.88),然后又开发了 2 个网站运行在服务器上。

那么问题来了,用户在自己的电脑或手机上如何来分别访问同一台服务器上两个程序呢?

其实,在计算机中有一个 端口 的概念,每个程序想要通过网络进行通讯都必须要指定一个端口,例如:

  • 网站A:使用8001端口,那么用户在自己电脑上或手机上访问时指定 IP和端口 即可,如: 123.206.15.88:8001

  • 网站B:使用8002端口,那么用户在自己电脑上或手机上访问时指定 IP和端口 即可,如: 123.206.15.88:8002

注意:端口的取值范围:0 ~ 65535,很多端口在计算机的内部已被使用,我们平时自定义时尽量选择5000之后的端口。

示例:访问百度

提示:如果在浏览器上只写IP不写端口,则默认是80端口。

1.2.6 域名

假设你创业开发了一个网站,用户很难记住你的公网IP:123.206.15.88:80 `123.206.15.88

所以,域名就诞生了,让域名和IP创建对应关系,用户只需要记住域名就可以了,例如:

www.baidu.com   -->  110.242.68.3
www.taobao.com  --> 121.18.239.232
...

注意:域名只是和IP创建了对应关系,与端口无关 www.baidu.com:80

在用户在自己的电脑或手机上输入域名去访问时,其实要执行两个步骤:

  • 根据域名寻找IP。(寻找IP)

  • 获得IP之后,再通过IP再去访问指定服务器。

在电脑上属如域名后,寻找IP的过程如下:

  • 第一步:在自己电脑的DNS缓存记录中寻找 域名对应的IP,如果未命中,则执行下一步。

  • 第二步:在自己电脑的hosts文件中寻找,如果未命中,则执行下一步。

    - mac系统:/etc/hosts 文件中
    - win系统:C:\Windows\System32\drivers\etc\hosts 文件中
    # 内容示例
    127.0.0.1	localhost
    255.255.255.255	broadcasthost
    127.0.0.1 kubernetes.docker.internal
    192.168.1.55 www.pythonav.com
  • 第三步:在自己电脑上找到DNS配置的地址(本地域名服务器),去这个地址寻找域名对应的IP,如果未命中,则执行下一步。

  • 常见的DNS服务器地址:
    	114.114.114.114(114 DNS)
        223.5.5.5(阿里 AliDNS)
        8.8.8.8(Google DNS,随着Google在中国的没落和国内官方的限制,已经不是太好用了)
        ...
        各大运营商也有相应的DNS服务器...
        
    如果你选择的是自动获得DNS,那么就会使用本地运营商的DNS服务器了。
  • 第四步:去根域名服务器中询问(全球共13台根域名服务器,距离中国最近的一台是在日本)

问题来了

了解域名是怎么回事之后?现在你如果想要让自己的网站通过域名来访问,应该怎么办呢?【目前了解即可】

  • 租一个域名

    ICANN,域名的总管理者(美国一个非营利机构),它仅制定域名政策,注册业务它会授权给一些顶级注册商。
    顶级注册商,可以对外销售域名,但要受国家 互联网络信息中心的管理。例如:中国万网(阿里云收购),中国新网,新网互联,商务中国,中国频道等。
    代理注册商,顶级注册上可以再招一些代理帮助他们卖域名。

  • 备案

    现在国内注册域名后,需要进行备案(提交一些网站、个人或企业 等信息)后才能使用。
    注册成功后,可按照引导备案:https://beian.aliyun.com/
    
    注意:国外的域名无需备案就能使用。
  • 域名解析

    让域名和IP创建关联关系,并将关系同步到相关:本地域名服务器 和 根域名服务器(含顶级和二级域名服务器)。

     

2. 网络编程 socket模块

Python中内置了一个socket模块,可以快速实现网络之间进行传输数据。例如:

  • 服务端,放在左边云服务器中(有固定IP)

    import socket
    
    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('123.206.15.88', 8001)) # IP,端口
    sock.listen(5) # 支持排队等待5人
    
    while True:
        # 2.等待,有人来连接(阻塞)
        conn, addr = sock.accept() # 等待客户端来连接(阻塞)
    
        # 3.等待,连接者发送消息(阻塞)
        client_data = conn.recv(1024) # 等待接收客户端发来数据
        print(client_data.decode('utf-8')) # 字节
    
        # 4.给连接者回复消息
        conn.sendall("hello world".encode('utf-8'))
    
        # 5.关闭连接
        conn.close()
    
    # 6.停止服务端程序
    sock.close()
  • 客户端,放在右边用户电脑上

    import socket
    
    # 1. 向指定IP发送连接请求
    client = socket.socket()
    client.connect(('123.206.15.88', 8001)) # 向服务端发起连接(阻塞)10s
    
    # 2. 连接成功之后,发送消息
    client.sendall('hello'.encode('utf-8'))
    
    # 3. 等待,消息的回复(阻塞)
    reply = client.recv(1024)
    print(reply)
    
    # 4. 关闭连接
    client.close()

上述示例需要借助于互联网,你至少需要租一台云服务器才能通信。

为了节省学习成本,大家可以在自己电脑上模拟【服务端】和【客户端】,等以后项目开发完毕后,再租服务器并部署到服务器上。

注意:在自己本地运行上述代码时,要监听和连接时的IP地址。

当然,你也可以把在自己的局域网内找两台电脑,A作为服务端,B作为客户端,这样两者也可以通信。

服务端的代码需修改:监听的IP修改为A的IP地址。
客户端的代码需修改:连接的IP修改为A的IP地址(客户端要去找到服务端,并与服务端创建连接)。

注意事项:

  • 本机:

    服务端IP:127.0.0.1  / 192.168.28.92(局域网IP)
  • 局域网:

    服务端IP:192.168.28.92(局域网IP)    
  • 互联网

    服务端IP:123.206.15.88(外网IP)

案例:智障客服

  • 服务端

    import socket
    
    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))  # 127.0.0.1 或 查看自己局域网本地IP地址
    sock.listen(5)
    
    while True:
        # 2.等待,有人来连接(阻塞)
        conn, addr = sock.accept()
        print("有人来连接了...")
    
        # 3.连接成功后立即发送
        conn.sendall("欢迎使用xx系统,请输入您想要办理的业务!".encode("utf-8"))
    
        while True:
            # 3.等待接受信息
            data = conn.recv(1024)
            if not data:
                break
            data_string = data.decode("utf-8")
    
            # 4.回复消息
            conn.sendall("你说啥?".encode("utf-8"))
        print("断开连接了")
        # 5.关闭与此人的连接
        conn.close()
    
    # 6.停止服务端程序
    sock.close()

  • 客户端

    import socket
    
    # 1. 向指定IP发送连接请求
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8001))
    
    # 2.连接成功后,获取系统登录信息
    message = client.recv(1024)
    print(message.decode("utf-8"))
    
    while True:
        content = input("请输入(q/Q退出):")
        if content.upper() == 'Q':
            break
        client.sendall(content.encode("utf-8"))
    
        # 3. 等待,消息的回复
        reply = client.recv(1024)
        print(reply.decode("utf-8"))
    
    # 关闭连接,关闭连接时会向服务端发送空数据。
    client.close()

案例:文件上传

  • 服务端

    import socket
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))  # 127.0.0.1 或 查看自己局域网本地IP地址
    sock.listen(5)
    
    conn, addr = sock.accept()
    
    # 接收文件大小
    data = conn.recv(1024)
    total_file_size = int(data.decode('utf-8'))
    
    # 接收文件内容
    file_object = open('xxx.png', mode='wb')
    recv_size = 0
    while True:
        # 每次最多接收1024字节
        data = conn.recv(1024)
        file_object.write(data)
        file_object.flush()
    
        recv_size += len(data)
        # 上传完成
        if recv_size == total_file_size:
            break
    
    # 接收完毕,关闭连接
    conn.close()
    sock.close()

  • 客户端

    import time
    import os
    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
    
    file_path = input("请输入要上传的文件:")
    
    # 先发送文件大小
    file_size = os.stat(file_path).st_size
    client.sendall(str(file_size).encode('utf-8'))
    
    print("准备...")
    time.sleep(2)
    print("开始上传..")
    file_object = open(file_path, mode='rb')
    read_size = 0
    while True:
        chunk = file_object.read(1024) # 每次读取1024字节
        client.sendall(chunk)
        read_size += len(chunk)
        if read_size == file_size:
            break
    
    client.close()

案例:用户登录认证

  • 服务端
import socket
import json

# 1.监听本机的IP和端口
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))  # 127.0.0.1 或 查看自己局域网本地IP地址
sock.listen(5)

while True:
    # 2.等待,有人来连接(阻塞)
    conn, addr = sock.accept()

    # 3.连接成功后立即发送
    conn.sendall("欢迎使用xx系统".encode("utf-8"))

    while True:
        # 3.等待接受信息
        data = conn.recv(1024)
        if not data:
            break
        data_string = data.decode("utf-8")
        username, password = data_string.split('|')


        file_object = open("db.csv", mode='r', encoding='utf-8')

        is_success = False
        for line in file_object:
            user, pwd = line.strip().split(",")
            if user == username and pwd == password:
                is_success = True

        file_object.close()

        if is_success:
            info = {"status": True, 'msg': "登录成功"}
            conn.sendall(json.dumps(info).encode("utf-8"))
            break
        else:
            info = {"status": False, 'msg': "登录失败"}
            conn.sendall(json.dumps(info).encode("utf-8"))
    # 5.关闭与此人的连接
    conn.close()

# 6.停止服务端程序
sock.close()
  • 客户端

import socket
import json

# 1. 向指定IP发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8001))

# 2.连接成功后,获取系统登录信息
message = client.recv(1024)
print(message.decode("utf-8"))

while True:
    user = input("请输入用户名:")
    pwd = input("请输入密码:")
    content = "{}|{}".format(user, pwd)
    client.sendall(content.encode("utf-8"))

    reply = client.recv(1024)
    info = json.loads(reply.decode("utf-8"))
    if info['status']:
        print(info['msg'])  # 登录成功
        break
    else:
        print(info['msg'])  # 登录失败
# 关闭连接
client.close()

3. B/S和C/S架构

平时在开发或与人沟通时,经常会有人提到b/s和c/s架构,他们是啥意思呢?

  • C/S架构,是Client和Server的简称。开发这种架构的程序意味着你即需要开发客户端也需要开发服务端。

    例如:你电脑的上QQ、百度网盘、钉钉、QQ音乐 等安装在电脑上的软件。
    
    服务端:互联网公司会开发一个程序放在他们的服务器上,用于给客户端提供数据支持。
    客户端:大家在电脑安装的相关程序,内部会连接服务端进行收发数据并提供 交互和展示的功能。
  • B/S架构,是Browser和Server的简称。开发这种架构的程序意味着你开发服务端即可,客户端用用户电脑上的浏览器来代替。

    例如:淘宝、京东等网站。
    
    服务端:互联网公司开发一个网站,放在他们的服务器上。
    客户端:不需要开发,用现成的浏览器即可。

简而言之,B/S架构就是开发网站;C/S架构就是开发安装在电脑的软件。

小总结

  1. 了解常见设备和网络架构。

  2. 掌握常见网络词汇的意思。

  3. 了解B/S和C/S架构的区别。

  4. 基于Python的socket模块实现网络编程。

# 1. 简述 二层交换机 & 路由器 & 三层交换机 的作用。
"""
二层交换机:构建局域网并实现局域网内数据的转发。
路由器:实现跨局域网进行通信。
三层交换机:具备二层交换机和路由的功能。
"""

# 2. 简述常见词:IP、子网掩码、DHCP、公网IP、端口、域名的作用。
"""
IP,本质上是一个32位的二进制,通过 . 等分为了4组8位二进制。
子网掩码,用于指定IP的网络地址和主机地址。
DHCP,网络设备中的一个服务,用于给接入当前网络的电脑自动设置 IP、子网掩码、网关。
公网IP,一般企业拉专线时会给固定的公网IP,只有具备公网IP才可以被互联网上的其他电脑访问。
端口,IP用于表示某台电脑,端口则表示此电脑上的具体程序。0-65535
域名,与IP构造对应关系,方便用户记忆。
"""

1. OSI 7层模型

 

OSI的7层模型对于大家来说可能不太好理解,所以我们通过一个案例来讲解:

假设,你在浏览器上输入了一些关键字,内部通过DNS找到对应的IP后,再发送数据时内部会做如下的事:

  • 应用层:规定数据的格式。

    "GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n"
  • 表示层:对应用层数据的编码、压缩(解压缩)、分块、加密(解密)等任务。

    "GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
  • 会话层:负责与目标建立、中断连接。

    在发送数据之前,需要会先发送 “连接” 的请求,与远程建立连接后,再发送数据。当然,发送完毕之后,也涉及中断连接的操作。
  • 传输层:建立端口到端口的通信,其实就确定双方的端口信息。

    数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
        - 目标:80
        - 本地:6784
  • 网络层:标记目标IP信息(IP协议层)

    数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
        - 目标:80
        - 本地:6784
    IP:
        - 目标IP:110.242.68.3(百度)
        - 本地IP:192.168.10.1
  • 数据链路层:对数据进行分组并设置源和目标mac地址

    数据:"POST /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
        - 目标:80
        - 本地:6784
    IP:
        - 目标IP:110.242.68.3(百度)
        - 本地IP:192.168.10.1
    MAC:
        - 目标MAC:FF-FF-FF-FF-FF-FF 
        - 本机MAC:11-9d-d8-1a-dd-cd
  • 物理层:将二进制数据在物理媒体上传输。

    通过网线将二进制数据发送出去

每一层各司其职,最终保证数据呈现在到用户手中。

简单的可以理解为发快递:将数据外面套了7个箱子,最终用户收到箱子时需要打开7个箱子才能拿到数据。而在运输的过程中有些箱子是会被拆开并替换的,例如:

最终运送目标:上海 ~ 北京(中途可能需要中转站),在中转站会会打开箱子查看信息,在进行转发。
    - 对于二级中转站(二层交换机):拆开数据链路层的箱子,查看mac地址信息。
    - 对于三级中转站(路由器或三层交换机):拆开网络层的箱子,查看IP信息。

在开发过程中其实只能体现:应用层、表示层、会话层、传输层,其他层的处理都是在网络设备中自动完成的。

import socket
​
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('110.242.68.3', 80)) 
'''
向服务端发送了数据包 
另一边接收到解包同意建立连接并回复一个同意包  
最后客户端向服务端发一个 收到 包,服务端收到确认后,链接建立成功。详情见TCP三次握手
​'''

key = "你好"
# 应用层
content = "GET /s?wd={} http1.1\r\nHost:www.baidu.com\r\n\r\n".format(key)
# 表示层
content = content.encode("utf-8")
​
client.sendall(content)#在内部指定了端口ip mac地址等将数据发送过去 另一边接收到解包 再封包回复
result = client.recv(8196) #解包拆数据
print(result.decode('utf-8'))
​
# 会话层 & 传输层
client.close() #发送数据包 表示关闭连接
'''
客户端——>服务端  我要关闭
服务端——>客户端  好的等一下
服务端——>客户端  ok可以关闭了
客户端——>服务端  收到
'''

2. UDP和TCP协议

协议,其实就是规定 连接、收发数据的一些规定。

在OSI的 传输层 除了定义端口信息以外,常见的还可以指定UDP或TCP的协议,协议不同连接和传输数据的细节也会不同。

  • UDP(User Data Protocol)用户数据报协议, 是⼀个⽆连接的简单的⾯向数据报的传输层协议。 UDP不提供可靠性, 它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。 由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快。

    常见的有:语音通话、视频通话、实时游戏画面 等。 
    有时语音通话断断续续,便是因为网络问题导致数据丢包了。
  • TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。

    常见有:网站、手机APP数据获取等。 
    就算服务端没有收到消息,也会显示发送失败可以重新发送。

2.1 UDP和TCP 示例代码

UDP示例如下:

  • 服务端

    import socket
    ​
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)#UDP和TCP最后一个参数不同
    server.bind(('127.0.0.1', 8002))
    ​
    while True:
        data, (host, port) = server.recvfrom(1024) # 阻塞
        print(data, host, port)
        server.sendto("好的".encode('utf-8'), (host, port))

  • 客户端

    import socket
    ​
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while True:
        text = input("请输入要发送的内容:")
        if text.upper() == 'Q':
            break
        client.sendto(text.encode('utf-8'), ('127.0.0.1', 8002))
        data, (host, port) = client.recvfrom(1024)
        print(data.decode('utf-8'))
    ​
    client.close()

TCP示例如下:

  • 服务端

    import socket
    ​
    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    ​
    while True:
        # 2.等待,有人来连接(阻塞)
        conn, addr = sock.accept()
    ​
        # 3.等待,连接者发送消息(阻塞)
        client_data = conn.recv(1024)
        print(client_data)
    ​
        # 4.给连接者回复消息
        conn.sendall(b"hello world")
    ​
        # 5.关闭连接
        conn.close()
    ​
    # 6.停止服务端程序
    sock.close()

2.2 TCP三次握手和四次挥手

这是一个常见的面试题。

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |传输层添加4字节
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |传输层添加4字节
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |传输层添加4字节
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |传输层添加4字节
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |传输层添加4字节
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |传输层添加4字节
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |上一层数据
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

网络中的双方想要基于TCP连接进行通信,必须要经过:

  • 创建连接,客户端和服务端要进行三次握手。

    # 服务端
    import socket
    ​
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    ​
    while True:
        conn, addr = sock.accept() # 等待客户端连接
        ...
    # 客户端
    import socket
    client = socket.socket()
    client.connect(('127.0.0.1', 8001)) # 发起连接
          客户端                                                服务端
    ​
      1.  SYN-SENT    --> <seq=100><CTL=SYN>               --> SYN-RECEIVED
    ​
      2.  ESTABLISHED <-- <seq=300><ack=101><CTL=SYN,ACK>  <-- SYN-RECEIVED
    ​
      3.  ESTABLISHED --> <seq=101><ack=301><CTL=ACK>       --> ESTABLISHED
    ​
          
    At this point, both the client and server have received an acknowledgment of the connection. The steps 1, 2 establish the connection parameter (sequence number) for one direction and it is acknowledged. The steps 2, 3 establish the connection parameter (sequence number) for the other direction and it is acknowledged. With these, a full-duplex communication is established.

'''
向服务端发送了数据包 
另一边接收到解包同意建立连接并回复一个同意包  
最后客户端向服务端发一个 收到 包,服务端收到确认后,链接建立成功。详情见TCP三次握手
​'''

  • 传输数据

    在收发数据的过程中,只有有数据的传送就会有应答(ack),如果没有ack,那么内部会尝试重复发送。
  • 关闭连接,客户端和服务端要进行4次挥手。

    import socket
    ​
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
        conn, addr = sock.accept()
        ...
        conn.close() # 关闭连接
    sock.close()
    import socket
    ​
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
    ...
    client.close() # 关闭连接
           TCP A                                                TCP B
    ​
      1.  FIN-WAIT-1  --> <seq=100><ack=300><CTL=FIN,ACK>  --> CLOSE-WAIT
    ​
      2.  FIN-WAIT-2  <-- <seq=300><ack=101><CTL=ACK>      <-- CLOSE-WAIT
    ​
      3.  TIME-WAIT   <-- <seq=300><ack=101><CTL=FIN,ACK>  <-- LAST-ACK
    ​
      4.  TIME-WAIT   --> <seq=101><ack=301><CTL=ACK>      --> CLOSED

'''
客户端——>服务端  我要关闭
服务端——>客户端  好的等一下
服务端——>客户端  ok可以关闭了
客户端——>服务端  收到
'''

3. 粘包

 

两台电脑在进行收发数据时,其实不是直接将数据传输给对方。

  • 对于发送者,执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。

  • 对于接受者,执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据。

所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。例如:

# socket客户端(发送者)
import socket

client = socket.socket()
client.connect(('127.0.0.1', 8001))

#client.sendall('lbw正在吃'.encode('utf-8')) 缓冲区没地方的时候可能指发送一部分 发不全

client.sendall('lbw正在吃'.encode('utf-8')) #源源不断循环 直到写完为止
client.sendall('翔'.encode('utf-8'))

client.close()
# socket服务端(接收者)
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
conn, addr = sock.accept()

client_data = conn.recv(1024)#最多1024个字节
print(client_data.decode('utf-8'))

conn.close()
sock.close()

如何解决粘包的问题?

每次发送的消息时,都将消息划分为 头部(固定字节长度) 和 数据 两部分。例如:头部,用4个字节表示后面数据的长度。

  • 发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。

  • 接收数据,先读4个字节就可以知道自己这个数据包中的数据长度,再根据长度读取到数据。

对于头部需要一个数字并固定为4个字节,这个功能可以借助python的struct包来实现:

import struct

# ########### 数值转换为固定4个字节,四个字节的范围 -2147483648 <= number <= 2147483647  ###########
v1 = struct.pack('i', 199)
print(v1)  # b'\xc7\x00\x00\x00'

for item in v1:
    print(item, bin(item))

# ########### 4个字节转换为数字 ###########
v2 = struct.unpack('i', v1) # v1= b'\xc7\x00\x00\x00'
print(v2) # (199,)

示例代码:

  •  服务端
import socket
import struct

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
conn, addr = sock.accept()

# 固定读取4字节
header1 = conn.recv(4)
data_length1 = struct.unpack('i', header1)[0] # 数据字节长度 21
has_recv_len = 0
data1 = b""
#这个循环暂时先不优化
while True:
    length = data_length1 - has_recv_len
    if length > 1024:
        lth = 1024
	else:
        lth = length
	chunk = conn.recv(lth) # 可能一次收不完,自己可以计算长度再次使用recv收取,直到收完为止。 (默认最多一次接收1024*8 = 8192)
    data1 += chunk
    has_recv_len += len(chunk)
    if has_recv_len == data_length1:
        break
print(data1.decode('utf-8'))

# 写法同上
header2 = conn.recv(4)
data_length2 = struct.unpack('i', header2)[0] # 数据字节长度
data2 = conn.recv(data_length2) # 长度
print(data2.decode('utf-8'))

conn.close()
sock.close()
  • 客户端
import socket
import struct

client = socket.socket()
client.connect(('127.0.0.1', 8001))

# 第一条数据
data1 = 'lbw正在吃'.encode('utf-8')

header1 = struct.pack('i', len(data1))

client.sendall(header1)
client.sendall(data1)

# 第二条数据
data2 = '翔'.encode('utf-8')
header2 = struct.pack('i', len(data2))
client.sendall(header2)
client.sendall(data2)

client.close()

案例:消息 & 文件上传

  • 服务端
import os
import json
import socket
import struct


def recv_data(conn, chunk_size=1024):
    # 获取头部信息:数据长度
    has_read_size = 0
    bytes_list = []
    while has_read_size < 4:
        chunk = conn.recv(4 - has_read_size)
        has_read_size += len(chunk)
        bytes_list.append(chunk)
    header = b"".join(bytes_list)
    data_length = struct.unpack('i', header)[0]

    # 获取数据
    data_list = []
    has_read_data_size = 0
    while has_read_data_size < data_length:
        size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
        chunk = conn.recv(size)
        data_list.append(chunk)
        has_read_data_size += len(chunk)

    data = b"".join(data_list)

    return data


def recv_file(conn, save_file_name, chunk_size=1024):
    save_file_path = os.path.join('files', save_file_name)
    # 获取头部信息:数据长度
    has_read_size = 0
    bytes_list = []
    while has_read_size < 4:
        chunk = conn.recv(4 - has_read_size)
        bytes_list.append(chunk)
        has_read_size += len(chunk)
    header = b"".join(bytes_list)
    data_length = struct.unpack('i', header)[0]

    # 获取数据
    file_object = open(save_file_path, mode='wb')
    has_read_data_size = 0
    while has_read_data_size < data_length:
        size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
        chunk = conn.recv(size)
        file_object.write(chunk)
        file_object.flush()
        has_read_data_size += len(chunk)
    file_object.close()


def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # IP可复用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
        conn, addr = sock.accept()

        while True:
            # 获取消息类型
            message_type = recv_data(conn).decode('utf-8')
            if message_type == 'close':  
                print("关闭连接")
                break
            # 文件:{'msg_type':'file', 'file_name':"xxxx.xx" }
            # 消息:{'msg_type':'msg'}
            message_type_info = json.loads(message_type)
            if message_type_info['msg_type'] == 'msg':
                data = recv_data(conn)
                print("接收到消息:", data.decode('utf-8'))
            else:
                file_name = message_type_info['file_name']
                print("接收到文件,要保存到:", file_name)
                recv_file(conn, file_name)

        conn.close()
    sock.close()


if __name__ == '__main__':
    run()
  • 客户端
import os
import json
import socket
import struct


def send_data(conn, content):
    data = content.encode('utf-8')
    header = struct.pack('i', len(data))
    conn.sendall(header)
    conn.sendall(data)


def send_file(conn, file_path):
    file_size = os.stat(file_path).st_size
    header = struct.pack('i', file_size)
    conn.sendall(header)

    has_send_size = 0
    file_object = open(file_path, mode='rb')
    while has_send_size < file_size:
        chunk = file_object.read(2048)
        conn.sendall(chunk)
        has_send_size += len(chunk)
    file_object.close()


def run():
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))

    while True:
        """
        请发送消息,格式为:
            - 消息:msg|你好呀
            - 文件:file|xxxx.png
        """
        content = input(">>>")  # msg or file
        if content.upper() == 'Q':
            send_data(client, "close")
            break
        input_text_list = content.split('|')
        if len(input_text_list) != 2:
            print("格式错误,请重新输入")
            continue

        message_type, info = input_text_list

        # 发消息
        if message_type == 'msg':

            # 发消息类型
            send_data(client, json.dumps({"msg_type": "msg"}))

            # 发内容
            send_data(client, info)

        # 发文件
        else:
            file_name = info.rsplit(os.sep, maxsplit=1)[-1]

            # 发消息类型
            send_data(client, json.dumps({"msg_type": "file", 'file_name': file_name}))

            # 发内容
            send_file(client, info)

    client.close()


if __name__ == '__main__':
    run()

4. 阻塞和非阻塞

默认情况下我们编写的网络编程的代码都是阻塞的(等待),阻塞主要体现在:

# ################### socket服务端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 阻塞
conn, addr = sock.accept()

# 阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()


# ################### socket客户端(发送者) ###################
import socket

client = socket.socket()

# 阻塞
client.connect(('127.0.0.1', 8001))
#client.recv(1024) 也阻塞
client.sendall('lbw正在吃翔'.encode('utf-8'))

client.close()

如果想要让代码变为非阻塞,需要这样写:

# ################### socket服务端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.setblocking(False) # 加上就变为了非阻塞

sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 非阻塞
conn, addr = sock.accept()

# 非阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()

# ################### socket客户端(发送者) ###################
import socket

client = socket.socket()

client.setblocking(False) # 加上就变为了非阻塞

# 非阻塞
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃翔'.encode('utf-8'))

client.close()

如果代码变成了非阻塞,程序运行时一旦遇到 acceptrecvconnect 就会抛出 BlockingIOError 的异常。

这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。(可以try except)

非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。

5. IO多路复用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求,例如:

# ################### socket服务端 ###################
import select
import socket
​
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)  # 加上就变为了非阻塞
server.bind(('127.0.0.1', 8001))
server.listen(5)
​
inputs = [server, ] # socket对象列表 -> [server, 第一个客户端连接conn ]
​
while True:
    # 当 参数1 序列中的socket对象发生可读时(accetp和read),则获取发生变化的对象并添加到 r列表中。
    # r = [] 此时没有发起连接的用户
    # r = [server,] 有了 向下执行 加入到列表中
    # r = [] 此时没有发起连接的用户 也没有已连接的发了东西的用户
    # r = [第一个客户端连接conn,] 此时第一个客户发了消息
    # r = [server,]
    # r = [第一个客户端连接conn,第二个客户端连接conn]
    # r = [第二个客户端连接conn,]
    r, w, e = select.select(inputs, [], [], 0.05)
    for sock in r:
        # server
        if sock == server:
            conn, addr = sock.accept() # 接收新连接。
            print("有新连接")
            # conn.sendall()
            # conn.recv("xx")
            inputs.append(conn)
        else:
            data = sock.recv(1024)
            if data:
                print("收到消息:", data)
            else:
                print("关闭连接")
                inputs.remove(sock)
    # 干点其他事 20s
"""
优点:
    1. 干点那其他的事。
    2. 让服务端支持多个客户端同时来连接。
"""
# ################### socket客户端 ###################
import socket
​
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
​
while True:
    content = input(">>>")
    if content.upper() == 'Q':
        break
    client.sendall(content.encode('utf-8'))
​
client.close()
# ################### socket客户端 ###################
import socket
​
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
​
​
while True:
    content = input(">>>")
    if content.upper() == 'Q':
        break
    client.sendall(content.encode('utf-8'))
​
client.close() # 与服务端断开连接(四次挥手),默认会想服务端发送空数据。

IO多路复用 + 非阻塞,可以实现让TCP的客户端同时发送多个请求,例如:去某个网站发送下载图片的请求。

import socket
import select
import uuid
import os
​
client_list = []  # socket对象列表
​
for i in range(5):
    client = socket.socket()
    client.setblocking(False)
​
    try:
        # 连接百度,虽然有异常BlockingIOError,但向还是正常发送连接的请求
        client.connect(('47.98.134.86', 80))
    except BlockingIOError as e:
        pass
​
    client_list.append(client)
​
recv_list = []  # 放已连接成功,且已经把下载图片的请求发过去的socket
while True:
    # w = [第一个socket对象,]
    # r = [socket对象,]
    r, w, e = select.select(recv_list, client_list, [], 0.1)
    for sock in w:
        # 连接成功,发送数据
        # 下载图片的请求
        sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
        recv_list.append(sock)
        client_list.remove(sock)
​
    for sock in r:
        # 数据发送成功后,接收的返回值(图片)并写入到本地文件中
        data = sock.recv(8196)
        content = data.split(b'\r\n\r\n')[-1]
        random_file_name = "{}.png".format(str(uuid.uuid4()))
        with open(os.path.join("images", random_file_name), mode='wb') as f:
            f.write(content)
        recv_list.remove(sock)
​
    if not recv_list and not client_list:
        break

"""
优点:
    1. 可以伪造除并发的现象。
"""
 
 

基于 IO多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中

  • IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。

  • 非阻塞,socket的connect、recv过程不再等待。

注意:IO多路复用只能用来监听 IO对象 是否发生变化,常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。

在Linux操作系统化中 IO多路复用 有三种模式,分别是:select,poll,epoll。(windows 只支持select模式)

监测socket对象是否新连接到来 or 新数据到来。

select
 
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
 
poll
 
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
 
epoll
 
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

补充:socket + 非阻塞+ IO多路复用(IO操作对象都可以监测 + 文件)。

猜你喜欢

转载自blog.csdn.net/suic009/article/details/120819978