自己動手做聊天工具(基於 Redis 開發)

[TOC]

聊天工具CCT使用實例

chat最終會一步步做成一個基本可用的聊天工具。我為它取了一個簡潔的名字:

CCT - Command line Chat Tool

實際使用截圖如下:

chat力求淺顯易懂,學完本chat, 相信你也能自己做一個聊天工具了~:).

Redis 簡介及配置

簡介

Redis起源於一個的實時web log分析器 wiki. 所以一誕生就帶有"實時"性。

Redis 把一切資料庫的內容都放在內存中。(註:現在Redis也支持配置項,可以將數據寫入磁碟)

學過計算機體系結構的同學都知道,現代基於馮.諾依曼體系的計算機,基本的數據處理一般先從磁碟等非易失介質載入到內存,再從內存載入到CPU進行處理。

磁碟載入速度一般比內存載入至少慢10倍(即使是SSD硬碟)。

Redis的數據全部放在內存,因而能極大的提高速度!

Redis還有其它一些優點。

  • Redis是一種 NoSQL 的非關係型資料庫,意味著你可以往裡面存入各種不同的數據結構,如strings, lists, maps, sets等。這也給編程帶來極大的自由。

  • Redis配置簡單, 幾乎是傻瓜式地拿來即用。 伺服器可以一行命令配置。 客戶端自帶REPL。 可以互動式查看資料庫中存儲的內容。 由於發展的比較早,現在也有專門的組織Redis Lab維護,易用性也做得非常不錯。客戶端介面包含各種語言的介面C/C++, Python, Perl等;

今天也有很多國內大公司用Redis做緩存資料庫,如去哪兒網,螞蟻金服,新浪微博等。

這篇chat選中Redis做聊天工具的消息隊列緩存,也是因為它把數據放內存,做消息的查詢和更改會非常快,介面也豐富,不用重新造太多太細節的技術"輪子"。

講了這麼多Redis的優點,吊起了無數胃口。所謂"百聞不如一見", 下面,請同學們跟我一起,直接玩Redis

註: 以下代碼實踐環境均為ubuntu 16.04

安裝Redis

sudo apt install redis

這會安裝一個Redis server 和一個Redis Client

配置server

安裝好redis後,可以找到和編輯配置文件。 sudo vim /etc/redis/redis.conf

我一般更改如下重要參數:

bind 0.0.0.0 # 允許全網的伺服器可以訪問
maxmemory <bytes> # 設定最大可使用的內存, 防止被惡意刷爆內存(為了更安全的話,也可以改變 masterauth 加一個密碼)

設置完成後,重啟一下redis-server , 方法是sudo service redis-server restart 啟動完成,如果沒有問題我們可以進入下一步

客戶端連結伺服器

連伺服器的命令如下:

redis-cli -h <hostname> -p <port>

如果是在同一臺機器上默認port, 我們可以簡單用下面的命令:

redis-cli

客戶端連進去伺服器後, 會進入一個REPL的交互界面。 在這裡可以操作伺服器上的資料庫, 如下:

# redis-cli
127.0.0.1:6379>

下面的基本操作都是基於這個界面的。

Redis 基本操作

常用操作示例

  • 建一個< key, value >strings鍵值對

示例

127.0.0.1:6379> set key_0 value_0
OK

  • 查看所有的資料庫元素

示例

127.0.0.1:6379> keys *
1) "key_0"

  • 新建一個list

示例

rpush list_0 e_0 e_1 e_2

  • 查看一個list

示例

lrange list_0 0 -1

  • 左邊移出list的一個元素

示例

lpop list_0

此時再查看list, 發現只有兩個元素 "e_1", "e_2"了, "e_0"已經被移出了。

Redis還有很多複雜的操作,更多可以參考:Redis Command Line

Redis-Python 資料庫介面

上面說的,都是如何通過在SHELL中互動式REPL去操作Redis資料庫,但是我們想要通過程序去操作。

這個程序介面就是Redis資料庫介面。我們可以用C++, Python或者Perl等等程序。

chat中,我們選用Redis-Python介面

Redis-Python項目參考

Redis-Python數據安裝方法也很簡單:

pip3 install redis

注意我們這裡是用pip3. 因為我們要用Python3的資料庫介面。

安裝完成以後,我們可以驗證一下

python3 -c "import redis; print (1234)"

如果輸出正確輸出1234, 則表明redis模塊安裝OK。

python代碼連伺服器

def connect_r(host, port=6379):
return redis.Redis(host=host, port=port, db=0)

正確的話程序將返回一個 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()";

建立消息隊列

對每一個人都需要建立一個消息隊列,隊列名字是如"MSG_to_Bob"

每個消息是一個list。按照到達的時間順序,從左到右依次排布。

如給Bob的消息隊列:

MSG_to_Bob => [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
}

一條消息,我們會包含時間信息,來源,目的地,消息體。這樣,我們就可以解釋消息的頭信息及內容。

如何將新消息放入消息隊列

首先我們需要把給某人的消息定義一個名字。

這樣,當某人登陸時,或者在線時,我們就能定期去查TARedis資料庫中對應的消息隊列,防止在一個大的消息池子裏去查找,避免了低效率的消息查找。

如發給Bob的消息,我們取名為MSG_TO_bob. 取名主要是為了能快速找到給某人的消息, 而消息來自於誰,則依賴於解釋消息結構來得到。

給消息取了名之後,我們就能往其尾部插入消息結構。我們這裡用的是"rpush"

由於一個消息是一個Python的字典結構,而Redis資料庫並不認識這樣的結構,所以在rpush之前,我們需要做一些技術上的處理——將它們進行序列化序列化這個詞你可能在其它地方也看到過,主要是將一個特定語言的結構體變成與語言特性無關,能夠自由存儲和解釋的一段內存,也可以簡單地理解為字元流。然後我們可以將這個字元流rpush寫到Redis資料庫中。詳情請看附錄的子函數:

def create_msg_from_whom(id_):

def send_msg_to_whom(con, msg_pickle_string, msg_pkg):

如何讀取,刪除和反序列化消息

一旦用戶已經登陸到伺服器,我們的輪詢器會開始輪詢數據,如果TA的消息隊列中收到了給他的消息。我們就會讀取到這些消息。由於消息可以暫存在Redis中,所以我們也可以發送給某某離線的消息。

讀取消息時,如果消息列隊為空,則會返回None. 否則我們能得到一條消息。同時該消息會被從列隊中刪除。然後,把讀取到的消息反序列化一下,得到一個Python字典結構的消息。我們可以解釋得到其中的源,目的等信息。

在實例中, 我們只需要左邊移出"MSG_TO_bob"的消息隊列,就可以查找Bob是不是收到了新消息。 運行查找之後,如果有消息,則將消息轉發給Bob, 否則如果為空,我們就不轉發。

詳情請看附錄相關的代碼:

def check_msg_to_me(con, my_id_):

pickle.loads(msg_pickle_string) def pretty_msg_pkg(msg_pkg):

代碼編寫心得

python2 vs python3 細微差別

  • global 變數 Python2global 簡單變數可讀寫,但到了Python3中,global簡單變數能讀不能寫。所以在程序中我用了一個數組. 算是一個小trick
  • input vs raw_input Python2中輸入是raw_input(), 而到了Python3中變成了input()

signal in python3

可以在中斷函數中改變一個全局變數。在這裡,我們用它設置了進入用戶輸入的標誌狀態。

對消息對象的序列化與反序列化

為什麼我們要對消息進行序列化與反序列化? 因為消息中包含了對象(時間),所以必須對消息進行序列化才能存入Redis. 序列化可以用pickle模塊

如果消息隊列中沒有消息了?

Redis-Python編程介面中,當list為空時,lpop 會返回None. 我們只需要對返回值進行判斷,就知道是不是有新消息。

CCT其它思考及改進

  • 寫消息的互斥與同步
  • 如何評估此聊天工具的效率
  • 加入註冊功能,加入密碼用於驗證身份

完整代碼

請查看項目的 CCT - github page 為了防止後期github代碼更改,導致脫節,這裡也附一份chat版代碼

代碼命名相對規範,讀完,你肯定豁然開朗:)

#!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 connect_r(host, port=6379):
return redis.Redis(host=host, port=port, db=0)

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_pkg = {
"timestamp": timestamp,
"from":from_whom,
"to":to_whom,
"msg_body":msg_body
}

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()

心動不如行動, 趕緊自己可以寫一個簡單的聊天工具壓壓驚吧!

推薦閱讀:

相關文章