Redis
[TOC]
CCT
本chat最終會一步步做成一個基本可用的聊天工具。我為它取了一個簡潔的名字:
chat
CCT - Command line Chat Tool
實際使用截圖如下:
本chat力求淺顯易懂,學完本chat, 相信你也能自己做一個聊天工具了~:).
Redis起源於一個的實時web log分析器 wiki. 所以一誕生就帶有"實時"性。
"實時"
Redis 把一切資料庫的內容都放在內存中。(註:現在Redis也支持配置項,可以將數據寫入磁碟)
學過計算機體系結構的同學都知道,現代基於馮.諾依曼體系的計算機,基本的數據處理一般先從磁碟等非易失介質載入到內存,再從內存載入到CPU進行處理。
磁碟載入速度一般比內存載入至少慢10倍(即使是SSD硬碟)。
10
SSD
Redis的數據全部放在內存,因而能極大的提高速度!
Redis還有其它一些優點。
NoSQL
strings, lists, maps, sets
REPL
Redis Lab
C/C++, Python, Perl
今天也有很多國內大公司用Redis做緩存資料庫,如去哪兒網,螞蟻金服,新浪微博等。
這篇chat選中Redis做聊天工具的消息隊列緩存,也是因為它把數據放內存,做消息的查詢和更改會非常快,介面也豐富,不用重新造太多太細節的技術"輪子"。
講了這麼多Redis的優點,吊起了無數胃口。所謂"百聞不如一見", 下面,請同學們跟我一起,直接玩Redis。
註: 以下代碼實踐環境均為ubuntu 16.04
sudo apt install redis
這會安裝一個Redis server 和一個Redis Client
Redis server
Redis Client
安裝好redis後,可以找到和編輯配置文件。 sudo vim /etc/redis/redis.conf
redis
sudo vim /etc/redis/redis.conf
我一般更改如下重要參數:
bind 0.0.0.0 # 允許全網的伺服器可以訪問 maxmemory <bytes> # 設定最大可使用的內存, 防止被惡意刷爆內存(為了更安全的話,也可以改變 masterauth 加一個密碼)
設置完成後,重啟一下redis-server , 方法是sudo service redis-server restart 啟動完成,如果沒有問題我們可以進入下一步
redis-server
sudo service redis-server restart
連伺服器的命令如下:
redis-cli -h <hostname> -p <port>
如果是在同一臺機器上默認port, 我們可以簡單用下面的命令:
port
redis-cli
客戶端連進去伺服器後, 會進入一個REPL的交互界面。 在這裡可以操作伺服器上的資料庫, 如下:
# redis-cli 127.0.0.1:6379>
下面的基本操作都是基於這個界面的。
< key, value >
strings
示例
127.0.0.1:6379> set key_0 value_0 OK
127.0.0.1:6379> keys * 1) "key_0"
rpush list_0 e_0 e_1 e_2
lrange list_0 0 -1
lpop list_0
此時再查看list, 發現只有兩個元素 "e_1", "e_2"了, "e_0"已經被移出了。
"e_1", "e_2"
"e_0"
Redis還有很多複雜的操作,更多可以參考:Redis Command Line
上面說的,都是如何通過在SHELL中互動式REPL去操作Redis資料庫,但是我們想要通過程序去操作。
SHELL
這個程序介面就是Redis資料庫介面。我們可以用C++, Python或者Perl等等程序。
C++, Python
Perl
此chat中,我們選用Redis-Python介面
Redis-Python
Redis-Python項目參考
Redis-Python數據安裝方法也很簡單:
pip3 install redis
注意我們這裡是用pip3. 因為我們要用Python3的資料庫介面。
pip3
Python3
安裝完成以後,我們可以驗證一下
python3 -c "import redis; print (1234)"
如果輸出正確輸出1234, 則表明redis模塊安裝OK。
1234
python
def connect_r(host, port=6379): return redis.Redis(host=host, port=port, db=0)
正確的話程序將返回一個 connection 的實例對象 con。
connection
con
import redis host = "localhost" port = 6379 con = connect_r(host, port) print(con.keys("*"))# read keys con.get("key_0") # value_0
con.set("key_0", "value_0_edited") con.get("key_0") # value_0_edited
con.rpush("list_0", "e_3") # 右邊插入一個元素 con.lpop("list_0") # 左邊移出一個無素
箭頭表示信息傳送方向 "1", "4" 箭頭是存入消息隊列,對應"rpush()"; "2", "3" 是從列隊中讀出消息,對應"lpop()";
rpush()
lpop()
對每一個人都需要建立一個消息隊列,隊列名字是如"MSG_to_Bob"
"MSG_to_Bob"
每個消息是一個list。按照到達的時間順序,從左到右依次排布。
list
如給Bob的消息隊列:
MSG_to_Bob => [msg_content_0, msg_content_1, ... ]
msg_content_0 發送時間早於 msg_content_1 , 依此類推。
msg_content_0
msg_content_1
為了讓消息內容能夠包含必要的信息,我們定義瞭如下的消息結構:
msg_pkg = { "timestamp": timestamp, "from":from_whom, "to":to_whom, "msg_body":msg_body }
一條消息,我們會包含時間信息,來源,目的地,消息體。這樣,我們就可以解釋消息的頭信息及內容。
首先我們需要把給某人的消息定義一個名字。
這樣,當某人登陸時,或者在線時,我們就能定期去查TA在Redis資料庫中對應的消息隊列,防止在一個大的消息池子裏去查找,避免了低效率的消息查找。
TA
如發給Bob的消息,我們取名為MSG_TO_bob. 取名主要是為了能快速找到給某人的消息, 而消息來自於誰,則依賴於解釋消息結構來得到。
MSG_TO_bob
給消息取了名之後,我們就能往其尾部插入消息結構。我們這裡用的是"rpush"
由於一個消息是一個Python的字典結構,而Redis資料庫並不認識這樣的結構,所以在rpush之前,我們需要做一些技術上的處理——將它們進行序列化。序列化這個詞你可能在其它地方也看到過,主要是將一個特定語言的結構體變成與語言特性無關,能夠自由存儲和解釋的一段內存,也可以簡單地理解為字元流。然後我們可以將這個字元流用rpush寫到Redis資料庫中。詳情請看附錄的子函數:
Python
rpush
序列化
字元流
def create_msg_from_whom(id_): def send_msg_to_whom(con, msg_pickle_string, msg_pkg):
def create_msg_from_whom(id_):
一旦用戶已經登陸到伺服器,我們的輪詢器會開始輪詢數據,如果TA的消息隊列中收到了給他的消息。我們就會讀取到這些消息。由於消息可以暫存在Redis中,所以我們也可以發送給某某離線的消息。
讀取消息時,如果消息列隊為空,則會返回None. 否則我們能得到一條消息。同時該消息會被從列隊中刪除。然後,把讀取到的消息反序列化一下,得到一個Python字典結構的消息。我們可以解釋得到其中的源,目的等信息。
None
在實例中, 我們只需要左邊移出"MSG_TO_bob"的消息隊列,就可以查找Bob是不是收到了新消息。 運行查找之後,如果有消息,則將消息轉發給Bob, 否則如果為空,我們就不轉發。
"MSG_TO_bob"
Bob
詳情請看附錄相關的代碼:
def check_msg_to_me(con, my_id_): pickle.loads(msg_pickle_string) def pretty_msg_pkg(msg_pkg):
def check_msg_to_me(con, my_id_):
Python2
global
trick
raw_input()
input()
可以在中斷函數中改變一個全局變數。在這裡,我們用它設置了進入用戶輸入的標誌狀態。
為什麼我們要對消息進行序列化與反序列化? 因為消息中包含了對象(時間),所以必須對消息進行序列化才能存入Redis. 序列化可以用pickle模塊
pickle
在Redis-Python編程介面中,當list為空時,lpop 會返回None. 我們只需要對返回值進行判斷,就知道是不是有新消息。
lpop
請查看項目的 CCT - github page 為了防止後期github代碼更改,導致脫節,這裡也附一份chat版代碼
github
代碼命名相對規範,讀完,你肯定豁然開朗:)
:)
#!python # readme # written by Jidor Tang <[email protected]> , 2018-12-15
# a tiny command line chat tools based redis, python3 # dependency: redis , # . run "pip3 install redis" to install
# steps # . login use an id # . CTRL+C to send msg
import redis import time import os from multiprocessing import Process
import signal import datetime import pickle import sys
### def list ### global flag_check_msg flag_check_msg = [1] # default check msg
# utils_ begin def get_timestamp(): dt = datetime.datetime.now() return dt
def chomp(id_str): return id_str.strip() # utils_ end
def signal_handler_sys_exit(sig, frame): sys.exit(1)
def signal_handler_send_msg(sig, frame): flag_check_msg[0] = 0
def login_as(): id_ = input("- login as: ") id_ = chomp(id_) assert( id_ != "") return id_
def create_msg_from_whom(id_): timestamp = get_timestamp() from_whom = id_ to_whom = "NULL" msg_body = "" to_whom = input("- send msg to whom? ") to_whom = chomp(to_whom)
print("- input your msg content, use END to end input BEGIN ")
e_l = "" while e_l != "END": msg_body += e_l + " " e_l = input("> ")
msg_pickle_string = pickle.dumps(msg_pkg) return [msg_pkg, msg_pickle_string]
def pretty_msg_pkg(msg_pkg): ret_str = "" my_id_ = msg_pkg["to"] from_ = msg_pkg["from"] msg_body = msg_pkg["msg_body"]
timestamp = msg_pkg["timestamp"] timestring = timestamp.strftime("%Y-%m-%d %H:%M") ret_str = " ------ " + timestring + " msg from " + from_ + " ------" + msg_body + " " return ret_str
def check_msg_to_me(con, my_id_): MSG_PREFIX = "MSG_TO_" msg_pickle_string = con.lpop(MSG_PREFIX + my_id_) if msg_pickle_string != None: msg_pkg = pickle.loads(msg_pickle_string) print(pretty_msg_pkg(msg_pkg)) return msg_pickle_string
def send_msg_to_whom(con, msg_pickle_string, msg_pkg): MSG_PREFIX = "MSG_TO_" con.rpush(MSG_PREFIX + msg_pkg["to"] , msg_pickle_string) return "- send msg from " + msg_pkg["from"] + " to " + msg_pkg["to"] + ", done!"
### end def ###
if __name__ == "__main__":
if len(sys.argv) == 2 and (sys.argv[1] == "-V" or sys.argv[1] == "-v"): #print (sys.argv[1]) print ("cct Command-line Chat Tools version 1.0
written by Jidor Tang<[email protected]> at 2018-12-15") sys.exit(1)
signal.signal(signal.SIGINT, signal_handler_send_msg) #signal.signal(signal.SIGQUIT, signal_handler_sys_exit)
host = "localhost" port = 6379
con = connect_r(host)
id_ = login_as() print ("login as " + id_)
cnt_check = 0 print("- press [CTRL + C] to send msg !")
while True: ck_status = None if flag_check_msg[0]: ck_status = check_msg_to_me(con, id_) else: flag_check_msg[0] = 1 cnt_check = 0 s_or_r = input(" - do you want to send msg Y(YES) | N(NO)?: ") s_or_r = chomp(s_or_r)
if s_or_r == "YES" or s_or_r == "Y" or s_or_r == "yes" or s_or_r == "y": [msg_pkg, msg_pickle_string] = create_msg_from_whom(id_) send_res = send_msg_to_whom(con, msg_pickle_string, msg_pkg) print (send_res)
if ck_status == None and cnt_check % 25 == 0: print (".", flush=True,end="")
cnt_check += 1 if cnt_check == 0.5 * 60 * 60: sys.exit(1)
time.sleep(1) con.connection_pool.disconnect()
心動不如行動, 趕緊自己可以寫一個簡單的聊天工具壓壓驚吧!
推薦閱讀: