网络编程及socket模块(2)

简介: 网络编程及socket模块(2)

3. B/S和C/S架构

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

平时在开发或与人沟通时,经常会有人提到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层模型

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

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

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

假设,你在浏览器上输入了一些关键字,内部通过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

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


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

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

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


简单的可以理解为发快递:将数据外面套了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三次握手和四次挥手

这是一个常见的面试题。

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

20210527153548522.png

 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. 粘包

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

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


对于发送者,执行 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,)

96447814-120fc980-1245-11eb-938d-6ea408716c72.png

示例代码:

  • 服务端
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()

如果代码变成了非阻塞,程序运行时一旦遇到 accept、recv、connect 就会抛出 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()时便得到通知。
相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
14天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
14天前
|
程序员 开发者 Python
Python网络编程基础(Socket编程) 错误处理和异常处理的最佳实践
【4月更文挑战第11天】在网络编程中,错误处理和异常管理不仅是为了程序的健壮性,也是为了提供清晰的用户反馈以及优雅的故障恢复。在前面的章节中,我们讨论了如何使用`try-except`语句来处理网络错误。现在,我们将深入探讨错误处理和异常处理的最佳实践。
|
13天前
|
存储 算法 Linux
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
37 6
|
1天前
|
安全 JavaScript 前端开发
第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题—B模块安全事件响应/网络安全数据取证/应用安全
该内容描述了一次网络安全演练,包括七个部分:Linux渗透提权、内存取证、页面信息发现、数字取证调查、网络安全应急响应、Python代码分析和逆向分析。参与者需在模拟环境中收集Flag值,涉及任务如获取服务器信息、提权、解析内存片段、分析网络数据包、处理代码漏洞、解码逆向操作等。每个部分都列出了若干具体任务,要求提取或生成特定信息作为Flag提交。
3 0
|
1天前
|
安全 测试技术 Linux
2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-A模块安全事件响应/网络安全数据取证/应用安全
该内容描述了一个网络安全挑战,涉及Windows和Linux系统的渗透测试以及隐藏信息探索和内存取证。挑战包括使用Kali Linux对Windows Server进行服务扫描、DNS信息提取、密码获取、文件名和内容查找等。对于Linux系统,任务包括收集服务器信息、提权并查找特定文件内容和密码。此外,还有对Server2007网站的多步骤渗透,寻找登录界面和页面中的隐藏FLAG。最后,需要通过FTP获取win20230306服务器的内存片段,从中提取密码、地址、主机名、挖矿程序信息和浏览器搜索关键词。
2 0
|
1天前
|
安全 测试技术 网络安全
2024年甘肃省职业院校技能大赛中职组 “网络安全”赛项竞赛样题-C模块安全事件响应/网络安全数据取证/应用安全
涉及安全事件响应和应用安全测试。需使用Kali对Windows Server2105进行渗透测试,包括服务扫描、DNS信息提取、管理员密码、文件名与内容、图片中单词等。另外,需收集win20230305的服务器端口、页面信息、脚本、登录后信息等。在Linux Server2214上,要获取SSH端口、主机名、内核版本并进行提权操作。网络安全响应针对Server2228,涉及删除后门用户、查找SSH后门时间、恢复环境变量、识别篡改文件格式和矿池钱包地址。最后,对lin20230509进行网站渗透,获取端口号、数据库服务版本、脚本创建时间、页面路径、内核版本和root目录下的flag文件内容
3 0
|
1天前
|
SQL 安全 测试技术
2021年职业院校技能大赛“网络安全”项目 江西省比赛任务书—B模块
B模块涵盖安全事件响应和网络数据取证,涉及多项应用安全挑战。任务包括使用nmap扫描靶机、弱口令登录、生成反弹木马、权限验证、系统内核版本检查、漏洞源码利用、文件名和内容提取等。此外,还有Linux渗透测试,要求访问特定目录下的文件并提取内容。应用服务漏洞扫描涉及服务版本探测、敏感文件发现、私钥解密、权限提升等。SQL注入测试需利用Nmap扫描端口,进行SQL注入并获取敏感信息。应急响应任务包括处理木马、删除恶意用户、修复启动项和清除服务器上的木马。流量分析涉及Wireshark数据包分析,查找黑客IP、枚举测试、服务破解等。渗透测试任务涵盖系统服务扫描、数据库管理、漏洞利用模块搜索等。
8 0
|
13天前
|
网络协议 Java API
Python网络编程基础(Socket编程)Twisted框架简介
【4月更文挑战第12天】在网络编程的实践中,除了使用基本的Socket API之外,还有许多高级的网络编程库可以帮助我们更高效地构建复杂和健壮的网络应用。这些库通常提供了异步IO、事件驱动、协议实现等高级功能,使得开发者能够专注于业务逻辑的实现,而不用过多关注底层的网络细节。
|
1月前
|
机器学习/深度学习 数据采集 人工智能
m基于深度学习网络的手势识别系统matlab仿真,包含GUI界面
m基于深度学习网络的手势识别系统matlab仿真,包含GUI界面
41 0
|
1月前
|
机器学习/深度学习 算法 计算机视觉
基于yolov2深度学习网络的火焰烟雾检测系统matlab仿真
基于yolov2深度学习网络的火焰烟雾检测系统matlab仿真