Python-简单网络编程 I

发布于:2025-05-14 ⋅ 阅读:(11) ⋅ 点赞:(0)


一、UDP 网络程序

1. 通信结构图

创建一个基于 UDP 的网络程序流程很简单,具体步骤如下:

  • 创建一个套接字(socket.socket()

  • 服务器绑定 ip 和 port(bind()

  • 发送 / 接收数据(sendto()recvfrom()

  • 关闭套接字(close()

2. Python 代码实现

1)服务器端

import socket

# 先运行
# 创建udp socket对象,AF_INET代表IPv4,SOCK_DGRAM代表UDP
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 服务器地址(本机ip+指定端口号),用 ipconfig 命令查看本机ip地址
address = ('10.204.6.120', 8080)  # 写1024以上的端口
try:
    # 绑定地址,利用 netstat -an|grep 8080 命令查看端口是否被占用
    # 绑定后才能发送接收,绑定失败直接抛异常
    server.bind(address)
    # 接收数据,参数为缓冲区大小
    data, addr = server.recvfrom(1024)
    print(f"Received message: '{data.decode("utf-8")}' from {addr}")
    # 发送数据
    server.sendto('nice to meet you'.encode("utf-8"), addr)
except OSError:
    print("Binding failed, the port is occupied or the IP address is invalid.")
finally:
    # 关闭udp socket对象
    server.close()

2)客户端

import socket

# 后运行
# 创建客户端udp socket对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 目标服务器的IP和端口
dest_addr = ('10.204.6.120', 8080)
# 不绑定端口,系统会自动分配一个临时端口
try:
    # 发送数据
    client.sendto(b'nice to meet you too', dest_addr)
    # 打印自动分配的端口号
    print("Local socket address:", client.getsockname()[1])
    # 接收数据,设置超时防止阻塞过久
    client.settimeout(5)
    data, addr = client.recvfrom(1024)
    print(f"Received message: '{data.decode("utf-8")}' from {addr}")
except socket.timeout:
    print("No response received within timeout.")
finally:
    client.close()

3. 注意

  • 如果出现 OSError: [Errno 98] Address already in use 的情况,说明端口已被其他进程占用,因此 bind 失败,换一个端口号即可。

  • 使用 lsof 命令查看端口。

  • UDP 中 recvform 内部要接收的大小必须大于发送的字节数。否则:

    • 对于英文字符:在 Windows 下会直接报错;在 Linux 下不会报错,没有接收到的数据直接丢弃。
    • 对于中文字符:不管在 Windows 下还是 Linux 下都直接报错。
  • 对于 UDP :sendto 和 recvfrom 的次数要完全对等。

UDP 发送接收数据的特点:
每 sendto 一次,就往内核里放了一个队列结点;每 recvfrom 一次,就拿走一个队列结点,同时内核删除该结点。因此,recvfrom 的大小要大于 sendto 发送的报文大小,否则在获取时 Linux 会直接丢弃(在 Windows 下会报异常)。

  • UDP 网络程序的服务器端需要绑定 IP 地址和端口号,而客户端可以不绑定端口号。
    • 如果不显式调用绑定(bind)端口号的操作,操作系统会自动帮程序分配一个随机的 “临时端口号” ,因此,如果重新运行程序,端口可能会发生变化。
    • 如果显式调用绑定操作,指定某个固定的 IP 地址和端口号,操作系统就会把收到的发往这个端口号的 UDP 数据包交给该程序处理。

总结:不绑定端口,系统分配随机端口,端口会变;绑定端口,端口固定,程序用该端口接收数据。
换句话说,端口号是程序接收数据的 “身份标识” ,只要绑定成功,这个端口号就是程序的固定入口,其他程序或设备给该端口发的 UDP 数据都会被该程序接收。

二、TCP 网络程序

1. 通信结构图

在程序中,如果想要完成一个 TCP 服务器的功能,需要的流程如下:

  • 创建一个套接字(socket.socket()

  • 服务器绑定 ip 和 port(bind()

  • 将服务器的套接字从主动连接变为被动连接(listen()

  • 服务器等待客户端的连接(accept()

  • 客户端连接服务器(connect()

  • 接收 / 发送数据(send()recv()

  • 关闭套接字(close()

2. Python 代码实现

1)服务器端

import socket  

def tcp_server():  
    # 创建tcp socket对象,SOCK_STREAM代表TCP
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
    # 服务器地址(本机ip+端口号)  
    address = ('10.204.6.120', 8080)  # 写1024以上的端口  
    # 绑定地址,此时端口没有激活  
    server.bind(address)  
    # 服务器端开始监听,此时端口激活
    # 参数是socket可以排队的最大连接个数
    server.listen(128)  
    # 等待客户端连接,跟客户端进行后续通信的是new_client
    new_client, client_addr = server.accept()  
    # 发送数据  
    new_client.send('你好'.encode('utf-8'))  
    # 接收数据
    data = new_client.recv(1024)  
    print(f'接收到的数据:{data.decode("utf-8")}')  
    # 关闭tcp socket对象  
    new_client.close()  
    server.close()  
  
if __name__ == '__main__':  
tcp_server()

2)客户端

import socket  
  
def tcp_client():  
    # 创建客户端tcp socket对象  
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
    # 服务器地址  
    dest_addr = ('10.204.6.120', 8080)  
    # 连接服务器  
    client.connect(dest_addr)  
    # 接收数据  
    data = client.recv(1024)  
    print(f'接收到的数据:{data.decode("utf-8")}')
    # 发送数据  
    client.send('Hello'.encode('utf-8'))  
    # 关闭tcp socket对象  
    client.close()  
  
if __name__ == '__main__':  
    tcp_client()

3. 注意

  • TCP 网络程序的服务器端需要绑定 IP 地址和端口号,否则客户端找不到该服务器。

  • TCP 网络程序的客户端不需要进行绑定操作,因为需要客户端主动连接服务器,所以只要确定好服务器的 IP 地址和 Port 等信息,而本地客户端的信息可以随机。

  • TCP 服务器调用 listen() 操作将主动套接字变为被动套接字,导致套接字从 CLOSED 状态转换为 LISTEN 状态,等待接受来自其他主动套接字的连接请求。

  • 当客户端需要连接服务器时,就需要调用 connect() 操作,UDP 是直接发送的,可以不需要连接;但是 TCP 只有连接成功后才能通信。当一个 TCP 客户端连接服务器时,服务器端会产生一个新的套接字,该套接字用来标记这个客户端,单独为这个客户端服务。

listen() 操作将主动套接字变为被动套接字,用于接收客户端的连接请求;
accept() 操作返回的新套接字用于标记新客户端。

  • 关闭 listen() 后的套接字又从被动套接字变回主动套接字,会导致新的客户端不能连接服务器,但之前已经成功连接的客户端仍可正常通信。

  • 关闭 accept() 返回的套接字意味着这个客户端已经服务完毕。

  • 当客户端的套接字调用 close() 操作关闭连接后,服务器端的 recv() 解堵塞,并且返回的长度为 0,因此服务器可以通过返回数据的长度来判断客户端是否已经下线。

如果服务器之前执行 recv() 时因为套接字没有数据而阻塞(等待数据),那么当客户端关闭连接后,服务器端的 recv() 不再阻塞,会立即返回。recv() 函数返回值代表接收到的数据长度,如果返回 0,表示 “连接被关闭” ,即对端已经关闭了连接。服务器通过检查 recv() 的返回值,判断是否为 0,如果是 0,就能确定客户端已经正常关闭了连接(客户端下线)。

三、文件下载

基本协议设计:

  • 客户端(Client)send 文件名 → recv 文件内容 → 内容写入文件

  • 服务器(Server)recv 文件名 → 读文件内容 → send 文件内容

1. PyCharm 程序传参

sys.argv 是参数列表(需要 import sys),len(sys.argv) 计算参数列表的长度。

1)图形化界面传参

  • 选择需要传参的程序,点击 “编辑配置…” 。

  • 在如下图所示的位置填写需要填写的参数。例如我填写了本机的 IP 地址和一个随机的端口号,最后点击 “确定” 即可。

  • 运行下面程序可知参数传入成功。

2)命令行运行传参

打开 PyCharm 终端,运行 python [运行的程序] [传递的参数] 命令,如下图所示。

PyCharm 启动时用的是绝对路径,而在命令行执行的是相对路径。

2. Python 代码实现

1)服务器端

from socket import *
import sys

def get_file_content(file_name):  # 获取文件的内容
    try:
        with open(file_name, "rb") as f:
            content = f.read()
            f.close()
        return content
    except:
        print(f'没有名为《{file_name}》的文件')
        return None

def file_tcp_server():
    if len(sys.argv) != 3:
        print("请先在终端进行传参操作:python server.py IP地址 端口号")
        return
    else:
        ip = sys.argv[1]  # '10.204.6.120' (str)
        port = int(sys.argv[2])  # 7890 (int)
    # 创建 socket
    tcp_server_socket = socket(AF_INET, SOCK_STREAM)
    # 本地信息
    address = (ip, port)
    # 绑定本地信息
    tcp_server_socket.bind(address)
    # 128 表示等待连接的最大数量
    tcp_server_socket.listen(128)
    # 等待客户端的连接,即为这个客户端发送文件
    client_socket, client_addr = tcp_server_socket.accept()
    # 接收对方发送过来的数据
    recv_data = client_socket.recv(1024)  # 接收 1024 个字节
    file_name = recv_data.decode("utf-8")
    print("对方请求下载的文件名为: %s" % file_name)
    # 获取文件的内容
    file_content = get_file_content(file_name)
    # 发送文件的数据给客户端
    # 因为获取打开文件时是以 rb 方式打开,所以 file_content 中的数据已经是二进制的格式,因此不需要 encode 编码
    if file_content is not None:
        client_socket.send(file_content)
    # 关闭这个套接字
    client_socket.close()
    # 关闭监听套接字
    tcp_server_socket.close()

if __name__ == "__main__":
    file_tcp_server()

2)客户端

from socket import *

def file_tcp_client():
    # 创建 socket
    tcp_client_socket = socket(AF_INET, SOCK_STREAM)
    # 目的信息
    # server_ip = input("请输入服务器 ip:")
    server_ip = '10.204.6.120'
    # server_port = int(input("请输入服务器 port:"))
    server_port = 7890
    # 连接服务器
    tcp_client_socket.connect((server_ip, server_port))
    # 输入需要下载的文件名
    file_name = input("请输入要下载的文件名:")
    # 发送文件下载请求
    tcp_client_socket.send(file_name.encode("utf-8"))
    # 接收对方发送过来的数据,最大接收 1024 个字节(1K)
    recv_data = tcp_client_socket.recv(1024)
    if recv_data:
        print('接收到的数据为:', recv_data.decode('utf-8'))
    else:
        print('没有接收到数据')
    # 关闭套接字
    tcp_client_socket.close()

if __name__ == "__main__":
    file_tcp_client()

注:

  • 运行时先运行服务器端(server),再运行客户端(client)。
  • 服务器的 IP 请根据实际情况自行修改(cmd 中使用 ipconfig 命令查看),端口号也可以自行填写(2000 以上的均可)。
  • 传参在配置中修改。

四、使用 epoll 实现即时聊天

epoll 是对 select 和 poll 模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的 C/S 架构中。它的触发方式是边缘触发 / 水平触发,只适用于 Unix / Linux 操作系统,如果想在 PyCharm 上运行,需要连接远程服务器。

1. 原理与步骤

  • 创建一个 epoll 对象:
    导入 select 模块:import select
    创建一个 epoll 对象:epoll = select.epoll()

  • 让 epoll 对象在指定的 socket 上监听指定的事件:
    注册要监控的文件描述符和事件:epoll.register(文件描述符, 事件类型)

(I) 返回 epoll 的控制文件描述符:epoll.fileno()
(II) 事件类型:
可读事件 select.EPOLLIN ;
可写事件 select.EPOLLOUT ;
错误事件 select.EPOLLERR ;
客户端断开事件 select.EPOLLHUP 。

  • 轮询 epoll 对象,哪些 socket 发生了哪些指定的事件:
    epoll.poll(timeout) :当文件句柄发生变化时,会以列表的形式主动报告给用户进程

timeout 为超时时间,默认为 -1 ,即一直等待直到文件句柄发生变化;如果指定为 1 ,那么 epoll 每 1 秒汇报一次当前文件句柄的变化情况,如果无变化则返回空。

  • 在这些 socket 上执行一些操作。

  • 让 epoll 对象修改并监控 socket 列表和 / 或事件。

  • 重复以上步骤 ,直至完成。

  • 销毁 epoll 对象:
    关闭 epoll 对象的控制文件描述符:epoll.close()
    销毁文件描述符:epoll.unregister(文件描述符)

2. Python 代码实现 (Linux 下运行)

1)服务器端

# !/usr/bin/python
# -*- coding:utf-8 -*-

import socket
import select
import sys

class ChatServer:
    def __init__(self, ip, port):
        self.ip = ip
        self.port = port
        # 创建socket
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 设置端口复用
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 绑定本地信息
        self.sock.bind((self.ip, self.port))
        # 开始监听
        self.sock.listen(10)
        # 创建一个epoll对象
        self.epoll = select.epoll()

    def chat(self):
    	# 注册并监控self.sock
        self.epoll.register(self.sock.fileno(), select.EPOLLIN)
        # 注册并监控标准输入
        self.epoll.register(sys.stdin.fileno(), select.EPOLLIN)
        while True:
            events = self.epoll.poll(-1)  # 轮询注册的事件
            for fd, event in events:
                if fd == self.sock.fileno():  # 如果是socket创建的套接字
                    # 等待客户端连接
                    client_socket, client_addr = self.sock.accept()  # 接收连接
                    print("New connected by:", client_addr)
                    # 注册并监控新连接的可读事件
                    self.epoll.register(client_socket.fileno(), select.EPOLLIN)
                elif fd == sys.stdin.fileno():  # 如果是标准输入
                    # 服务器端先读标准输入
                    msg = input()
                    # 然后将标准输入发送给客户端
                    client_socket.send(msg.encode("utf-8"))
                elif fd == client_socket.fileno():  # 如果是客户端发送过来的数据
                    # 接收客户端发送过来的数据
                    data = client_socket.recv(1024).decode("utf-8")
                    if data:
                        print("Received from client:", data)
                    else:
                        print("Client has been closed.")
                        self.epoll.unregister(client_socket.fileno())  # 取消注册
                        client_socket.close()
                        break

if __name__ == "__main__":
    s = ChatServer("192.168.200.128", 8080)
    s.chat()

2)客户端

# !/usr/bin/python
# -*- coding:utf-8 -*-

import socket
import select
import sys

class ChatClient:
    def __init__(self, ip, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((ip, port))
        self.epoll = select.epoll()

    def chat_client(self):
        # 让epoll监控self.sock套接字的可读事件
        self.epoll.register(self.sock.fileno(), select.EPOLLIN)
        self.epoll.register(sys.stdin.fileno(), select.EPOLLIN)
        while True:
            events = self.epoll.poll(-1)
            for fd, event in events:
                if fd == self.sock.fileno():
                    data = self.sock.recv(1024).decode("utf-8")
                    if not data:
                        print("Server has been closed.")
                        self.epoll.unregister(self.sock.fileno())
                        self.sock.close()
                        break
                    else:
                        print("Received from server:", data)
                elif fd == sys.stdin.fileno():
                    msg = input()
                    self.sock.send(msg.encode("utf-8"))

if __name__ == "__main__":
    c = ChatClient("192.168.200.128", 8080)
    c.chat_client()

3. PyCharm 连接远程服务器步骤

1)启用相关插件

首先,确保 PyCharm 是专业版的。

步骤:

  • 打开 PyCharm ,进入 “文件 File” → “设置 Settings” → “插件 Plugins” 。

  • 在 “已安装 Installed” 标签下,确保 “部署 Deployment” 中的 “FTP/SFTP/WebDAV Connectivity” 和 “其他工具 Other Tools” 中的 “Terminal” 已勾选。如果没有这两个插件,前往 “Marketplace” 中下载。

勾选 “FTP/SFTP/WebDAV Connectivity” 可以解决 “工具 Tools” 下没有 “部署 Deployment” 的问题。

  • 如果未勾选,则勾选后需重启 PyCharm 。

2)添加 SSH 解释器

步骤:

  • 打开 PyCharm ,进入 “文件 File” → “设置 Settings” → “项目 Project: [项目名称]” → “Python 解释器 Interpreter” 。

  • 点击右上角的添加解释器 Add ,选择 “基于 SSH …” 并进行配置。
    注:确保虚拟机已开启。

主机如何填写?

打开 VMware Workstation Pro 中的虚拟机,点击虚拟机右上角的倒三角 ▼ ,选择 “网络” ,按下图所示步骤查看 IPv4 地址,将该地址复制并填入主机处。

连接 SSH 服务器的详细步骤

  • 选择 “新建” ,并填写主机和用户名信息,点击三次 “下一步” 。
    此时一定要确保虚拟机是开启状态,否则将无法成功连接 SSH 服务器。

  • 选择 “系统解释器” ,可以自行选择是否修改远程路径,最后点击 “创建” 即可。
    例如:我将远程路径改为 /home/kusunoki/PyCharm_SSH

  • 此时就可以通过右下角随时切换解释器和默认部署服务器了。
    注意在切换 SSH 解释器时需要确保虚拟机开启。


网站公告

今日签到

点亮在社区的每一天
去签到