Python之路【第十五篇】開發FTP多線程程式

来源:https://www.cnblogs.com/hackerer/archive/2019/08/15/11355665.html
-Advertisement-
Play Games

server服務端 bin下的文件 ftp_server.py conf下的文件 accounts.ini(這個可以在執行中創建) settings.py core下的文件 main.py server.py user_handle.py client客戶端 download文件是儲存下載的文件;u ...


要求:
1.用戶加密認證
2.允許同時多用戶登錄
3.每個用戶有自己的家目錄,且只能訪問自己的家目錄
4.對用戶進行磁碟配額,每個用戶的可用空間不同
5.允許用戶在ftp server上隨意切換目錄
6.允許用戶查看當前目錄下的文件
7.允許上傳和下載文件,並保證文件的一致性md5
8.文件傳輸過程中顯示進度條
9.支持文件的斷點續傳
使用:
1.啟動ftp_server.py
2.創建用戶,輸入:用戶名(預設密碼是zhurui)
3.啟動FTP伺服器
4.啟動客戶端ftp_client.py
5.輸入用戶名和密碼:alex zhurui | william zhurui
6.與伺服器server交互:

    

 

server服務端

bin下的文件 

ftp_server.py

#_*_ coding:utf-8 _*_
#Author :simon

import os
import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

sys.path.append(BASE_DIR)

from core.main import Manager

if __name__ == '__main__':
    Manager().run()

conf下的文件

accounts.ini(這個可以在執行中創建)

[william]
password = 39da56d2e7a994d38b9aaf329640fc6e
homedir = home/william
quota = 10

[zhurui]
password = 39da56d2e7a994d38b9aaf329640fc6e
homedir = home/zhurui
quota = 10

[simon]
password = 39da56d2e7a994d38b9aaf329640fc6e
homedir = home/simon
quota = 10

settings.py

#_*_ coding:utf-8 _*_
# Author:Simon
# Datetime:2019/8/14 11:00
# Software:PyCharm

import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ACCOUNTS_FILE = os.path.join(BASE_DIR, 'conf', 'accounts.ini')

HOST = '127.0.0.1'
PORT = 8080

MAX_CONCURRENT_COUNT = 10

core下的文件

main.py

# _*_ coding:utf-8 _*_
#Author:Simon

from core.user_handle import UserHandle
from core.server import Ftpserver

class Manager():
    def __init__(self):
        pass
    def start_ftp(self):
        '''啟動ftp_server端'''
        server = Ftpserver()
        server.run()
        server.close()

    def create_user(self):
        '''創建用戶'''
        username = input('請輸入要創建的用戶>:').strip()
        UserHandle(username).add_user()

    def quit_func(self):
        quit('get out...')

    def run(self):
        msg = '''\033[31;0m
        1、啟動ftp伺服器
        2、創建用戶
        3、退出\033[0m\n
        '''
        msg_dic = {'1': 'start_ftp', '2': 'create_user', '3': 'quit_func'}
        while True:
            print(msg)
            num = input('請輸入數字num>>>>:').strip()
            if num in msg_dic:
                getattr(self,msg_dic[num])()
            else:
                print('\033[1;31m請重新選擇\033[0m')

server.py

#-*- coding:utf-8 -*-
# Author:Simon
# Datetime:2019/8/13 21:02
# Software:PyCharm

import os
import socket
import struct
import pickle
import hashlib
import subprocess
import queue
from conf import settings
# from core.user_handle import UserHandle
from core.user_handle import UserHandle

from threading import Thread, Lock

class Ftpserver():
    MAX_SOCKET_LISTEN = 5
    MAX_RECV_SIZE = 8192

    def __init__(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.bind((settings.HOST, settings.PORT))
        self.socket.listen(self.MAX_SOCKET_LISTEN)

        self.q = queue.Queue(settings.MAX_CONCURRENT_COUNT) # 可以配置最大併發數

    def server_accept(self):
        '''等待client連接'''
        print('starting...')
        while True:
            self.conn,self.client_addr = self.socket.accept()
            print('客戶端地址:', self.client_addr)

            #pool.submit(self.get_recv, self.conn)
        #self.server_accept.close()
            try:
                # t = Thread(target=self.server_handle, args=(self.conn, )) #報這個錯(TypeError: server_handle() takes 1 positional argument but 2 were given)
                t = Thread(target=self.server_handle(), args=(self.conn, ))
                self.q.put(t)
                t.start()
            except Exception as e:
                print(e)
                self.conn.close()
                self.q.get()

    def get_recv(self):
        '''接收client發來的數據'''
        return pickle.loads(self.conn.recv(self.MAX_RECV_SIZE))

    def auth(self):
        '''處理用戶的認證請求
        1、根據username讀取accounts.ini文件,password相比,判斷用戶是否存在
        2、將程式運行的目錄從bin/ftp_server.py修改到用戶home/alice,方便之後查詢ls
        3、給client返回用戶的詳細信息
        '''

        while True:
            user_dic = self.get_recv()
            username = user_dic.get('username')
            user_handle = UserHandle(username)
            user_data = user_handle.judge_user()
            # 判斷用戶是否存在,返回列表
            # 如[('password','202cb962ac59075b964b07152d234b70'),('homedir','home/alex'),('quota','100')]
            if user_data:
                if user_data[0][1] == hashlib.md5(user_dic.get('password').encode('utf-8')).hexdigest(): # 密碼也相同
                    self.conn.send(struct.pack('i', 1))  #登錄成功返回
                    self.username = username
                    self.homedir_path = '%s %s %s' %(settings.BASE_DIR, 'home', self.username)
                    os.chdir(self.homedir_path) #將程式運行的目錄名修改到用戶home目錄下
                    self.quota_bytes = int(user_data[2][1]) * 1024 * 1024 #將用戶配額大小從M改到位元組
                    user_info_dic = {
                        'username': username,
                        'homedir': user_data[1][1],
                        'quota': user_data[2][1]
                    }
                    self.conn.send(pickle.dumps(user_info_dic))   #用戶的詳細信息發送到客戶端
                    return True
                else:
                    self.conn.send(struct.pack('i', 0))
            else:
                self.conn.send(struct.pack('i', 0))
    def readfile(self):
        '''讀取文件,得到文件內容的bytes型'''
        with open(self.filepath, 'rb') as f:
            filedata = f.read()
        return filedata

    def getfile_md5(self):
        '''對文件內容md5'''
        return hashlib.md5(self.readfile()).hexdigest()
    def get(self):
        '''從server下載文件到client
        '''
        if len(self.cmds) > 1:
            filename = self.cmds[1]
            filepath = os.path.join(os.getcwd(),filename)  #os.getcwd()得到當前工作目錄
            if os.path.isfile(filepath):  #判斷文件是否存在
                exist_file_size = struct.unpack('i', self.conn.recv(4))[0]
                self.filepath = filepath
                header_dic = {
                    'filename': filename,
                    'file_md5': self.getfile_md5(),
                    'file_size': os.path.getsize(self.filepath)
                }
                header_bytes = pickle.dumps(header_dic)
                if exist_file_size:  #表示之前被下載過一部分
                    self.conn.send(struct.pack('i', len(header_bytes)))
                    self.conn.send(header_bytes)
                    if exist_file_size != os.path.getsize(self.filepath):
                        with open(self.filepath, 'rb') as f:
                            f.seek(exist_file_size)
                            for line in f:
                                self.conn.send(line)
                    else:
                        print('斷電和文件本身大小一樣')
                else:    #文件第一次下載
                    self.conn.send(struct.pack('i', len(header_bytes)))
                    self.conn.send(header_bytes)
                    with open(self.filepath, 'rb') as  f:
                        for line in f:
                            self.conn.send(line)
            else:
                print('當前目錄下文件不存在')
                self.conn.send(struct.pack('i',0))
        else:
            print('用戶沒用輸入文件名')
    def recursion_file(self,menu):
        '''遞歸查詢用戶home/alice目錄下的所有文件,算出文件的大小'''
        res = os.listdir(menu) #指定目錄下所有的文件和目錄名
        for i in res:
            path = '%s %s' % (menu, i)
            if os.path.isdir(path): #判斷指定對象是否為目錄
                self.recursion_file(path)
            elif os.path.isfile(path):
                self.home_bytes_size += os.path.getsize(path)

    def current_home_size(self):
        '''得到當前用戶home/alice目錄的大小,位元組/M'''
        self.home_bytes_size = 0
        self.recursion_file(self.homedir_path)
        print('位元組:', self.home_bytes_size)  # 單位是位元組
        home_m_size = round(self.home_bytes_size / 1024 /1024, 1)
        print('單位M:', home_m_size)  #單位是: M

    def put(self):
        '''從client上傳文件到server當前工作目錄下'''
        if len(self.cmds) > 1:
            state_size = struct.unpack('i',self.conn.recv(4))[0]
            if state_size:
                self.current_home_size()  #算出了home下已被占用的大小self.home_bytes_size
                header_bytes = self.conn.recv(struct.unpack('i', self.conn.recv(4))[0])
                header_dic = pickle.loads(header_bytes)
                print(header_dic)
                filename = header_dic.get('filename')
                file_size = header_dic.get('file_size')
                file_md5 = header_dic.get('file_md5')

                upload_filepath = os.path.join(os.getcwd(), filename)
                self.filepath = upload_filepath  #為了全局變數讀取文件算md5時方便
                if os.path.exists(upload_filepath): #文件已經存在
                    self.conn.send(struct.pack('i', 1))
                    has_size = os.path.getsize(upload_filepath)
                    if has_size == file_size:
                        print('文件已經存在')
                        self.conn.send(struct.pack('i', 0))
                    else: #上次沒有傳完,接著繼續傳
                        self.conn.send(struct.pack('i', 1))
                        if self.home_bytes_size + int(file_size - has_size) > self.quota_bytes:
                            print('超出了用戶的配額')
                            self.conn.send(struct.pack('i', 0))
                        else:
                            self.conn.send(struct.pack('i',1))
                            self.conn.send(struct.pack('i', has_size))
                            with open(upload_filepath, 'ab') as f:
                                f.seek(has_size)
                                while has_size < file_size:
                                    recv_bytes = self.conn.recv(self.MAX_RECV_SIZE)
                                    f.write(recv_bytes)
                                    has_size += len(recv_bytes)
                                    self.conn.send(struct.pack('i', has_size))  #為了顯示進度條
                            if self.getfile_md5() == file_md5:  #判斷下載下來的文件MD5值和server傳過來的MD5值是否一致
                                print('\033[1;32m上傳成功\033[0m')
                                self.conn.send(struct.pack('i', 1))
                            else:
                                print('\033[1;32m上傳失敗\033[0m')
                                self.conn.send(struct.pack('i', 0))
                else: #第一次上傳
                    self.conn.send(struct.pack('i', 0))
                    if self.home_bytes_size + int(file_size) > self.quota_bytes:
                        print('\033[1;32m超出了用戶的配額\033[0m')
                        self.conn.send(struct.pack('i', 0))
                    else:
                        self.conn.send(struct.pack('i', 1))
                        with open(upload_filepath, 'wb') as f:
                            recv_size = 0
                            while recv_size < file_size:
                                file_bytes = self.conn.recv(self.MAX_RECV_SIZE)
                                f.write(file_bytes)
                                recv_size += len(file_bytes)
                                self.conn.send(struct.pack('i', recv_size)) #為了進度條的顯示

                        if self.getfile_md5() == file_md5:  #判斷下載下來的文件MD5值和server傳過來的MD5值是否一致
                            print('\033[1;32m上傳成功\033[0m')
                            self.conn.send(struct.pack('i', 1))
                        else:
                            print('\033[1;32m上傳失敗\033[0m')
                            self.conn.send(struct.pack('i', 0))
            else:
                print('待傳的文件不存在')
        else:
            print('用戶沒有輸入文件名')

    def ls(self):
        '''查詢當前工作目錄下,先返迴文件列表的大小,再返回查詢的結果'''
        subpro_obj = subprocess.Popen('dir', shell=True,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
        stdout = subpro_obj.stdout.read()
        stderr = subpro_obj.stderr.read()
        self.conn.send(struct.pack('i', len(stdout + stderr)))
        self.conn.send(stdout)
        self.conn.send(stderr)

    def mkdir(self):
        '''在當前目錄下,增加目錄'''
        if len(self.cmds) > 1:
            mkdir_path = os.path.join(os.getcwd(),self.cmds[1])
            if not os.path.exists(mkdir_path): #查看目錄名是否存在
                os.mkdir(mkdir_path)
                print('增加目錄成功')
                self.conn.send(struct.pack('i', 1)) #增加目錄成功,返回1
            else:
                print('目錄名已存在')
                self.conn.send(struct.pack('i', 0))  #失敗返回0
        else:
            print('用戶沒有輸入目錄名')

    def cd(self):
        '''切換目錄'''
        if len(self.cmds) > 1:
            dir_path = os.path.join(os.getcwd(), self.cmds[1])
            if os.path.isdir(dir_path) :#查看是否是目錄名
                previous_path = os.getcwd() #拿到當前工作的目錄
                os.chdir(dir_path) #改變工作目錄到 . . .
                target_dir = os.getcwd()
                if self.homedir_path in target_dir: #判斷homedir_path是否在目標目錄
                    print('切換成功')
                    self.conn.send(struct.pack('i', 1)) #切換成功返回1
                else:
                    print('切換失敗')   #切換失敗後,返回到之前的目錄下
                    os.chdir(previous_path)
                    self.conn.send(struct.pack('i', 0))
            else:
                print('要切換的目錄不在該目錄下')
                self.conn.send(struct.pack('i', 0))
        else:
            print('沒有傳入切換的目錄名')

    def remove(self):
        '''刪除指定的文件,或者空文件夾'''
        if len(self.cmds) > 1:
            file_name = self.cmds[1]
            file_path = '%s\%s' %(os.getcwd(), file_name)
            if os.path.isfile(file_path):
                os.remove(file_path)
                self.conn.send(struct.pack('i', 1))
            elif os.path.isdir(file_path):  #刪除空目錄
                if not len(os.listdir(file_path)):
                    os.removedirs(file_path)
                    print('刪除成功')
                    self.conn.send(struct.pack('i', 1))
                else:
                    print('文件夾非空,不能刪除')
                    self.conn.send(struct.pack('i', 0))
            else:
                print('不是文件也不是文件夾')
                self.conn.send(struct.pack('i', 0))
        else:
            print('沒有輸入要刪除的文件')

    def server_handle(self):
        '''處理與用戶的交互指令'''
        if self.auth():
            print('\033[1;32m用戶登陸成功\033[0m')
            while True:
                try:    #try ...except 適合windows client斷開
                    user_input = self.conn.recv(self.MAX_RECV_SIZE).decode('utf-8')
                    # if not user_input: continue #這裡適合 linux client斷開
                    self.cmds = user_input.split()
                    if hasattr(self,self.cmds[0]):
                        getattr(self,self.cmds[0])()
                    else:
                        print('\033[1;31請用戶重覆輸入\033[0m')
                except Exception:
                    break

    def run(self):
        self.server_accept()

    def  close(self):
        self.socket.close()

#if __name__ == '__main__':
    #pool = ThreadPoolExecutor(10)

user_handle.py

#_*_ coding:utf-8 _*_
# Author:Simon
# Datetime:2019/8/14 10:26
# Software:PyCharm

import configparser
import hashlib
import os

from conf import settings

class UserHandle():
    def __init__(self,username):
        self.username= username
        self.config = configparser.ConfigParser() #先生成一個對象
        self.config.read(settings.ACCOUNTS_FILE)
    @property
    def password(self):
        '''生成用戶的預設密碼 zhurui'''
        return hashlib.md5('zhurui'.encode('utf-8')).hexdigest()
    @property

    def quota(self):
        '''生成每個用戶的磁碟配額'''
        quota = input('請輸入用戶的磁碟配額大小>>>:').strip()
        if quota.isdigit():
            return quota
        else:
            exit('\033[1;31m磁碟配額必須是整數\033[0m')

    def add_user(self):
        '''創建用戶,存到accounts.ini'''
        if not self.config.has_section(self.username):
            print('creating username is : ', self.username)
            self.config.add_section(self.username)
            self.config.set(self.username, 'password', self.password)
            self.config.set(self.username, 'homedir', 'home/'+self.username)
            self.config.set(self.username, 'quota', self.quota)
            with open(settings.ACCOUNTS_FILE, 'w') as f:
                self.config.write(f)
            os.mkdir(os.path.join(settings.BASE_DIR, 'home', self.username)) #創建用戶的home文件夾
            print('\033[1;32m創建用戶成功\033[0m')
        else:
            print('\033[1;32m用戶已存在\033[0m')


    def judge_user(self):
        '''判斷用戶是否存在'''
        if self.config.has_section(self.username):
            return self.config.items(self.username)
        else:
            return

client客戶端

download文件是儲存下載的文件;upload是上傳文件的儲存庫(download裡邊可以不放東西,等待下載即可;upload裡邊放你準備上傳給服務端的文件)

ftp_client.py

#_*_ coding:utf-8 _*_
# Author:Simon
# Datetime:2019/8/14 11:12
# Software:PyCharm

import os
import sys
import socket
import struct
import pickle
import hashlib

class Ftpclient():
    HOST = '127.0.0.1'  #伺服器IP
    PORT = 8080 #服務端的埠
    MAX_RECV_SIZE = 8192
    DOWNLOAD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'download')
    UPLOAD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'upload')

    def __init__(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect()

    def connect(self):
        '''連接服務端server'''
        try:
            self.socket.connect((self.HOST, self.PORT))
        except Exception:
            exit('\033[1;31mserver還未啟動\033[0m')

    def get_recv(self):
        '''獲取server返回的數據'''
        return pickle.loads(self.socket.recv(self.MAX_RECV_SIZE))

    def auth(self):
        '''用戶認證'''
        count = 0
        while count < 3:
            name = input('請輸入用戶名>>:').strip()
            if not name: continue
            password = input('請輸入密碼>>:').strip()
            user_dic = {
                'username':name,
                'password':password
            }
            self.socket.send(pickle.dumps(user_dic))   #把用戶名和密碼發送給server
            res = struct.unpack('i',self.socket.recv(4))[0]
            if res:   #接收返回的信息,並判斷
                print('welcome'.center(20,'-'))
                user_info_dic = self.get_recv()
                self.username = user_info_dic.get('username')
                print(user_info_dic)
                return True
            else:
                print('\033[1;31m用戶名或者密碼不對!\033[0m')
            count += 1

    def readfile(self):
        '''讀取文件,得到的文件內容的bytes型'''
        with open(self.filepath, 'rb') as f:
            filedata = f.read()
        return filedata

    def getfile_md5(self):
        '''對文件內容md5'''
        return hashlib.md5(self.readfile()).hexdigest()

    def progress_bar(self, num, get_size, file_size):
        '''進度條顯示'''
        float_rate = get_size / file_size
        # rate = str(float_rate * 100)[:5]  #  95.85%
        rate = round(float_rate * 100,2)   # 95.85%

        if num == 1: #1表示下載
            sys.stdout.write('\r已下載:\033[1;32m{0}%\033[0m'.format(rate))
        elif num == 2:   #2 表示上傳
            sys.stdout.write('\r已上傳:\033[1;32m{0}%\033[0m	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 關於樹的數據展示,前後用過兩個插件,一是zTree,二是jsTree,無論是提供的例子(可下載),還是提供的API在查找時的便捷程度,zTree比jsTree強多了,也很容易上手,所以這裡只講下jsTree的使用 官網:https://www.jstree.com 中文API文檔:https: ...
  • 工廠函數,顧名思義,就是通過一個"工廠的加工" 來創建一個函數 這種操作在需要創建多個相似對象時可以有效地減少重覆代碼,但是這樣有個缺點就是,每次調用工廠函數創建的對象都是獨立的object,不存在繼承關係,顯然,這樣的面向對象編程失去了靈魂 於是, 對象構造函數就出現了 使用構造函數有幾個要註意的 ...
  • 1、使用input透明覆蓋法 將input的z-index設置為1以上的數字並覆蓋到需點擊的內容上,將input的樣式opacity設置為0(即為透明度為0),這樣通過綁定在input上的change事件觸發 推薦 2、使用vue的ref參數直接操作input的點擊事件觸發 3、使用HTML的lab ...
  • 最終頁面顯示效果為 主頁面 parent.vue 子頁面child.vue有兩種方法 第一種 第二種 這是兩個最簡單的例子 參考鏈接 https://cn.vuejs.org/v2/guide/render-function.html ...
  • 在電腦科學中,圖是一種網路結構的抽象模型,它是一組由邊連接的頂點組成。一個圖G = (V, E)由以下元素組成: V:一組頂點 E:一組邊,連接V中的頂點 下圖表示了一個圖的結構: 在介紹如何用JavaScript實現圖之前,我們先介紹一些和圖相關的術語。 如上圖所示,由一條邊連接在一起的頂點稱為 ...
  • 深入淺出一致性Hash原理 ...
  • 文章轉載自:http://www.pythonheidong.com/blog/article/3303/ 一、數據結構與演算法基礎 · 說一下幾種常見的排序演算法和分別的複雜度。 · 用Java寫一個冒泡排序演算法 · 描述一下鏈式存儲結構。 · 如何遍歷一棵二叉樹? · 倒排一個LinkedList。 ...
  • 文章轉載自:http://www.pythonheidong.com/blog/article/3009/ 熱點隨筆: · [譯]Web設計者和開發者必備的28個Chrome插件(JK_Rush)· 程式員裝B指南(查一把)· javascript 設計模式 - 文章很長,請自備瓜子,水果和眼藥水( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...