主頁 > 軟體設計 > 把酒言歡話聊天,基于Vue3.0+Tornado6.1+Redis發布訂閱(pubsub)模式打造異步非阻塞(aioredis)實時(websocket)通信聊天系統

把酒言歡話聊天,基于Vue3.0+Tornado6.1+Redis發布訂閱(pubsub)模式打造異步非阻塞(aioredis)實時(websocket)通信聊天系統

2021-12-23 09:41:32 軟體設計

原文轉載自「劉悅的技術博客」https://v3u.cn/a_id_202

“表達欲”是人類成長史上的強大“源動力”,恩格斯早就直截了當地指出,處在蒙昧時代即低級階段的人類,“以果實、堅果、根作為食物;音節清晰的語言的產生是這一時期的主要成就”,而在網路時代人們的表達欲往往更容易被滿足,因為有聊天軟體的存在,通常意義上,聊天大抵都基于兩種形式:群聊和單聊,群聊或者群組聊天我們可以理解為聊天室,可以有人數上限,而單聊則可以認為是上限為2個人的特殊聊天室,

為了開發高質量的聊天系統,開發者應該具備客戶機和服務器如何通信的基本知識,在聊天系統中,客戶端可以是移動應用程式(C端)或web應用程式(B端),客戶端之間不直接通信,相反,每個客戶端都連接到一個聊天服務,該服務支撐雙方通信的功能,所以該服務在業務上必須支持的最基本功能:

1.能夠實時接收來自其他客戶端的資訊,

2.能夠將每條資訊實時推送給收件人,

當客戶端打算啟動聊天時,它會使用一個或多個網路協議連接聊天服務,對于聊天服務,網路協議的選擇至關重要,這里,我們選擇Tornado框架內置Websocket協議的介面,簡單而又方便,安裝tornado6.1

pip3 install tornado==6.1

隨后撰寫程式啟動檔案main.py:

import tornado.httpserver  
import tornado.websocket  
  
import tornado.ioloop  
  
import tornado.web  
  
import redis  
  
import threading  
  
import asyncio  
  
# 用戶串列  
users = []  
  
# websocket協議  
class WB(tornado.websocket.WebSocketHandler):  
  
  
	# 跨域支持  
	def check_origin(self,origin):  
  
		return True  
  
	# 開啟鏈接  
	def open(self):  
  
                users.append(self)  
  
  
	# 接收訊息  
	def on_message(self,message):  
  
		self.write_message(message['data'])  
  
	# 斷開  
	def on_close(self):  
  
		users.remove(self)

# 建立torando實體  
  
app = tornado.web.Application(  
  
	[  
  
	(r'/wb/',WB)  
  
	],debug=True  
  
)  
  
if __name__ == '__main__':  
  
  
	# 宣告服務器  
	http_server_1 = tornado.httpserver.HTTPServer(app)  
  
	# 監聽埠  
	http_server_1.listen(8000)  
  
	# 開啟事件回圈  
	tornado.ioloop.IOLoop.instance().start() 

如此,就在短時間搭建起了一套websocket協議服務,每一次有客戶端發起websocket連接請求,我們都會將它添加到用戶串列中,等待用戶的推送或者接收資訊的動作,

下面我們需要通過某種形式將訊息的發送方和接收方聯系起來,以達到“聊天”的目的,這里選擇Redis的發布訂閱模式(pubsub),以一個demo來實體說明,server.py

import redis  
  
r = redis.Redis()  
r.publish("test",'hello')

隨后撰寫 client.py:

import redis  
r = redis.Redis()  
ps = r.pubsub()  
ps.subscribe('test')    
for item in ps.listen():   
    if item['type'] == 'message':  
        print(item['data'])

可以這么理解:訂閱者(listener)負責訂閱頻道(channel);發送者(publisher)負責向頻道(channel)發送二進制的字串訊息,然后頻道收到訊息時,推送給訂閱者,

頻道不僅可以聯系發布者和訂閱者,同時,也可以利用頻道進行“訊息隔離”,即不同頻道的訊息只會給訂閱該頻道的用戶進行推送:

根據發布者訂閱者邏輯,改寫main.py:

import tornado.httpserver  
import tornado.websocket  
  
import tornado.ioloop  
  
import tornado.web  
  
import redis  
  
import threading  
  
import asyncio  
  
# 用戶串列  
users = []  
  
# 頻道串列  
channels = ["channel_1","channel_2"]  
  
  
# websocket協議  
class WB(tornado.websocket.WebSocketHandler):  
  
  
	# 跨域支持  
	def check_origin(self,origin):  
  
		return True  
  
	# 開啟鏈接  
	def open(self):  
  
  
		users.append(self)  
  
  
	# 接收訊息  
	def on_message(self,message):  
  
		self.write_message(message['data'])  
  
	# 斷開  
	def on_close(self):  
  
		users.remove(self)  
  
  
  
  
  
  
# 基于redis監聽發布者發布訊息  
def redis_listener(loop):  
  
	asyncio.set_event_loop(loop)  
  
	async def listen():   
  
		r = redis.Redis(decode_responses=True)  
  
		# 宣告pubsb實體  
		ps = r.pubsub()  
  
		# 訂閱聊天室頻道  
  
		ps.subscribe(["channel_1","channel_2"])  
  
  
		# 監聽訊息  
		for message in ps.listen():  
  
			print(message)  
  
			# 遍歷鏈接上的用戶  
			for user in users:  
  
				print(user)  
  
				if message["type"] == "message" and message["channel"] == user.get_cookie("channel"):  
  
  
					user.write_message(message["data"])  
  
	future = asyncio.gather(listen())  
	loop.run_until_complete(future)  
  
  
  
# 介面  發布資訊  
class Msg(tornado.web.RequestHandler):  
  
  
	# 重寫父類方法  
	def set_default_headers(self):  
  
		# 設定請求頭資訊  
		print("開始設定")  
		# 域名資訊  
		self.set_header("Access-Control-Allow-Origin","*")  
		# 請求資訊  
		self.set_header("Access-Control-Allow-Headers","x-requested-with")  
		# 請求方式  
		self.set_header("Access-Control-Allow-Methods","POST,GET,PUT,DELETE")  
  
	  
  
	# 發布資訊  
	async def post(self):  
  
		data = self.get_argument("data",None)  
  
		channel = self.get_argument("channel","channel_1")  
  
		print(data)  
  
		# 發布  
		r = redis.Redis()  
  
		r.publish(channel,data)  
  
		return self.write("ok")  
  
  
# 建立torando實體  
  
app = tornado.web.Application(  
  
	[  
  
	(r'/send/',Msg),  
	(r'/wb/',WB)  
  
	],debug=True  
  
)  
  
if __name__ == '__main__':  
  
  
	loop = asyncio.new_event_loop()  
  
	# 單執行緒啟動訂閱者服務  
	threading.Thread(target=redis_listener,args=(loop,)).start()  
  
  
	# 宣告服務器  
	http_server_1 = tornado.httpserver.HTTPServer(app)  
  
	# 監聽埠  
	http_server_1.listen(8000)  
  
	# 開啟事件回圈  
	tornado.ioloop.IOLoop.instance().start()

這里假設默認有兩個頻道,邏輯是這樣的:由前端控制websocket鏈接用戶選擇將訊息發布到那個頻道上,同時每個用戶通過前端cookie的設定具備頻道屬性,當具備頻道屬性的用戶對該頻道發布了一條訊息之后,所有其他具備該頻道屬性的用戶通過redis進行訂閱后主動推送剛剛發布的訊息,而頻道的推送只匹配訂閱該頻道的用戶,達到訊息隔離的目的,

需要注意的一點是,通過執行緒啟動redis訂閱服務時,需要將當前的loop實體傳遞給協程物件,否則在訂閱方法內將會獲取不到websocket實體,報這個錯誤:

IOLoop.current() doesn't work in non-main

這是因為Tornado底層基于事件回圈ioloop,而同步框架模式的Django或者Flask則沒有這個問題,

下面撰寫前端代碼,這里我們使用時下最流行的vue3.0框架,撰寫chat.vue:

<template>  
  <div>  
  
  
            <h1>聊天視窗</h1>  
  
  
            <van-tabs v-model:active="active" @click="change_channel">  
  
              <van-tab title="客服1號">  
  
  
                <table>  
                
              <tr v-for="item,index in msglist" :key="index">  
                  
                {{ item }}  
  
              </tr>  
  
            </table>  
                  
  
  
              </van-tab>  
  
  
              <van-tab title="客服2號">  
                  
  
                <table>  
                
              <tr v-for="item,index in msglist" :key="index">  
                  
                {{ item }}  
  
              </tr>  
  
            </table>  
  
  
              </van-tab>  
  
            </van-tabs>  
  
  
              
  
  
            <van-field label="聊天資訊" v-model="msg" />  
  
            <van-button color="gray" @click="commit">發送</van-button>  
  
     
  </div>  
</template>  
  
<script>  
  
export default {  
 data() {  
    return {  
      auditlist:[],  
  
      //聊天記錄  
      msglist:[],  
      msg:"",  
       websock: null, //建立的連接  
      lockReconnect: false, //是否真正建立連接  
      timeout: 3 * 1000, //30秒一次心跳  
      timeoutObj: null, //外層心跳倒計時  
      serverTimeoutObj: null, //內層心跳檢測  
      timeoutnum: null, //斷開 重連倒計時  
      active:0,  
      channel:"channel_1"  
       
    }  
  },  
  methods:{  
  
  
    //切換頻道  
    change_channel:function(){  
  
  
          if(this.active === 0){  
  
  
                this.channel = "channel_1";  
  
                var name = "channel";  
          var value = "channel_1";  
  
            
  
          }else{  
  
  
              this.channel = "channel_2";  
  
                var name = "channel";  
          var value = "channel_2";  
  
  
          }  
  
  
          //清空聊天記錄  
          this.msglist = [];  
  
  
          var d = new Date();  
          d.setTime(d.getTime() + (24 * 60 * 60 * 1000));  
          var expires = "expires=" + d.toGMTString();  
          document.cookie = name + "=" + value + "; " + expires;  
  
  
          this.reconnect();  
  
  
    },  
     initWebSocket() {  
      //初始化weosocket  
      const wsuri = "ws://localhost:8000/wb/";  
      this.websock = new WebSocket(wsuri);  
      this.websock.onopen = this.websocketonopen;  
      this.websock.onmessage = this.websocketonmessage;  
      this.websock.onerror = this.websocketonerror;  
      this.websock.onclose = this.websocketclose;  
    },  
  
    reconnect() {  
      //重新連接  
      var that = this;  
      if (that.lockReconnect) {  
        // 是否真正建立連接  
        return;  
      }  
      that.lockReconnect = true;  
      //沒連接上會一直重連,設定延遲避免請求過多  
      that.timeoutnum && clearTimeout(that.timeoutnum);  
      // 如果到了這里斷開重連的倒計時還有值的話就清除掉  
      that.timeoutnum = setTimeout(function() {  
        //然后新連接  
        that.initWebSocket();  
        that.lockReconnect = false;  
      }, 5000);  
    },  
  
     reset() {  
      //重置心跳  
      var that = this;  
      //清除時間(清除內外兩個心跳計時)  
      clearTimeout(that.timeoutObj);  
      clearTimeout(that.serverTimeoutObj);  
      //重啟心跳  
      that.start();  
    },  
  
    start() {  
      //開啟心跳  
      var self = this;  
      self.timeoutObj && clearTimeout(self.timeoutObj);  
      // 如果外層心跳倒計時存在的話,清除掉  
      self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);  
      // 如果內層心跳檢測倒計時存在的話,清除掉  
      self.timeoutObj = setTimeout(function() {  
        // 重新賦值重新發送 進行心跳檢測  
        //這里發送一個心跳,后端收到后,回傳一個心跳訊息,  
        if (self.websock.readyState == 1) {  
          //如果連接正常  
          // self.websock.send("heartCheck");  
        } else {  
          //否則重連  
          self.reconnect();  
        }  
        self.serverTimeoutObj = setTimeout(function() {  
          // 在三秒一次的心跳檢測中如果某個值3秒沒回應就關掉這次連接  
          //超時關閉  
         // self.websock.close();  
        }, self.timeout);  
      }, self.timeout);  
      // 3s一次  
    },  
  
    websocketonopen(e) {  
      //連接建立之后執行send方法發送資料  
      console.log("成功");  
  
     // this.websock.send("123");  
      // this.websocketsend(JSON.stringify(actions));  
    },  
    websocketonerror() {  
      //連接建立失敗重連  
      console.log("失敗");  
      this.initWebSocket();  
    },  
    websocketonmessage(e) {  
  
      console.log(e);  
      //資料接收  
      //const redata = JSON.parse(e.data);  
      const redata = e.data;  
  
      //累加  
      this.msglist.push(redata);  
  
      console.log(redata);  
  
       
    },  
    websocketsend(Data) {  
      //資料發送  
      this.websock.send(Data);  
    },  
    websocketclose(e) {  
      //關閉  
      this.reconnect()  
      console.log("斷開連接", e);  
    },  
  
    //提交表單  
    commit:function(){  
  
  
        //發送請求  
  
        this.myaxios("http://localhost:8000/send/","post",{"data":this.msg,channel:this.channel}).then(data =>{  
  
          console.log(data);  
  
        });  
  
  
  
    },  
    
  
  },  
  
  mounted(){  
  
  
      //連接后端websocket服務  
      this.initWebSocket();  
  
  
  
      var d = new Date();  
          d.setTime(d.getTime() + (24 * 60 * 60 * 1000));  
          var expires = "expires=" + d.toGMTString();  
          document.cookie = "channel" + "=" + "channel_1" + "; " + expires;  
  
      
  
  }  
  
}  
</script>  
  
  
<style scoped>  
  @import url("../assets/style.css");  
  
  .chatbox{  
  
      color:black;  
  
  }  
  
  .mymsg{  
  
      background-color:green;  
  
  }  
  
  
</style>

這里前端在線客戶端定期向狀態服務器發送心跳事件,如果服務端在特定時間內(例如x秒)從客戶端接收到心跳事件,則認為用戶處于聯機狀態,否則,它將處于脫機狀態,脫機后在閾值時間內可以進行重新連接的動作,同時利用vant框架的標簽頁可以同步切換頻道,切換后將頻道標識寫入cookie,便于后端服務識別后匹配推送,

效果是這樣的:

誠然,功能業已實作,但是如果我們處在一個高并發場景之下呢?試想一下如果一個頻道有10萬人同時在線,每秒有100條新訊息,那么后臺tornado的websocket服務推送頻率是100w*10/s = 1000w/s ,

這樣的系統架構如果不做負載均衡的話,很難抗住壓力,那么瓶頸在哪里呢?沒錯,就是資料庫redis,這里我們需要異步redis庫aioredis的幫助:

pip3 install aioredis

aioredis通過協程異步操作redis讀寫,避免了io阻塞問題,使訊息的發布和訂閱操作非阻塞,

此時,可以新建一個異步訂閱服務檔案main_with_aioredis.py:

import asyncio  
import aioredis  
from tornado import web, websocket  
from tornado.ioloop import IOLoop  
import tornado.httpserver  
import async_timeout

之后主要的修改邏輯是,通過aioredis異步建立redis鏈接,并且異步訂閱多個頻道,隨后通過原生協程的asyncio.create_task方法(也可以使用asyncio.ensure_future)注冊訂閱消費的異步任務reader:

async def setup():  
    r = await aioredis.from_url("redis://localhost", decode_responses=True)  
    pubsub = r.pubsub()  
  
    print(pubsub)  
    await pubsub.subscribe("channel_1","channel_2")  
  
    #asyncio.ensure_future(reader(pubsub))  
    asyncio.create_task(reader(pubsub))

在訂閱消費方法中,異步監聽所訂閱頻道中的發布資訊,同時和之前的同步方法一樣,比對用戶的頻道屬性并且進行按頻道推送:

async def reader(channel: aioredis.client.PubSub):  
    while True:  
        try:  
            async with async_timeout.timeout(1):  
                message = await channel.get_message(ignore_subscribe_messages=True)  
                if message is not None:  
                    print(f"(Reader) Message Received: {message}")  
  
                    for user in users:  
  
                        if user.get_cookie("channel") == message["channel"]:  
  
                            user.write_message(message["data"])  
          
                await asyncio.sleep(0.01)  
        except asyncio.TimeoutError:  
            pass

最后,利用tornado事件回圈IOLoop傳遞中執行回呼方法,將setup方法加入到事件回呼中:

if __name__ == '__main__':  
  
    # 監聽埠  
    application.listen(8000)  
  
    loop = IOLoop.current()  
    loop.add_callback(setup)  
    loop.start()

完整的異步訊息發布、訂閱、推送服務改造 main_aioredis.py:

import asyncio  
import aioredis  
from tornado import web, websocket  
from tornado.ioloop import IOLoop  
import tornado.httpserver  
import async_timeout  
  
users = []  
  
# websocket協議  
class WB(tornado.websocket.WebSocketHandler):  
  
  
    # 跨域支持  
    def check_origin(self,origin):  
  
        return True  
  
    # 開啟鏈接  
    def open(self):  
  
  
        users.append(self)  
  
  
    # 接收訊息  
    def on_message(self,message):  
  
        self.write_message(message['data'])  
  
    # 斷開  
    def on_close(self):  
  
        users.remove(self)  
  
  
class Msg(web.RequestHandler):  
  
  
    # 重寫父類方法  
    def set_default_headers(self):  
  
        # 設定請求頭資訊  
        print("開始設定")  
        # 域名資訊  
        self.set_header("Access-Control-Allow-Origin","*")  
        # 請求資訊  
        self.set_header("Access-Control-Allow-Headers","x-requested-with")  
        # 請求方式  
        self.set_header("Access-Control-Allow-Methods","POST,GET,PUT,DELETE")  
  
  
    # 發布資訊  
    async def post(self):  
  
        data = self.get_argument("data",None)  
  
        channel = self.get_argument("channel","channel_1")  
  
        print(data)  
  
        # 發布  
        r = await aioredis.from_url("redis://localhost", decode_responses=True)  
  
        await r.publish(channel,data)  
  
        return self.write("ok")  
  
  
async def reader(channel: aioredis.client.PubSub):  
    while True:  
        try:  
            async with async_timeout.timeout(1):  
                message = await channel.get_message(ignore_subscribe_messages=True)  
                if message is not None:  
                    print(f"(Reader) Message Received: {message}")  
  
                    for user in users:  
  
                        if user.get_cookie("channel") == message["channel"]:  
  
                            user.write_message(message["data"])  
          
                await asyncio.sleep(0.01)  
        except asyncio.TimeoutError:  
            pass  
  
  
async def setup():  
    r = await aioredis.from_url("redis://localhost", decode_responses=True)  
    pubsub = r.pubsub()  
  
    print(pubsub)  
    await pubsub.subscribe("channel_1","channel_2")  
  
    #asyncio.ensure_future(reader(pubsub))  
    asyncio.create_task(reader(pubsub))  
  
  
application = web.Application([  
    (r'/send/',Msg),  
    (r'/wb/', WB),  
],debug=True)      
  
  
if __name__ == '__main__':  
  
    # 監聽埠  
    application.listen(8000)  
  
    loop = IOLoop.current()  
    loop.add_callback(setup)  
    loop.start()

從程式設計角度上講,充分利用了協程的異步執行思想,更加地絲滑流暢,

結語:實踐操作來看,Redis發布訂閱模式,非常契合這種實時(websocket)通信聊天系統的場景,但是發布的訊息如果沒有對應的頻道或者消費者,訊息則會被丟棄,假如我們在生產環境在消費的時候,突然斷網,導致其中一個訂閱者掛掉了一段時間,那么當它重新連接上的時候,中間這一段時間產生的訊息也將不會存在,所以如果想要保證系統的健壯性,還需要其他服務來設計高可用的實時存盤方案,不過那就是另外一個故事了,最后奉上專案地址,與眾鄉親同饗:https://github.com/zcxey2911/tornado_redis_vue3_chatroom

原文轉載自「劉悅的技術博客」 https://v3u.cn/a_id_202

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/390623.html

標籤:其他

上一篇:Cadence Allegro走線包地打孔-走包地孔圖文教程及視頻演示

下一篇:[ 網路協議篇 ] vlan 詳解之 GVRP 詳解

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more