网络编程

IP 地址

端口和端口号

什么是端口号?

端口号在计算机网络中用来标识特定的进程或网络服务。IP 地址用来定位计算机,而端口号用来定位计算机上的服务。

不同电脑上的飞秋之间进行数据通信,它是如何保证把数据给飞秋而不是给其它软件呢?

其实,每运行一个网络程序都会有一个端口,想要给对应的程序发送数据,找到对应的端口即可。

网络编程

端口号分类

端口号共为 16 位,从 0 到 65535。常用的分类方式把端口号分成三个范围:

  1. 知名端口(Well-known Ports):
    • 范围:0-1023
    • 描述:这些端口通常分配给标准服务和协议。这些端口由互联网号码分配局 (IANA) 管理和分配,常用于一些常见网络服务。
    • 示例端口:
      • 20/21: FTP (File Transfer Protocol)
      • 22: SSH (Secure Shell)
      • 23: Telnet
      • 25: SMTP (Simple Mail Transfer Protocol)
      • 53: DNS (Domain Name System)
      • 80: HTTP (Hypertext Transfer Protocol)
      • 443: HTTPS (HTTP Secure)
  2. 注册端口(Registered Ports):
    • 范围:1024-49151
    • 描述:这些端口主要为用户进程和应用程序分配。可以注册给某些特定应用程序,但它们相对于知名端口要更灵活。
    • 这些端口为未能在知名端口中注册的专用服务使用,但仍然可通过 IANA 注册。
    • 示例:
      • 1433: Microsoft SQL Server
      • 3306: MySQL 数据库
  3. 动态或私有端口(Dynamic or Private Ports):
    • 范围:49152-65535
    • 描述:这些端口不被指定给任何特定的服务,通常在需要动态、临时通信时使用,特别是在客户端请求到达服务器时(即临时端口)。例如,在一个 TCP/IP 会话中,客户端设备会动态选择一个这部分范围中的端口来建立连接。
    • 也被称为临时端口。
    • 常用于短期用途,例如当应用程序向一个指定端口发起请求时,客户端会维护一个自身的私有端口以管理发出的请求。

TCP 介绍

TCP,或传输控制协议(Transmission Control Protocol),是互联网协议套件的核心协议之一。它提供了一种可靠的、面向连接的通信方式,用于数据在计算机网络中的传输。以下是 TCP 的一些关键特性和介绍:

特性:

  1. 面向连接:
    • 在通信双方开始数据传输之前,TCP 通过三次握手建立连接。这是一个初始的设置过程,确保双方准备好进行通信。
  2. 可靠传输:
    • TCP 通过确认、重传、序列号和校验和,保证数据包的传送是准确无误的。丢失的数据包会被重传,确保数据完整性。
  3. 流量控制:
    • TCP 使用滑动窗口机制来管理数据传输,确保不会溢出接收方的缓冲区。这使得发送方能适应接收方的能力,并调整数据传输速率。
  4. 拥塞控制:
    • TCP 实施拥塞控制算法,例如慢启动、拥塞避免、快恢复和快重传,以防止网络拥塞并优化数据流。
  5. 有序传输:
    • 数据包按顺序传递。即使在数据传输期间包被重新排序或丢失,TCP 会使用序列号确保数据在接收端精确重构。
  6. 数据完整性:
    • 每个 TCP 报文段(数据块)通过校验和验证数据完整性。如果在传输过程中发生错误,校验和帮助识别并修正这些错误。

socket

socket(简称 套接字)是进程之间网络通信一个工具,好比现实生活中的插座,所有的家用电器要想工作都是基于插座进行,进程之间想要进行网络通信需要基于这个 socket。套接字在网络编程中用于在两台设备之间建立连接,并进行数据交换。

基本概念:

  • 套接字可以视作通信的端点,一个允许程序读写网络流的数据结构。在网络编程中,套接字负责把应用程序分发到传输接收机制。
  • 套接字支持 TCP、UDP 等协议,并提供了一种标准化的接口,使得应用程序能够使用这些协议进行网络通信。

类型:

  1. 流套接字(Stream Socket):
    • 通常用于 TCP 协议。
    • 提供可靠、面向连接的服务,确保数据传输的完整性和正确顺序。
  2. 数据报套接字(Datagram Socket):
    • 通常用于 UDP 协议。
    • 提供不可靠、无连接的服务,数据可能不按顺序到达,并且不保证数据的完整性。
  3. 原始套接字(Raw Socket):
    • 允许直接访问底层协议对象,例如 IP。
    • 用于需要更低级控制和定制的数据传输。

使用:

套接字的使用遵循特定的流程,一般包括以下几个步骤:

  1. 创建套接字:
    • 使用适当的协议(TCP 或 UDP)创建一个新的套接字。例如使用 socket() 函数创建一个 TCP 套接字。
  2. 绑定:
    • 将套接字绑定到一个特定的地址和端口。通常服务器端使用此步骤,以准备接收客户端的连接请求。例如使用 bind() 将服务器套接字绑定到一个本地 IP 和端口。
  3. 监听:
    • 设置套接字的监听状态,等待客户端连接。在 TCP 中特有。例如使用 listen() 启动监听,等待客户端连接。
  4. 连接:
    • 客户端尝试连接到服务器上的套接字,服务器接受连接请求。例如使用 accept() 接收客户端的连接请求,并返回一个新的套接字用于后续数据传输。
  5. 数据传输:
    • 在连接建立后进行数据交换。可以使用 send() 和 recv() 方法,或者在 UDP 中使用 sendto() 和 recvfrom()
  6. 关闭连接:
    • 传输结束后,双方关闭套接字以释放资源。使用 close() 关闭连接。

tcp 网络应用程序开发流程

TCP 网络应用程序开发分为:

  • TCP 服务端程序开发
  • TCP 客户端程序开发
网络编程

 套接字编程准备

  • 选择协议: TCP 是面向连接且可靠的协议,适用于需要确保数据全部传输并且顺序无误的场景。
  • 架构设计: 确定网络架构,决定应用程序是需要单服务器多客户端、多服务器多客户端还是其他结构。

1、开发服务器端应用

创建套接字: 使用 socket.socket() 创建一个 TCP 套接字。

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

地址绑定: 使用 bind()方法绑定套接字到指定的地址和端口。

server_address = ('localhost', 65432)
server_socket.bind(server_address)

监听连接: 使用 listen()方法使套接字开始监听来自客户端的连接请求。

server_socket.listen(5)  # 可以同时处理的连接数

等待连接并接受请求: 使用 accept()方法接收连接请求。

connection, client_address = server_socket.accept()

数据处理和传输: 使用 recv()方法接收数据,使用 sendall()方法发送响应数据。

data = connection.recv(1024)
connection.sendall(response)

关闭连接: 处理完成后关闭连接。

connection.close()

2、开发客户端应用

步骤:

  1. 创建客户端套接字:使用 socket() 创建 TCP 套接字。
  2. 连接服务器:使用 connect() 方法连接到服务器的 IP 和端口。
  3. 数据交换:通过 send()recv() 方法与服务器进行数据传输。
  4. 关闭连接:完成数据传输后,关闭套接字。
client_socket.connect(server_address)
  • 发送请求与接收响应: 使用 sendall()发送请求,使用 recv()接收服务器的响应。
  • 关闭连接: 数据交换完成后关闭套接字。

3、测试与优化

  • 功能测试: 确保所有功能正常工作,模拟并实际测试。
  • 性能优化: 解决发现的问题,增强处理能力、减少延迟、优化处理大规模连接或数据传输。
  • 异常处理: 集中处理可能出现的异常,比如网络中断、数据错误等。

4、部署与维护

  • 部署: 根据网络应用的需求部署到合适的环境,包括所需的服务器和网络设施。
  • 维护与更新: 定期更新和维护应用程序,包括优化代码、增加新功能等。

tcp 服务端程序开发

import socket  # 导入socket模块,用于网络通信

def start_server():  # 定义一个函数来启动服务器
    # 创建一个TCP套接字,使用IPv4地址
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 允许重用地址和端口
    # server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
    
    # 将套接字绑定到所有可用的网络接口和端口8888
    server_socket.bind(('0.0.0.0', 8888))
    
    # 开始监听客户端连接,最大连接请求队列长度为5
    server_socket.listen(5)
    
    # 打印出服务器正在监听的消息
    print("服务器正在监听端口 8888...")

    # 无限循环,持续接受客户端连接
    while True:
        # 阻塞等待客户端连接,一旦有连接,返回客户端套接字和地址
        client_socket, client_address = server_socket.accept()

        # 打印连接的客户端地址
        print(f"连接来自: {client_address}")
        
        # 接收客户端发送的数据,最多接收1024字节
        data = client_socket.recv(1024)

        # 将接收到的字节数据解码为字符串并打印
        print(f"收到数据: {data.decode('utf-8')}")
        
        # 创建HTTP响应报文
        response = "HTTP/1.1 200 OK\r\n\r\nHello, TCP Client!"
        
        # 将响应编码为字节并发送给客户端
        client_socket.sendall(response.encode('utf-8'))
        
        # 关闭与客户端的连接
        client_socket.close()

# 检查是否是直接运行该脚本
if __name__ == "__main__":
    # 调用启动服务器的函数
    start_server()

tcp 客户端程序开发

import socket

def start_client():
    # 创建一个TCP套接字
    # AF_INET表示 IPv4,SOCK_STREAM 表示 TCP 协议
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 连接到服务器
    # connect() 方法用于连接到指定的服务器地址和端口,里面是元祖:字符串类的 ip 地址和数字类型的端口号
    client_socket.connect(('服务端 IP', 8888))
    
    # 准备发送的数据
    # 将字符串 "Hello, TCP Server!" 转换为字节流
    # 使用 UTF-8 编码,将字符串编码为字节
    # 相比于 sendall():此方法也是用于发送数据,但它会确保所有的数据都被发送完毕。该方法会在发送完所有数据之前不会返回,因此它会处理在发送过程中可能出现的部分发送的情况。
    message = "Hello, TCP Server!"
    client_socket.sendall(message.encode('utf-8'))
    
    # 等待并接收来自服务器的数据
    # recv() 方法用于从连接中读取数据,参数 1024 指定一次最多接收 1024 字节
    data = client_socket.recv(1024)
    # 将接收到的字节数据解码为字符串,并打印出来
    print(f"收到服务器响应: {data.decode('utf-8')}")
    
    # 关闭客户端套接字
    # 断开与服务器的连接,释放系统资源
    client_socket.close()

# 如果当前模块是主程序入口
# 则执行 start_client 函数
if __name__ == "__main__":
    start_client()

通讯展示:
服务端:

网络编程

客户端:

网络编程

新的优化需求:

1、如果重新启动服务端的接受脚本时,会出现一个错误:OSError: [Errno 98] Address already in use
如果想要马上释放需要设置一下 socket 选项。server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 这个注释放开即可

2、当使用 client_socket.recv(1024) 接收数据时,函数会阻塞直到有数据到来或连接关闭。一个典型的 TCP 客户端关闭连接后,recv() 会返回一个空字节串 b""。在 Python 中,空容器和空字节串在布尔上下文中被视为 False,因此 if not data: 判断就能捕获这种情况,表明客户端已经断开连接。

3、客户端断开连接/脚本执行异常 导致资源无法被释放:

  • 如果在 recv() 调用时客户端突然断开连接,可能会引发异常。没有 try except 会导致程序崩溃。例如用户按下 Ctrl+C 强制退出程序,程序会异常退出。没有 try except 会导致套接字没有正确关闭,可能会导致资源泄漏。
  • 没有 finally,如果出现异常,套接字 server_socketclient_socket 可能不会被关闭,从而导致系统资源被浪费。

需要使用 try...except...finally 优化此代码

import socket  # 导入socket模块,用于网络通信

def start_server():  # 定义一个函数来启动服务器
    # 创建一个TCP套接字,使用IPv4地址
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 允许重用地址和端口,在服务器重启时不出现“地址已在使用”错误
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 将套接字绑定到所有可用的网络接口(0.0.0.0)和端口8888
    server_socket.bind(('0.0.0.0', 8888))
    
    # 开始监听客户端连接,最大连接请求队列长度为5
    server_socket.listen(5)
    print("服务器正在监听端口 8888...")  # 打印出服务器正在监听的消息

    try:
        while True:  # 无限循环,持续接受客户端连接
            # 阻塞等待客户端连接,一旦有连接则返回新的套接字和客户端地址
            client_socket, client_address = server_socket.accept()
            print(f"连接来自: {client_address}")  # 打印连接的客户端地址

            # 接收客户端发送的数据,最大缓冲区为1024字节
            data = client_socket.recv(1024)
            
            # 如果没有接收到数据,即接收到空字节串,表示客户端已关闭连接
            # 因为 recv() 返回空意味着客户端已断开,在 Python 中空值在布尔上下文中视为 False
            if not data:  
                print(f"客户端 {client_address} 已关闭连接,主动退出监听。")
                client_socket.close()  # 关闭当前客户端连接
                break  # 退出循环,结束服务器
            
            print(f"收到数据: {data.decode('utf-8')}")  # 打印接收到的数据

            # 创建一个简单的HTTP响应报文并准备发送给客户端
            response = "HTTP/1.1 200 OK\r\n\r\nHello, TCP Client!"
            client_socket.sendall(response.encode('utf-8'))  # 发送响应到客户端
            
            client_socket.close()  # 关闭与客户端的连接

    except KeyboardInterrupt:  # 捕获键盘中断信号(Ctrl+C),以便优雅地关闭服务器
        print("\n服务器正在关闭...")
    finally:
        server_socket.close()  # 确保服务器正常关闭时释放资源

# 检查该模块是否是被直接执行
if __name__ == "__main__":
    start_server()  # 如果是直接运行,则调用启动服务器的函数
网络编程

详细解释:

  • server_socket.setsockopt(): 确保服务器重启时可以立即重新绑定到相同的端口,解决因 socket 短暂占用导致的“地址已在使用”错误。
  • accept(): 阻塞等待客户端连接,一旦有连接请求到达,它会接受连接,创建一个新的 socket 对象用于与客户端通信。
  • recv() 和 sendall(): 用于接收和发送数据。recv() 在没有数据时返回空,触发 if not data 的条件部分。据此来判断是否关闭连接。
  • try...except...finally结构: 用于处理程序中的错误,并确保即使程序出现错误或中断时,socket 连接也能正常关闭以释放系统资源。

多任务版 tcp 服务器程序开发

import socket
import threading

def handle_client(client_socket, client_address):
    print(f"连接已建立: {client_address}")
    try:
        while True:
            # 接收数据
            data = client_socket.recv(1024)
            if not data:
                # 如果没有数据,说明客户端已经关闭连接
                break
            print(f"收到来自 {client_address} 的数据: {data.decode('utf-8')}")
            
            # 回送数据给客户端
            client_socket.sendall(data)
    
    except socket.error as e:
        print(f"Socket error: {e}")
    
    finally:
        # 关闭客户端连接
        client_socket.close()
        print(f"连接已关闭: {client_address}")

def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('0.0.0.0', 8888))
    server_socket.listen(5)
    print("服务器已启动,等待连接...")

    while True:
        # 接受新的连接
        client_socket, client_address = server_socket.accept()
        
        # 为每个新客户端启动一个线程进行通信
        client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
        client_thread.start()

if __name__ == "__main__":
    start_server()
网络编程

代码解释:

  1. 创建服务器套接字:
    • 使用 socket.socket() 创建服务器套接字,AF_INET 表示使用 IPv4,SOCK_STREAM 表示使用 TCP。
  2. 绑定和监听:
    • 使用 bind() 绑定到指定的地址和端口。在这里,0.0.0.0 表示服务器监听所有可用接口上的请求。
    • 使用 listen() 进行监听,最大等待连接数量为5。
  3. 接受连接和处理:
    • 使用 accept() 接受客户端连接,该函数会阻塞,直到有客户端连接。
    • 每有一个客户端连接,就启动一个新的线程来处理客户端的输入输出。threading.Thread() 用于创建新线程。
  4. 处理客户端线程:
    • 在 handle_client() 函数中处理与单个客户端的交互:接收数据,并将数据回送给客户端。
  5. 关闭连接:
    • 当客户端断开连接或发生异常时,确保关闭套接字。

socket 之 send 和 recv 原理剖析

recv是不是直接从客户端接收数据?

不是,应用软件是无法直接通过网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区(内存中的一片空间),应用程序再从接收缓存区获取客户端发送的数据

网络编程
  • 发送数据是发送到发送缓冲区
  • 接收数据是从接收缓冲区

发布者:LJH,转发请注明出处:https://www.ljh.cool/42360.html

(0)
上一篇 2025年5月23日 上午10:32
下一篇 5天前

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注