type
status
date
slug
summary
tags
category
icon
password

计算机网络大作业项目报告

摘要

基于Socket类库使用Python语言完成了具有GUI的多用户聊天程序,采用C-S架构,支持私聊与图片收发功能。

原理分析

Client-Server架构

客户端-服务器架构是一个基本的计算模型,其中客户端从服务器请求服务,而服务器处理这些请求并向客户端提供服务。在聊天室的应用程序中,希望支持许多客户的实时交谈。客户端软件将向聊天室服务器发送消息,聊天室服务器将向所有其他连接的客户端广播我们的消息。

网络协议

聊天室中使用的网络通信基于协议堆栈(在更简单、更基本的对话之上构建更高层次、更复杂的对话。)计算机网络协议堆栈可以用OSI模型和TCP/IP模型表示。每层对应一组特定于层的网络协议。考虑到应用程序的目的,我们可以忽略一些底层的协议(如物理层),而只讨论其中与聊天功能相关的协议。
TCP和UDP是传输层协议——它们控制数据如何从一个点发送到另一个点。我们正在构建TCP之上,这意味着我们不需要关心数据如何发送,只需要关心数据发送的内容和地点。
TCP和UDP的主要区别在于,TCP保证了可靠的交付,没有任何信息丢失、重复或过序。UDP不保证相同,并由应用程序层来处理丢弃的数据包。它要求服务器确认收到数据。

Socket套接字

在计算机通信领域,socket是计算机中进程面向网络通信与进程间通信的API,由传输层提供给应用层调用,IP作为网络层主机的身份标识,port为主机中通信端点的唯一身份标识,通过协议、IP,port三元组构成的socket address是网络中一个socket的唯一身份标识。 在一个通信网络里,终端中运行的应用程序想要互相进行信息交流需要遵守一定规范,这个规范即在这里即为socket。它规定了程序如何请求互联网基础设施传输数据到指定的运行在另一个终端上的目标程序,同时也作为信息交流规范使信息可以有效收发。

多线程

利用Thread模块创建接收消息的线程,以实现同时接收和发送消息。
多线程允许程序同时执行多个任务。在聊天室中,每个用户连接通常会分配一个独立的线程,负责处理该用户的消息发送和接收,以及其他与该用户相关的操作。同时用多线程可以实现非阻塞的操作。如果一个用户的操作(比如发送消息)需要一些时间,其他用户的操作仍然可以在其他线程中进行,而不会被阻塞。
考虑到聊天室的应用,聊天室通常是事件驱动的,即响应用户的消息或其他事件。每个线程可以独立地等待和处理特定的事件,使得整个聊天室能够同时处理多个事件而不阻塞。

应用设计

功能设计

经过设计,一个完整的聊天室应用包括以下功能:

网络通信

该应用采用客户端-服务器架构,通过TCP协议进行通信。服务器使用socket模块监听客户端连接,而客户端通过套接字连接到服务器,实现实时消息传输。

身份验证

应用提供用户身份验证功能,包括注册和登录。用户可以选择登录已有账户或注册新账户,通过服务器验证用户身份。

命令处理

客户端支持一些简单的命令,如发送图片、发送系统信息和私聊。 总体而言,该聊天程序采用了客户端-服务器架构,通过TCP协议进行通信,实现了基本的聊天功能,包括文字消息、图片消息、在线用户列表和系统提示。

图片处理 (PIL 模块)

应用使用PIL模块处理接收到的图片数据。当用户发送图片时,服务器将图片数据传输给其他在线用户,接收方通过PIL的ImageTk模块将图片显示在聊天窗口中。

异常处理

使用try-except块来捕获异常,例如连接失败、接收消息错误等情况。这样可以提高应用的稳定性和容错性,确保用户体验更加流畅。

线程管理 (Thread 模块)

通过Thread模块实现多线程,分别用于接收消息和处理客户端连接。在应用中,使用多线程的主要原因是为了处理两个关键任务:

接受信息

使用单独的线程来接收从服务器发送过来的消息,以确保在等待消息时不会阻塞主线程,从而保持用户界面的响应性。
在receive方法中,通过循环不断接收消息,以及使用try-except块捕获异常,确保即使在网络通信中发生问题时,程序也能够继续运行。

处理客户端连接

使用单独的线程来处理客户端的连接,以便能够同时接受多个客户端的连接请求。
在accept_client_connection方法中,通过在循环中等待客户端的连接请求,为每个新连接的客户端创建一个新线程,用于处理该客户端的消息和操作。

必要性

如果不使用多线程,而是在主线程中进行网络通信和客户端连接的处理,可能会导致以下问题:
  • 界面阻塞: 每当进行网络通信或等待客户端连接时,主线程会被阻塞,导致用户界面无法响应用户的输入,用户体验变差。
  • 无法同时处理多个任务: 单线程无法同时处理多个任务,因此在等待一个任务完成时,其他任务会被阻塞。
  • 消息延迟: 在接收消息时,如果在主线程中同步处理消息,可能会导致消息的延迟,用户无法及时看到新的消息

信息流传输

消息通过JSON格式进行编码和解码,其中Head包含消息的ID、内容和类型等信息,Content存储实际的消息内容。这种设计使得信息传输更加灵活和可扩展。
notion image

实现方法

通信模块

应用程序中通信模块的建立的示例如下:
首先,Server会创建一个socket.socket对象。socket接受两个参数:地址族和套接字类型。AF_INET地址系列用于IP网络。SOCK_STREAM套接字类型用于可靠的流控制数据流,例如TCP提供的数据流。另一方面,UDP需要基于数据包的套接字类型SOCK_DGRAM。
接下来,我们设置SO_REUSEADDR选项。此选项允许服务器在旧连接关闭后使用相同的端口(通常必须等待几分钟)。然后,我们使用bind()将socket对象绑定到服务器机器上的套接字地址。bind()以元组格式定义。考虑到一台机器可以有许多外部IP接口,可以使用ifconfig命令来确定IP接口。
在单一主机测试中,由于客户端与服务器都隶属于同一主机,因此可以使用一个特殊的IP来进行通信。如下面的ifconfig返回结果所示:
notion image
lo0接口,是是环回接口,只能通过在同一台机器上运行的其他程序访问。它的IP地址为127.0.0.1,仅在自己的机器中具有意义。它的主机名为“localhost”,并可以提供一个安全的测试环境。
最后,Server使用listen()来设置监听。TCP使用两种类型的套接字:监听套接字和连接套接字。在套接字上调用listen()之后,它成为一个侦听套接字,并且只能通过握手促进建立TCP连接,而不是实际的数据传输。每当客户端连接时,Server都需要创建一个全新的套接字,以便发送和接收数据。

保密模块

考虑到用户密码是机密信息,在用户登陆和注册时使用md5加密方法进行加密传输,增强应用的安全性

客户端 (client)

界面设计 (init_ui 方法)

使用tkinter创建图形用户界面,包括消息显示框、消息输入框、在线用户列表框、发送按钮等组件。考虑到篇幅所限,在这里不展示这部分代码的全部内容。 需要注意的是,考虑到应用程序在关闭GUI窗口时应该直接退出进程,在设计GUI时使用了下述代码来确保关闭按钮的有效性
在这里,receive_thread.daemon = True 将receive_thread设置为守护线程。守护线程是在程序退出时会被强制终止的线程。如果没有将线程设置为守护线程,程序将等待所有非守护线程结束后才会退出。通过将接收消息的线程设置为守护线程,可以确保在主线程结束时,这个线程也会被强制终止,而不会等待它完成。

通信交互 (send_message 和 receive 方法)

send_message 方法负责将用户输入的消息发送到服务器,通过socket发送消息,Client可发送信息按照客户需求分为以下类型:
  • 聊天室信息
    • 最基础的信息类型,任何不加前缀的信息都会归类到聊天室信息处理,经由Server广播到所有的Client中
  • 图片信息
    • 如果用户输入的消息以 /image 开头,表示用户想要发送图片。在这种情况下,从消息中提取图片路径,将图片文件读取为字节流,并使用 base64 进行编码。然后,通过两次 self.s.send 发送两个消息,一个用于通知Server用户发送了一张图片,另一个包含实际的图片数据。
  • 私聊信息
    • 如果用户输入的消息以 /pm 开头,表示用户想要发送私密消息。在这种情况下,从消息中提取收件人和消息内容,并通过 self.s.send 发送给服务器。同时,还检查收件人是否在线,如果不在线则弹出错误提示。
可以通过在发送内容中手动添加前缀来确定本次发送信息的类型,send_message在获取输入后会进行预处理,对于不同类型的数据进行不同类型的json封装,再传输到Server。
receive 方法负责接收服务器发送的消息,根据消息类型分别处理文字消息、图片消息、在线用户列表和系统提示消息。

安全模块 (authenticate_window 和 authenticate 方法)

authenticate_window 方法创建身份验证窗口,用户可以选择登录或注册。 authenticate 方法处理用户身份验证,通过socket向服务器发送用户信息,并根据服务器返回的验证结果决定是否创建客户端窗口。

消息编码 (message_code 方法)

message_code 方法负责将消息内容、类型和用户ID编码为JSON格式的字符串,方便在网络上传输。具体代码如下:
通过这样的编码方式可以为Client与Server之间的数据传递提供满足信息流传输需求的功能。

服务器 (server)

参数传递

利用argparse封装的方法提供Server的初始化参数接口
  • host IP
  • port 端口
  • CLIENT_NUM:允许接入最多client数量

初始化 (init 方法)

如之前给出的Server的init方法代码所述,初始化阶段执行的内容包括:创建服务器socket,使用TCP协议进行通信,绑定主机和端口,并设置允许重用地址。 启动服务器,监听客户端连接请求。同时,在初始化时Server类也会维护一个clients集合数据结构,

客户端连接接受 (accept_client_connection 方法)

Server创建一个无限循环来监听新的Client连接。accept()调用将等待新Client连接,当它连接时,创建一个新的线程服务于新的Client,并且进入目标函数handle_clients以处理Client信息。

用户数据库 (DATABASE_FILE 和 user_database)

使用JSON文件存储用户信息,包括用户名和加密后的密码。

客户端处理 (handle_clients 方法)

handle_clients的前半部分以客户段身份验证为主,即验证循环 (while AUTH_FLAG == False): 通过 conn.recv(1024) 接收客户端发送的数据,并使用 json.loads 解析为字典。 提取身份验证类型 auth_type,如果是注册则调用 self.handle_register(data) 处理,如果是登录则调用 self.handle_login(data) 处理。 如果身份验证成功(flag 为 True),发送一条成功消息给客户端,将客户端添加到 self.clients 字典中,并广播客户端加入的消息,然后更新在线用户列表。 如果身份验证失败,发送一条失败消息给客户端。 这个循环会一直运行,直到身份验证成功。

信息收发模块 (handle_clients 方法)

处理客户端连接,包括用户身份验证、注册、登录、消息的发送和接收。 通过broadcast方法向所有客户端广播消息,通过broadcast_online_users方法更新在线用户列表。
我们再次创建一个无限循环。这一次,我们不是监听新的连接,而是监听客户端发送的数据。
当调用recv()时,它将等待数据到达。如果没有可用的数据,recv()将不会返回(它“阻止”),程序会暂停,直到数据到达。像accept()和recv()这样的调用使程序等到一些新数据到达,允许它返回,被称为阻塞调用。数据通过网络作为字节字符串发送和接收,因此需要分别使用encode()和decode()进行编码和解码。

消息广播 (broadcast 方法)

将消息发送给所有连接到服务器的客户端。事实上执行的内容是遍历此时与Server连接的所有Client,并一一单播。

更新在线用户列表 (broadcast_online_users 方法)

将在线用户列表广播给所有客户端。使用服务器维护的clients数据,每次发生用户登入或登出,就重新广播,更新所有client中维护的在线用户数据。

消息编码 (message_code 方法)

类似于客户端的message_code方法,用于将消息内容、类型和用户ID编码为JSON格式的字符串。

实例展示

网络连接页面

notion image

身份验证页面

notion image

客户端GUI

notion image

多客户端视图

notion image
notion image

图片发送

notion image

私聊

notion image
notion image

用户登出

notion image

心得体会

通过这个项目,我学到了如何使用TCP协议进行网络通信,通过Socket套接字实现客户端和服务器的连接,利用多线程实现并发处理,以及如何处理消息的编码和解码。同时,使用了Tkinter和PIL模块设计了简单的图形用户界面和图片处理功能,并加入了一系列异常处理机制。经过试错和学习,我对计算机网络有了更深刻的理解。
Respeaker V2Broadcast