Python Socket通信入门指南:网络编程基础

目录

一、网络编程基础概念

1. 项目架构

C/S(Client Server)架构

B/S(Browser Server)架构

2. 网络通信相关概念

3. 五层模型

二、核心技术 Socket

(一)Socket 编程步骤

        TCP 服务端步骤

        TCP 客户端步骤

        UDP 服务端步骤

        UDP 客户端步骤

(二)TCP Socket 通信

1. 基本 TCP 服务端

2. 基本 TCP 客户端

3. 带消息交互的 TCP 服务端

4. 带消息交互的 TCP 客户端

5. 支持多客户端的 TCP 服务端

6. 支持持续交互的 TCP 客户端

(三)UDP Socket 通信

1. UDP 服务端

2. UDP 客户端

(四)自定义封装的 UDP 通信

1. 自定义封装类

代码解释

2. 自定义封装的 UDP 服务端

代码解释

3. 自定义封装的 UDP 客户端

代码解释

三、三次握手

1. 什么是三次握手?

2. 三次握手的具体步骤

3. 为什么需要三次握手?

4. 类比生活中的例子

5. 常见问题

6. 总结

四、Python网络编程中的"粘包"问题

(一)什么是粘包问题?

(二)为什么会出现粘包?

(三)粘包问题演示代码

(四)解决粘包的四种方案

1. 固定长度消息

2. 特殊分隔符

3. 消息长度前缀

4. 使用标准协议

(五)实际项目中的选择建议

(六)总结


一、网络编程基础概念

1. 项目架构

C/S(Client Server)架构

像钉钉、QQ、微信这类应用采用的就是 C/S 架构。其优点在于可以将一些不常修改的文件缓存到本地客户端,不过需要用户安装本地客户端。

B/S(Browser Server)架构

教务管理系统通常使用 B/S 架构,它其实是 C/S 架构的一种特殊形式。优点是不需要安装本地客户端,方便维护和统一更新,但要求服务器性能足够优秀,能够处理高并发,往往需要服务器集群支持。

2. 网络通信相关概念

  • MAC 地址:是一个唯一的设备物理地址,类似于身份证。
  • IP 地址:这台设备在网络中的标识位置,例如 172.16.1.48 ,127.0.0.1 是本地回环地址,也可表示为 localhost 。IP 地址的范围是 XXX.XXX.XXX.XXX ,理论上有 256*256*256*256 种组合。
  • 端口号:每个程序的标识位置,有效端口范围是 0 – 65535,其中 0 – 1024 为系统备用端口。ip + 端口 可以准确找到某一台设备的某一款软件。
  • 路由器:连接不同网段的路由设备。
  • 网关:某一个网段的入口和出口。
  • 子网掩码:通过 IP 地址 & 子网掩码 可以得到真实网段,例如 172.16.1.11 & 255.255.255.0 。
  • 3. 五层模型

  • 应用层:如 http 协议,例如 http://ssssss 。
  • 传输层:主要有 tcp/udp 两种协议。
  • 网络层:涉及 ip 地址和端口。
  • 数据链路层:使用 arp 协议,通过 ip 查找 mac 地址,与网卡相关。
  • 物理层:以电信号进行数据传输。
  • 二、核心技术 Socket

    (一)Socket 编程步骤

            TCP 服务端步骤
    1. 导入 socket 模块import socket
    2. 创建 socket 对象sk = socket.socket()
    3. 绑定 IP 地址和端口号sk.bind((ip, port))
    4. 开始监听客户端连接sk.listen()
    5. 接受客户端连接conn, addr = sk.accept()
    6. 进行消息交互:使用 conn.send() 和 conn.recv() 发送和接收消息
    7. 关闭与客户端的连接conn.close()
    8. 关闭服务器套接字sk.close()
            TCP 客户端步骤
    1. 导入 socket 模块import socket
    2. 创建 socket 对象sk = socket.socket()
    3. 连接到服务器sk.connect((ip, port))
    4. 进行消息交互:使用 sk.send() 和 sk.recv() 发送和接收消息
    5. 关闭客户端套接字sk.close()
            UDP 服务端步骤
    1. 导入 socket 模块import socket
    2. 创建 UDP socket 对象sk = socket.socket(type=socket.SOCK_DGRAM)
    3. 绑定 IP 地址和端口号sk.bind((ip, port))
    4. 接收和发送消息:使用 sk.recvfrom() 和 sk.sendto() 接收和发送消息
    5. 关闭 UDP 套接字sk.close()
            UDP 客户端步骤
    1. 导入 socket 模块import socket
    2. 创建 UDP socket 对象sk = socket.socket(type=socket.SOCK_DGRAM)
    3. 发送和接收消息:使用 sk.sendto() 和 sk.recvfrom() 发送和接收消息
    4. 关闭 UDP 套接字sk.close()

    (二)TCP Socket 通信

    1. 基本 TCP 服务端
    import socket
    
    # 创建 socket 对象,默认使用 TCP 协议
    sk = socket.socket()
    
    # 绑定 IP 地址和端口号,这里使用本地回环地址和 8080 端口
    sk.bind(("192.168.134.1", 8080))
    
    # 开始监听客户端的连接请求,参数未指定表示使用默认值
    sk.listen()
    
    print("服务器启动,等待客户端连接...")
    
    # 接受客户端的连接,返回一个新的套接字对象 conn 和客户端的地址 addr
    conn, addr = sk.accept()
    
    # 打印新的套接字对象和客户端地址
    print(conn)
    print(addr)
    
    # 关闭与客户端的连接
    conn.close()
    
    # 关闭服务器套接字
    sk.close()
    

    此代码创建了一个简单的 TCP 服务器,绑定到指定地址和端口,等待客户端连接,连接成功后打印相关信息,最后关闭连接。

    2. 基本 TCP 客户端
    import socket
    
    # 创建 TCP 套接字对象
    sk = socket.socket()
    
    # 连接到指定的服务器地址和端口
    sk.connect(("192.168.134.1", 8080))
    
    # 关闭客户端套接字
    sk.close()
    

    该代码创建了一个简单的 TCP 客户端,连接到指定的服务器后关闭连接。

    3. 带消息交互的 TCP 服务端
    import socket
    
    # 创建 TCP 套接字对象
    sk = socket.socket()
    
    # 绑定 IP 地址和端口号
    sk.bind(("172.16.1.48", 8080))
    
    # 开始监听客户端连接
    sk.listen()
    
    print("服务器启动,等待客户端连接...")
    
    # 接受客户端连接
    conn, addr = sk.accept()
    
    # 向客户端发送消息,需要将字符串编码为字节类型
    conn.send("你好,客户端".encode("utf-8"))
    
    # 接收客户端发送的消息,最多接收 100 字节
    msg = conn.recv(100)
    
    # 打印接收到的消息,需要将字节类型解码为字符串
    print(msg.decode("utf-8"))
    
    # 关闭与客户端的连接
    conn.close()
    
    # 关闭服务器套接字
    sk.close()

    该代码在基本服务器的基础上,增加了与客户端的消息交互,服务器先向客户端发送消息,再接收客户端消息。

    4. 带消息交互的 TCP 客户端
    import socket
    
    # 创建 TCP 套接字对象
    sk = socket.socket()
    
    # 连接到指定的服务器地址和端口
    sk.connect(("172.16.1.48", 8080))
    
    # 接收服务器发送的消息,最多接收 100 字节
    msg = sk.recv(100)
    
    # 向服务器发送消息,需要将字符串编码为字节类型
    sk.send("你好,服务器".encode("utf-8"))
    
    # 关闭客户端套接字
    sk.close()

    此代码在基本客户端的基础上,增加了与服务器的消息交互,先接收服务器消息,再向服务器发送消息。

    5. 支持多客户端的 TCP 服务端
    import socket
    
    # 创建 TCP 套接字对象
    sk = socket.socket()
    
    # 绑定 IP 地址和端口号
    sk.bind(("172.16.1.48", 8080))
    
    # 开始监听客户端连接
    sk.listen()
    
    while True:
        print("服务器启动,等待客户端连接...")
        # 接受客户端连接
        conn, addr = sk.accept()
    
        while True:
            print("等待客户端发送消息")
            # 接收客户端消息,最多接收 1024 字节,并解码为字符串
            msg_client = conn.recv(1024).decode("utf-8")
            print("客户端消息:" + msg_client)
    
            # 如果客户端发送 "886",则退出内层循环,结束与该客户端的通信
            if msg_client == "886":
                break
    
            # 服务器输入要发送给客户端的消息
            msg_server = input("请输入发送给客户端的消息")
            # 发送消息给客户端,需要将字符串编码为字节类型
            conn.send(msg_server.encode("utf-8"))
    
            # 如果服务器发送 "886",则退出内层循环,结束与该客户端的通信
            if msg_server == "886":
                break
    
        # 关闭与当前客户端的连接
        conn.close()
    
    # 关闭服务器套接字
    sk.close()

    此代码实现了一个支持多个客户端连接的 TCP 服务器,服务器会不断循环接受新的客户端连接,并与每个客户端进行消息交互,直到客户端或服务器发送 "886" 结束通信。

    6. 支持持续交互的 TCP 客户端
    import socket
    
    # 创建 TCP 套接字对象
    sk = socket.socket()
    
    # 连接到指定的服务器地址和端口
    sk.connect(("172.16.1.48", 8080))
    
    while True:
        # 客户端输入要发送给服务器的消息
        msg_server = input("请输入发送给服务器的消息")
        # 发送消息给服务器,需要将字符串编码为字节类型
        sk.send(msg_server.encode("utf-8"))
    
        # 如果客户端发送 "886",则退出循环,结束通信
        if msg_server == "886":
            break
    
        print("准备接收服务器的消息")
        # 接收服务器消息,最多接收 1024 字节,并解码为字符串
        msg_client = sk.recv(1024).decode("utf-8")
        print("服务器消息:" + msg_client)
    
        # 如果服务器发送 "886",则退出循环,结束通信
        if msg_client == "886":
            break
    
    # 关闭客户端套接字
    sk.close()

    该代码实现了一个支持持续消息交互的 TCP 客户端,客户端可以不断向服务器发送消息,并接收服务器的响应,直到客户端或服务器发送 "886" 结束通信。

    (三)UDP Socket 通信

    1. UDP 服务端
    import socket
    
    # 创建 UDP 套接字对象,SOCK_DGRAM 表示使用 UDP 协议
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    # 绑定 IP 地址和端口号
    sk.bind(('172.16.1.48', 8080))
    
    while True:
        print("等待客户端发送消息")
    
        # 接收客户端消息,最多接收 1024 字节,并返回消息和客户端地址
        msg_client, addr = sk.recvfrom(1024)
    
        # 打印接收到的消息,需要将字节类型解码为字符串
        print(msg_client.decode("utf-8"), addr)
    
        # 如果客户端发送 "exit",则退出循环,结束通信
        if msg_client.decode("utf-8") == "exit":
            break
    
        # 服务器输入要发送给客户端的消息
        msg_server = input("请输入要发送给客户端的消息...")
        # 发送消息给客户端,需要将字符串编码为字节类型,并指定客户端地址
        sk.sendto(msg_server.encode(), addr)
    
        # 如果服务器输入 "exit",则继续循环
        if msg_server == "exit":
            continue
    
    # 关闭 UDP 套接字
    sk.close()
    

    此代码创建了一个 UDP 服务器,绑定到指定地址和端口,不断接收客户端消息并做出响应,直到客户端或服务器发送 "exit" 结束通信。

    2. UDP 客户端
    import socket
    
    # 创建 UDP 套接字对象
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    while True:
        # 客户端输入要发送给服务器的消息
        msg_client = input("请输入要发送给服务器的消息...")
    
        # 发送消息给服务器,需要将字符串编码为字节类型,并指定服务器地址和端口
        sk.sendto(msg_client.encode(), ("172.16.1.48", 8080))
    
        # 如果客户端发送 "exit",则退出循环,结束通信
        if msg_client == "exit":
            break
    
        print("等待接收服务器的消息")
        # 接收服务器消息,最多接收 1024 字节,并返回消息和服务器地址
        msg_server, addr = sk.recvfrom(1024)
    
        # 打印接收到的消息,需要将字节类型解码为字符串
        print(msg_server.decode("utf-8"), addr)
    
        # 如果服务器发送 "exit",则退出循环,结束通信
        if msg_server.decode("utf-8") == "exit":
            break
    
    # 关闭 UDP 套接字
    sk.close()
    

    该代码创建了一个 UDP 客户端,不断向服务器发送消息并接收服务器响应,直到客户端或服务器发送 "exit" 结束通信。

    (四)自定义封装的 UDP 通信

    1. 自定义封装类
    import socket
    
    class MySocket:
        def __init__(self):
            # 创建 UDP 套接字对象
            self.sk = socket.socket(type=socket.SOCK_DGRAM)
            # 定义默认的编码方式为 UTF-8
            self.encoding = 'utf-8'
    
        def bind(self, address):
            # 绑定 IP 地址和端口号
            self.sk.bind(address)
    
        def my_sendto(self, message, address):
            # 将消息编码为字节类型并发送到指定地址
            self.sk.sendto(message.encode(self.encoding), address)
    
        def my_recv(self, buffer_size=1024):
            # 接收消息,最多接收 buffer_size 字节
            data, addr = self.sk.recvfrom(buffer_size)
            # 将接收到的字节数据解码为字符串
            decoded_data = data.decode(self.encoding)
            return decoded_data, addr
    
        def close(self):
            # 关闭 UDP 套接字
            self.sk.close()
    
    代码解释
  • __init__ 方法:初始化 MySocket 类的实例,创建一个 UDP 套接字对象,并设置默认的编码方式为 UTF – 8。
  • bind 方法:用于绑定 IP 地址和端口号,调用底层套接字的 bind 方法。
  • my_sendto 方法:将消息编码为字节类型,并发送到指定的地址。
  • my_recv 方法:接收消息,最多接收 buffer_size 字节的数据,然后将接收到的字节数据解码为字符串。
  • close 方法:关闭 UDP 套接字。
  • 2. 自定义封装的 UDP 服务端
    from udp_manager import MySocket
    
    # 创建自定义的 UDP 套接字对象,自动使用 UTF - 8 编码
    sk = MySocket()
    
    # 绑定本地回环地址和端口 8080
    sk.bind(("127.0.0.1", 8080))
    
    # 接收客户端消息,自动解码
    msg, addr = sk.my_recv(1024)
    
    # 打印消息和客户端地址
    print(msg, addr)
    
    # 关闭套接字
    sk.close()
    
    代码解释
  • __init__ 方法:初始化 MySocket 类的实例,创建一个 UDP 套接字对象,并设置默认的编码方式为 UTF – 8。
  • bind 方法:用于绑定 IP 地址和端口号,调用底层套接字的 bind 方法。
  • my_sendto 方法:将消息编码为字节类型,并发送到指定的地址。
  • my_recv 方法:接收消息,最多接收 buffer_size 字节的数据,然后将接收到的字节数据解码为字符串。
  • close 方法:关闭 UDP 套接字。
  • 3. 自定义封装的 UDP 客户端
    from udp_manager import MySocket
    
    # 创建自定义的 UDP 套接字对象
    sk = MySocket()
    
    # 向服务器发送消息
    sk.my_sendto("你好服务器", ("127.0.0.1", 8080))
    
    # 关闭套接字
    sk.close()
    
    代码解释
  • 导入 MySocket 类,创建一个 MySocket 对象。
  • 向服务器发送消息。
  • 关闭套接字。
  • 三、三次握手

    1. 什么是三次握手?

    三次握手是TCP(传输控制协议)在建立连接时使用的过程,确保客户端和服务器双方具备可靠的双向通信能力。它通过三次报文交互确认双方的发送和接收功能正常。

    2. 三次握手的具体步骤

    假设客户端(Client)和服务器(Server)建立连接:

    1. 第一次握手(SYN)

    2. 客户端 → 服务器:发送SYN=1(同步序列号)、随机生成一个初始序列号seq=x

    3. 目的:客户端告知服务器“我想建立连接,我的初始序列号是x”。

    4. 第二次握手(SYN + ACK)

    5. 服务器 → 客户端:发送SYN=1ACK=1(确认),确认号ack=x+1(表示已收到客户端的seq=x),并随机生成服务器的初始序列号seq=y

    6. 目的:服务器回应“我收到你的请求了,同意建立连接,我的初始序列号是y”。

    7. 第三次握手(ACK)

    8. 客户端 → 服务器:发送ACK=1,确认号ack=y+1(表示已收到服务器的seq=y),序列号seq=x+1(因第一次握手的SYN占用了一个序号)。

    9. 目的:客户端确认服务器的响应,连接正式建立。

    3. 为什么需要三次握手?
  • 防止历史重复连接初始化造成的资源浪费
    如果只有两次握手,服务器无法区分当前连接是否是因网络延迟而重传的旧SYN请求,可能导致无效连接占用资源。

  • 同步双方初始序列号(ISN)
    TCP依赖序列号保证数据有序性和可靠性。三次握手确保双方均确认对方的初始序列号,为后续数据传输奠定基础。

  • 双向通信能力验证
    三次握手确认客户端和服务器双方的发送和接收能力均正常:

  • 第一次握手:服务器确认客户端发送正常。

  • 第二次握手:客户端确认服务器收发正常。

  • 第三次握手:服务器确认客户端接收正常。

  • 4. 类比生活中的例子

    想象打电话的场景:

    1. 客户端:“喂,听得到吗?”(SYN)

    2. 服务器:“听得到,你能听到我吗?”(SYN + ACK)

    3. 客户端:“能听到!”(ACK)
      ——至此双方确认通信正常,开始对话。

    5. 常见问题
  • 为什么不是两次或四次?

  • 两次无法确认客户端的接收能力,可能导致服务器盲目发送数据。

  • 四次多余,三次已能可靠确认双方能力。

  • SYN洪泛攻击(SYN Flood)
    攻击者伪造大量SYN报文但不完成握手,耗尽服务器资源。防御方式包括SYN Cookie机制。

  • 6. 总结

    三次握手是TCP可靠性的核心机制之一,通过三次交互确保连接双方具备通信能力,同时避免无效连接占用资源。理解这一过程对学习网络协议和排查连接问题至关重要。

    四、Python网络编程中的"粘包"问题

    (一)什么是粘包问题?

    专业解释:在网络编程中,粘包(TCP粘包)是指发送方发送的若干数据包到达接收方时粘成了一个包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

    通俗理解:想象你正在通过快递发送几本书给朋友。理想情况下,每本书应该单独包装发送。但实际可能发生两种情况:1) 几本书被塞进同一个箱子发送(粘包);2) 一本书被拆分成多个包裹发送(拆包)。

    (二)为什么会出现粘包?

    专业术语

  • TCP是面向连接的流式协议

  • 数据传输采用Nagle算法优化

  • 接收缓冲区数据堆积

  • 通俗解释:TCP为了传输效率,会把多个小数据块合并发送(就像快递公司把多个小包裹合并装箱),而且不保留数据边界。就像水流一样,你无法直接看出哪里是一滴水的开始和结束。

    (三)粘包问题演示代码

    # 服务端代码
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 8888))
    server.listen(5)
    
    conn, addr = server.accept()
    data = conn.recv(1024)  # 接收数据
    print("Received:", data.decode())
    conn.close()
    server.close()
    
    # 客户端代码
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8888))
    
    # 发送两条消息
    client.send("Hello".encode())
    client.send("World".encode())
    
    client.close()

    运行上述代码,服务端可能会一次性打印"HelloWorld",而不是分开的"Hello"和"World"。

    (四)解决粘包的四种方案

    1. 固定长度消息

    专业术语:定长报文

    实现方式:每条消息都固定长度,不足部分填充空字符。

    # 发送方
    message = "Hello".ljust(10, ' ')  # 固定10字节
    client.send(message.encode())
    
    # 接收方
    data = conn.recv(10)  # 每次固定接收10字节
    print("Received:", data.decode().strip())

    优点:实现简单
    缺点:浪费带宽,不适合变长数据

    2. 特殊分隔符

    专业术语:分隔符界定法(Delimiter-based framing)

    实现方式:用特殊字符(如\n)标记消息结束。

    # 发送方
    client.send("Hello\n".encode())
    client.send("World\n".encode())
    
    # 接收方
    buffer = ""
    while True:
        data = conn.recv(1024).decode()
        if not data:
            break
        buffer += data
        while "\n" in buffer:
            message, buffer = buffer.split("\n", 1)
            print("Received:", message)

    优点:适合文本协议
    缺点:二进制数据中可能出现分隔符冲突

    3. 消息长度前缀

    专业术语:长度前缀法(Length-prefixed framing)

    实现方式:在消息前添加长度信息。

    # 发送方
    message = "Hello"
    client.send(len(message).to_bytes(4, 'big') + message.encode())
    
    # 接收方
    length_data = conn.recv(4)
    length = int.from_bytes(length_data, 'big')
    message = conn.recv(length).decode()
    print("Received:", message)

    优点:高效可靠
    缺点:实现稍复杂

    4. 使用标准协议

    专业术语:应用层协议封装

    推荐方案:直接使用现成的协议如HTTP、WebSocket等,它们已经解决了粘包问题。

    (五)实际项目中的选择建议

    1. 简单文本通信:使用分隔符法(\n)

    2. 二进制数据传输:使用长度前缀法

    3. 复杂应用:直接使用现成协议(如HTTP/WebSocket)

    (六)总结

    粘包问题是TCP流式传输的特性导致的,不是bug而是需要考虑的特性。理解其原理后,通过合适的拆包策略可以完美解决。在实际开发中,根据数据类型和业务需求选择最适合的方案即可。

    专业术语回顾

  • 粘包(TCP粘包)

  • 流式协议(Stream Protocol)

  • 定长报文(Fixed-length message)

  • 分隔符界定法(Delimiter-based framing)

  • 长度前缀法(Length-prefixed framing)

  • 作者:PythonicCC

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python Socket通信入门指南:网络编程基础

    发表回复