二十一、网络编程二

一、OSI七层模型

在这里插入图片描述

  • 应用层:对顶数据的格式;

  • 表示层:对因数层数据的编程、压缩(解压)、分块、加密(解密)、等任务;

  • 会话层: 负责建立目标、中断连接;

    在发送数据之前, 需要先发送"连接"的请求, 与远程建立连接后, 在发送数据, 当然发送完毕之后, 也涉及终端连接的操作。

  • 传输层:建立端口到端口的通信,其实就是去欸的那个双方的端口信息

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

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

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

二、UDP和TCP协议

UDP示例如下:

  • 服务端

    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind(('127.0.0.1', 9000))
    
    while True:
        data,(host, prot) = server.recvfrom(1024)
        print(data, host, port)
        server.sendto('好的'.encode('utf-8'), (host, port))
    
  • 客户端

    import socket
    
    client = socket.socket(socket.AF_INET, socket.SCOK_DGRAM)
    
    while True:
        text = inpurt('请输入要发送的内容')
        if text.upper() == 'Q':
            break
        client.sendto(text.encode('utf-8'), ('127.0.0.1', 9001))
    
    
    client.close()
    

TCP协议示例:

  • 服务端

    import socket
    
    sock = socket.socket(socket.AF_INET,SOCK_STRAM)
    sock.bind(('127.0.0.1', 9000))
    sock.listen(5)
    
    while True:
        conn,addr = sock.accept()
        client_data = conn.recv(1024)
        print(client_data)
        conn.sendall(b'hello world')
        conn.close()
    
    sock.close()
    
  • 客服端

    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1', 9000))
    
    client.sendall(b'hello')
    
    reply = client.recv(1024)
    print(reply)
    client.close()
    
  • TCP与UDP协议的区别

在这里插入图片描述

三、TCP三次握手与四次挥手

  • 三次握手

    第一次握手: 客户端发送一个TCP的SYN标志位置1的包致命客服端打算连接服务器的端口, 以及初始化序号X, 保存在报头的序号字段里.

    第二次握手: 服务器发回确认包(ACK)应答, 即SYN表示为何ACK标志为均为1同时, 将确认序号设置为客户端ISN加1, 即为X+1

    第三次握手: 客服端再次发送确认包(ACK)SYN标志位为0, ACK标志位为1, 并且把服务器发来的ACK的序号字段+1, 放在确定字段中发送给对方并且在数据端放些ISN的+1

    PS:三次握手是建立一个TCP连接, 需要客户端和服务端总共发送三个包

  • 四次挥手

    第一次挥手: 客户端发送报文, 报文中指定一个序列表, 此时客户护短处于FIN_WAIT状态

    第二次挥手: 服务端收到FIN之后, 会发送ACK报文, 并且客户端的序列表值为+1, 作为ACK报文的序列号至, 表明已经收到客户端报文, 此时服务端处于CLOSE_WAIT状态

    第三次挥手: 如果服务端也想端口连接了和客服端的第一次挥手一样, 发给FIN报文, 且指定一个序列号, 次数服务端出去LAST_ACK的状态

    第四次挥手: 客户端收到FIN之后, 一样发送一个ACK报文作为应答, 且把服务端的序列号值+1作为自己的ACK 报文的序列号, 此时可断的出去TIME_WAIT状态, 需要一阵子来确保服务端收到到自己的ACK报文之后才会进行CLOSE状态

四、粘包

  • 什么是粘包

    对于发送者: 执行send/sendall发送消息时, 是将数据线发送到自己的网络的缓冲去, 再由缓冲区将数据发送给对象网卡的读缓冲去

    对于接收者: 执行recv接收消息时, 是从自己网卡读缓冲去获取数据

    所以, 如果发送者连续快速发送了两条信息, 接收者在读取时会认为是一条信息, 这样两个包就会粘在一起

    • 示例:

      # 客户端
      import socket
      
      client = socket.socket()
      client.connect(('127.0.0.1', 9000))
      
      client.sendall('正在吃'.encode('utf-8'))
      client.sendall('翔'.encode('utf-8'))
      
      client.close()
      
      # 服务端
      import socket
      
      sock = socket.socket(socket.AF_INET, socket_SOCK_STREAM)
      sock.bind(('127.0.0.1', 9000))
      sock.listen(3)
      
      conn, addr = sock.accept()
      
      client_data = conn.recv(1024)
      print(client_data.decode('utf-8'))
      
      conn.close()
      sock.close()
      
如何解决粘包的问题

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

  • 发送数据, 先发送数据的长度, 在发送数据(或拼接起来在发送)
  • 接收数据, 先读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 = 8196
    data1 += chunk
    has_recv_len += len(chunk)
    if has_recv_len == data_length1:
        break
print(data1.decode('utf-8'))

# 固定读取4字节
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 struct

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

# 第一条数据
data1 = 'alex正在吃'.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()

五、阻塞与非阻塞

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

# ################### 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.sendall('alex正在吃翔'.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请求抛出的固定错误。
非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。

六、IO多路复用

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

# ################### 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 = [第一个客户端连接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多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中

  • IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。
  • 非阻塞,socket的connect、recv过程不再等待。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值