项目实战:《怎么用Python 做一款带用户界面的本地通讯软件》第二节:简单网络通讯模型。

前言

上一期,用pyqt5 写了用户登录及注册界面:
用Python做一款带用户界面本地通讯桌面软件 (一:用户登录与注册)
并且用本地文件操作,模拟了用户登录和注册的业务逻辑,本期主要介绍将用户登录和注册加上网络通信模块。

1、模块分析

这里我们主要分以下2个模块:
1、客户端 Client 模块:主要存放用户操作界面代码,负责用户操作,接收服务端返回数据,呈现给客户。
2、服务端 Service 模块:主要存放服务端代码,负责接收处理客户请求,并返回数据给客户端。

其他的文件分析:
Client 模块下的temp文件夹:保存用户基本的资料数据,如聊天记录等。
data 文件夹:因没用数据库,所以data文件夹可以看做是服务端数据保存路径。
image文件夹:保存客户端界面用到的图片。
项目目录

2、客户端功能及代码:

本次客户端相比于上期内容基本上没有多大变化,只是在上期基础上引入了负责与服务端通讯的client.py文件,修改了do_login用户登录方法及注册界面的register_func用户注册方法。

2.1 client.py

与服务端通讯:

'''
客户端通讯
'''
from socket import *

# 获取服务器地址:
with open('setting.txt','r') as f:
    data = f.readlines()
    address = (data[0].strip(),int(data[1]))

class Client(object):
    def __init__(self):
        # 创建套接字对象
        self.sock_fd = socket(AF_INET,SOCK_STREAM)
        # 连接服务器
        self.sock_fd.connect(address)

    # 发送请求,并接收服务端返回数据
    def send_receive(self,message):
        self.sock_fd.send(message.encode())
        data = self.sock_fd.recv(2048)
        return data.decode()

这里使用TCP套接字来进行通讯。

# 获取服务器地址:
with open('setting.txt','r') as f:
    data = f.readlines()
    address = (data[0].strip(),int(data[1]))

setting 文件作为客户端通讯地址文件,方便后期客户端在其他地址登录时修改。

以下为setting.txt文件内容:

127.0.0.1
8888

客户端通讯模块的引入:

from client import Client

# 用户登录页面
class LoginPage(QWidget):
    '''用户注册,登录窗口'''
    # 创建客户端以进行连接
    try:
        client = Client()
    except:
        pass

2.2 LoginPage类中do_login方法改写

	# 登录方法
    # 在登录方法中,检查用户设置的登录状态
    def do_login(self):
        # 获取用户输入的用户名及密码
        name,password = self.name_line.text(),self.password_line.text()
        # 向服务端发送登录请求
        method = 'LG'
        # 构建消息内容
        message = '%s@%s?%s' % (method,name,password)
        # 发送消息,并获得返回数据
        try:
            response = self.client.send_receive(message)
        except:
            QMessageBox.warning(self,'警告','服务器启动失败!')
            return

        # 根据服务器返回信号,处理登录逻辑
        if response == 'OK':
            self.check_login_state()
            QMessageBox.information(self, '提示', '登录成功!')
        elif response == 'None':
            QMessageBox.warning(self, '警告', '暂无用户数据!请先注册!')
        elif response == 'NME':
            QMessageBox.warning(self, '警告', '用户不存在!')
        elif response == 'PDE':
            QMessageBox.warning(self, '警告', '密码错误!')

解析:
自定义登录请求头为’LG‘并与登录请求的数据(用户名,密码)用’@‘连接发送给服务端,服务端接收到信息,根据请求头的不同处理相应请求。

用户名及密码匹配成功,返回 ’OK‘,客户端接收到’OK‘则提示登录成功,并执行相应的其他业务逻辑。
返回 ’None‘ 说明暂无用户数据,客户端发出警告提示框。提示:‘暂无用户数据!请先注册!’。
其他逻辑类似。

注意:
客户端与服务器可能会连接失败,如:服务端没有启动,所以发送请求的时候加了个try,except语句。

        try:
            response = self.client.send_receive(message)
        except:
            QMessageBox.warning(self,'警告','服务器启动失败!')
            return

2.3 LoginPage类中do_register方法的改写

将创建的通讯对象,传递给注册页面:

    # 用户注册方法
    def do_register(self):
        # 调用注册界面
        register_page = RegisterPage(self.client)
        # 注册页面的注册成功信号绑定,在登录页面输入注册成功后的用户ID及密码
        register_page.successful_signal.connect(self.successful_func)
        register_page.exec()

使注册页面可以和服务器进行通讯。

2.3 RegisterPage类中register_func的改写

 	# 用户注册方法
    def register_func(self):
        # 注册界面数据
        # 用户性别数据
        gender = self.gender_data()
        # 注意将性别的整数类型,转化为字符串类型
        rt_data = [self.name_line.text(), self.password1_line.text(),
            self.nick_line.text(),str(gender)]
        # 构造发送注册请求数据
        method = 'RT'
        data = '?'.join(rt_data)
        message = '%s@%s' % (method,data)
        response = self.client.send_receive(message)
        # 根据服务器返回信号,处理注册逻辑
        if response == 'NME':
            QMessageBox.information(self, '提示', '该用户ID已被注册!')
        elif response == 'OK':
            choice = QMessageBox.information(self, '提示',
                '注册成功,是否登录?',QMessageBox.Yes | QMessageBox.No)
            # 如选择是,关闭注册页面,并在登录页面用户ID显示注册ID,密码
            if choice == QMessageBox.Yes:
                self.successful_signal.emit([self.name_line.text(),
                                             self.password1_line.text()])
                self.close()
            # 如选择否,直接关闭注册页面。
            else:
                self.close()

解析:
注册请求的请求头为 ‘RT’,将请求头与注册的数据发送给服务器。让服务器完成注册的业务,并返回相应的信息。

3、服务端功能及代码

3.1 服务端主要代码:

'''
本地通讯服务端
'''
from socket import *
from multiprocessing import Process
import sys

# 导入处理客户端简单请求自定义模块
from function import SimpleFun

server_ip = '0.0.0.0'
server_port = 8888
server_ar = (server_ip,server_port)


# 创建服务器类
class Service(object):
    simple = SimpleFun()
    def __init__(self):
    	# 创建套接字,流式套接字
        self.sock_fd = socket(AF_INET, SOCK_STREAM)
        self.sock_fd.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        # 绑定服务器地址
        self.sock_fd.bind(server_ar)

    # 启动服务器方法,创建多进程来处理多个客户端的连接
    def service_forever(self):
        print('服务器已启动!')
        #  设置监听数
        self.sock_fd.listen(10)
        while True:
            try:
                # 连接对象,连接对象的IP地址
                con, ar = self.sock_fd.accept()
                print('连接对象IP:', ar)
            except:
                continue
            # 创建进程
            handle_client = Process(target=self.handle_client, args=(con,))
            # 启动进程
            handle_client.start()

    # 与客户端交互方法
    def handle_client(self,con):
        while True:
            # 接收客户端消息
            message = con.recv(2048)
            # 处理客户端异常关闭
            if not message:
                break
            # 解析消息
            request = message.decode().split('@')
            # 拆分消息请求方法,数据
            method, data = request[0],request[1]
            # 处理请求,并返回数据
            response = self.hand_request(method,data)
            # 向客户端返回数据
            con.send(response.encode())
        # 处理异常关闭
        con.close()
        sys.exit()

    # 处理客户端请求方法
    def hand_request(self,method,data):
        # 处理登录请求
        if method == 'LG':
            # 调用登录方法
            response = self.simple.login(data)
            return response
        # 处理注册请求
        elif method == 'RT':
            # 调用注册方法
            response = self.simple.register(data)
            return response


# 启动客户端
if __name__ == '__main__':
    S = Service()
    S.service_forever()

解析:
1、先创建套接字。
2、创建服务器启动方法: service_forever
这里用了多进程来处理客户端信息,用了Python的Process模块。不了解的同学可以自己网上搜索下资料简单的了解一下,这里用的也很简单。
3、服务端处理登录请求,注册请求方法:

    # 处理客户端请求方法
    def hand_request(self,method,data):
        # 处理登录请求
        if method == 'LG':
            # 调用登录方法
            response = self.simple.login(data)
            return response
        # 处理注册请求
        elif method == 'RT':
            # 调用注册方法
            response = self.simple.register(data)
            return response

根据请求头的不同,服务端执行相应的代码,这里我们处理登录和注册的函数放在function.py文件中:

# 导入处理客户端简单请求自定义模块
from function import SimpleFun

3.2 服务端的登录方法及注册方法

登录方法:

'''
处理客户端简单请求方法
'''
import pickle

class SimpleFun(object):
    def __init__(self):
        pass

    # 处理登录请求
    def login(self,data):
        # 拆分接收到的数据
        name,password = data.split('?')
        try:
            # 用Python自带模块pickle来进行文件读取操作,以rb方式读取文件
            with open('./data/users.pkl', 'rb') as f:
                users = pickle.load(f)
        except:
            # 无注册数据返回值
            return 'None'

        if name in users.keys():
            if password == users[name][0]:
                # 登录成功返回值
                return 'OK'
            else:
                # 密码错误返回值 password error
                return 'PDE'
        else:
            # 用户名错误返回值 name error
            return 'NME'

解析:
根据客户端发送过来的数据,来执行相应的数据判断。

主要注意信息的处理方式,客户端在构造登录请求的数据时,请求头与请求数据用‘@’连接,用户名与密码之间用‘?’连接,所以这里也用相应的方式分割数据。

		# 拆分接收到的数据
        name,password = data.split('?')

注册方法:

    # 处理注册请求
    def register(self,data):
        ID = data.split('?')[0]
        try:
            with open('./data/users.pkl', 'rb') as f1:
                users = pickle.load(f1)
        except:
            users = {}
            # 如果用户ID已存在,提示用户ID已被注册
        if ID in users.keys():
            # 返回用户ID已存在信号
            return 'NME'
            # 否则收集用户注册信息
        else:
            user_data = data.split('?')[1:]
            users[ID] = user_data
            with open('./data/users.pkl', 'wb') as f2:
                pickle.dump(users, f2)
            # 返回注册成功信号
            return 'OK'

数据处理方面的逻辑也和登录方法类似,代码也比较简单,应该不需要解释啥。

4 下篇预告

本次主要建立起了简单的通讯框架,下一篇应该主要会是登录后聊天界面的创建

5 最后

这次模块多了一点,全部源码贴上来意义也不大。还是那句话,要源码的可以私信。

以上内容为原创,未经允许,禁止转载!!

猜你喜欢

转载自blog.csdn.net/zhouz92/article/details/106650173