前言

写这篇文章,主要用来作为学习总结昨儿笔记,好记性不如烂笔头,并非用作商业用途。参考资料:

  1. 掘进课程:Python异步网络编程实战
  2. 慕课网bobby老师课程(异步编程讲的比较详细推荐学习):Python3高级核心技术97讲
  3. 三次握手和四次挥详解

网络的架构模型

下面是网络的五层架构,严格来说是七层架构,比较常见的就是这五层。

OSI层功能TCP/IP 协议
应用层文件传输,电子邮件,数据服务HTTP,FTP,STMP,DNS,数据库访问等等
传输层提供端对端的接口TCP、UDP
网络层为数据包选择路由IP、ICMP 等
数据链路层传输有地址的帧、错误检测功能ARP
物理层物理媒体1000BASE-SX等

而 Socket的含义是插座,就好比当作是我们电器设备和电源连接的一个接口,在我们的开发领域叫套字节,是两台服务器之间的网络 应用层和传输层的之间的传输接口。

socket的服务端和客户端交互图

注:客户端和服务端之间发送的数据都是字节类型的数据,所以字符串都需要进行编码成字节类型的数据

f1ff37314c042f27a9b09cfe8f48c3fa

调用流程:

write()方法其实对应的是send()方法,发送数据
read()方法其实对应的是recv()方法,用来接收数据
7327a121f2221f5fb6f9e53705e1284a

三次握手流程图:

c4b822657ed484e477b6ff6f0daf834c

第一次握手

建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;(x 是随机生成的一个 int 数值)然后,客户端进入SYN_SEND状态,等待服务器的确认;

第二次握手

服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为 y (y 是随机生存的一个 int 数值);服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;

第三次握手

客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

四次挥手

b3fc08a3818914ca10e06ae96651080f

第一次挥手:

Client (可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向 Server发送一个FIN报文段;此时,Client 进入FIN_WAIT_1状态;这表示 Client 没有数据要发送给 Server了;
客户端发送第一次挥手后,就不能在向 服务端发送数据了。

第二次挥手:

Server 收到了 Client 发送的FIN报文段,向 Client 回一个ACK报文段,Acknowledgment Number 为 Sequence Number 加 1;Client 进入 FIN_WAIT_2 状态;Server 告诉 Client ,我“同意”你的关闭请求;
Server 第一次响应后,还可以继续向 Client 发送数据,这里只是告诉 Client ,我收到你发送的关闭请求。

第三次挥手

Server 向 Client 发送 FIN 报文段,请求关闭连接,同时 Server 进入 CLOSE_WAIT 状态;
当 Server 的数据响应完成后,再告诉 Client,我这边也可以关闭请求了, 这时
Server 就不能再向 Client 发送数据了

第四次挥手

Client 收到 Server 发送的 FIN 报文段,向 Server 发送 ACK 报文段,然后 Client 进入
TIME_WAIT 状态;Server 收到 Client 的 ACK 报文段以后,就关闭连接;此时,Client
等待2MSL后依然没有收到回复,则证明 Server 端已正常关闭,那好,Client 也可以关闭连接了。

代码实现简单例子

主要用来展示服务端和客户端socket编程代码示例的演示

服务端单线程的代码实现

# --*-- conding:utf-8 --*--
# todo: tcp单线程的服务端测试
#
# @Time : 2024/6/21 23:20
# @Author : allen.huang
# @Email : hjc_042042@sina.cn
# @Software : PyCharm
import socket

# 预先定义好IP和端口
# ip为"",表示所有ip,也可以用0.0.0.0表示;port 为任意未被占用的端口
ip_port = ('', 9898)

# 定义 TCP 服务器的套字节
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 将IP 的地址和端口绑定到套字节上
server_socket.bind(ip_port)

# 开启服务端网络监听
server_socket.listen()

# 进入监听状态后,不断接收客户端的连接
while True:
    # 启动服务器后,立即打印这语句,与客户端断开连接后也会打印该语句
    print("waiting for connection...")

    # todo 下一行为为阻塞代码,等待客户端的连接
    # todo 服务器套字节的accept()方法会一直阻塞,直到有客户端连接上来
    # todo 当成功接入客户端,accept()方法会返回一个临时的套字节和客户端的地址,来进行发送和接收数据
    tcp_extension_socket, client_address = server_socket.accept()

    # todo 如果此时另一个客户端连接上来,会阻塞等待上一个连接断开
    # todo 连接成功后保持等待,前一个已经连接的客户端断开连接后,才会处理下一个客户端的连接。
    print(f"连接成功后:{client_address}")

    while True:
        # todo 不断接收客户端发送的数据
        # todo 此方法也是阻塞运行,每次接收固定大小的数据,直到全部接收完毕
        # todo recv()的参数为数据缓冲区大小,表示一次最多接收多少字节的数据,默认是1024
        data = tcp_extension_socket.recv(1024)
        if not data:
            break
        # todo 打印接收到的数据data,data为二进制数据,需要转换为字符串,默认是 utf-8编码
        print(f"收到数据:{data.decode()}")

        # todo 发送数据给客户端
        send_data = f"[Server] {data.decode()}"
        tcp_extension_socket.send(send_data.encode())
    # todo 循环结束后关闭临时套字节
    tcp_extension_socket.close()

# todo 主动关闭服务器套字节
server_socket.close()

服务端的多线程代码实现

服务端主要采用多线程来处理客户端的连接

# --*-- conding:utf-8 --*--
# todo:多线程服务端,并发处理客户端连接
#
# @Time : 2024/6/22 01:14
# @Author : allen.huang
# @Email : hjc_042042@sina.cn
# @Software : PyCharm
import socket
import threading

# ip为"",表示所有ip,也可以用0.0.0.0表示;port 为任意未被占用的端口
ADDR = ('', 9898)
# 一次接收的数据大小,这里是1024个字节
BUFF_SIZE = 1024
# 声明一个套字节
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址信息
tcp_server.bind(ADDR)
# 监听服务端网络
tcp_server.listen()


def handle(sock, addr):
    """
    出来每一个从客户端过来的数据
    """
    while True:
        # 套字节等待客户端的连接,使用recv接收数据这过程是阻塞的
        # 阻塞等待期间,释放 CPU,CPU 可以执行其他线程的任务
        data = sock.recv(BUFF_SIZE)
        if not data:
            sock.close()
            break
        print(f"[Server]收到数据:{data.decode()}")
        send_data = "服务端应答;success".encode()
        sock.send(send_data)
    # 关闭临时字节
    sock.close()
    print('{}已关闭'.format(addr))


def main():
    print("等待客户端连接...")
    # todo 进入无线循环,接收客户端连接请求,就循环一次,创建子线程
    # todo 这样就可以创建多线程来并发处理多个客户端请求
    while True:
        try:
            # 接收每一个客户端连接,由于 accept是阻塞的,所以使用多线程来处理可以提高性能。
            tcp_extension_socket, addr = tcp_server.accept()
        except KeyboardInterrupt:
            break
        print('[Server]接收到来自{}的连接'.format(addr))
        # 创建子线程
        # 子线程的参数是一个函数,函数的参数是一个套接字和一个地址
        t = threading.Thread(target=handle, args=(tcp_extension_socket, addr))
        t.start()

    # 循环结束,关闭服务端套字节,退出程序
    print("\n Exit")
    tcp_server.close()


if __name__ == '__main__':
    main()

客户端代码实现

客户端的逻辑比服务端要简单。只要连接服务端的地址和端口,然后给服务端发送数据,接收服务端的反馈就行。

# --*-- conding:utf-8 --*--
# todo:tcp 的客户端连接测试
#
# @Time : 2024/6/21 23:20
# @Author : allen.huang
# @Email : hjc_042042@sina.cn
# @Software : PyCharm
import socket

# todo 定义客户端套字节
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接服务端套字节
tcp_client.connect(('127.0.0.1', 9898))

# 不断发送给服务端数据
while True:
    data = input('>>>')
    if not data:
        break
    # 发送二进制数据,默认是二进制数据
    tcp_client.send(data.encode())
    # 接收服务端数据
    data = tcp_client.recv(1024)
    if not data:
        break
    print(data.decode())

# 关闭客户端套字节
# tcp_client.close()

查看单线执行效果图:

注意在使用时,需要先启动服务端,这样子客户端才能正常连接

如下这是服务端的效果图
6614f9bc064c26d9d116ab13ed57f612

客户端的交互图:
客户端输入一个,服务端就接收一个。在输入空字符,即回车的时候,就退出了
304a86572e1e8d89eff0eb44ad14539a

查看多线程执行效果图:

这是服务端的:
c445023b8b61d7549b5a2531ea369021

之后多个客户端连接进来,就可以看到多个的客户端id,并接收了不同的数据
8aea2d6101935d7c7883290cb93db95f

下面就是3个客户端的发数据给服务端的情况
e5116efcb400ce81773dd3d3b04a31c1

在客户端退出时,服务端就会显示这个提示
086299a34fac5853d7837abd0e6bee28

使用socket模拟http请求

可以通过开启一个 socket 的客户端来模拟 http 的请求,需要把 header 设置成固定的格式

...
client_socket.send(f'GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n'.encode('utf-8'))
....

在接受到数据超过1024字节时,可以通过拼接的方式来进行组装数据,直到数据请求完成:

...
# 接收响应内容,其实整个html响应结果肯定是大于1024字节的,所以我们需要用循环来接收
data = b""
while True:
    response = client_socket.recv(1024)
    if response:
        data += response
    else:
        # 如果没有接受到数据了,就退出
        break
...

完整代码实现

# --*-- conding:utf-8 --*--
# todo:模拟http客户端来请求百度网站
#
# @Time : 2024/5/30 02:33
# @Author : allen.huang
# @Email : hjc_042042@sina.cn
# @Software : PyCharm

import socket
from urllib.parse import urlparse


def parse_http(url):
    # 通过socket模拟http请求获取html
    url_info = urlparse(url)
    # 获取url的host
    host = url_info.netloc
    # 获取url的path
    path = url_info.path
    # 如果path为空,则设置为'/',这是相对路径
    if path == '':
        path = '/'

    # 创建socket连接
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, 80))
    print("等待服务端响应...")

    # 发送请求,这个格式是固定的
    client_socket.send(f'GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n'.encode('utf-8'))
    # 接收响应内容,其实整个html响应结果肯定是大于1024字节的,所以我们需要用循环来接收
    data = b""
    while True:
        response = client_socket.recv(1024)
        if response:
            data += response
        else:
            # 如果没有接受到数据了,就退出
            break

    # 解码
    html_data = data.decode('utf-8')
    # 关闭连接
    client_socket.close()
    # 返回响应
    return html_data


if __name__ == '__main__':
    # 获取html
    html = parse_http('https://www.baidu.com/')
    print(html)
    pass

执行效果:

这是响应出来的结构,包含响应头和响应体
96d5c608140ba068e14440610d8e27e8

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部