較為原生的WebSocket服務端

不洗碗工作室發表於2019-02-20
  1. 概念

    • 個人理解它是客戶端和服務端之間的通訊通道
    • 確定唯一一個socket(套接字)的屬性需要4個
      • 服務端IP,服務端Port,客戶端IP,客戶端Port
    • 通過這4個屬性不難在腦袋裡抽象出通道的概念,兩端分別是通道的入口和出口
  2. 函式解釋(python import socket)

    1. 建立socket(基礎socket,寫明協議編號,socket型別等,不必深究)
      • s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    2. 服務端
      • s.bind(address) address:(host,port)
      • s.listen(TCP連線數量限制)
      • s.accept() 接受客戶端TCP連線並返回(conn,address),conn是建立好的socket物件,也就是一個完整的已知入口出口的通道,address是與上文address同格式的客戶端地址
    3. 客戶端
      • s.connect(address)建立連線,錯誤時返回socket.error
    4. 公共函式
      • s.recv(bufsize[,flag]) 接受管道傳來的資訊,bufsize指定接收的最大資料量,flag提供有關訊息的其他資訊,通常可以忽略。
      • s.send(string[,flag]) 傳送資料,將string中的資料傳送到連線的套接字。返回值是要傳送的位元組數量,該數量可能小於string的位元組大小,也就是說管子不夠大,不能將資料一次性傳出。
      • s.close() 關閉socket
  3. socket建立分析 先分析一下哪些程式碼是堵塞的

    • s.accept()等不到就堵著
    • s.send()肯定要有等待輸入的資料變數,沒有資料就還得堵著唄
    • s.recv()接不到就堵著

    啊,好煩,習慣單程式的我真是醉了,這讓人咋寫! (╯‵□′)╯︵┻━┻ 首先我們要先分析一下我們要建立什麼樣的對話

    1:1對話

    • 程式碼互動大概是這個流程
      • server:(省略s=s.socket.socket())
        • s.bind->s.listen->s.accept 好,到這裡堵住,等待連線到來
      • client:
        • s.connect()
      • 建立連線,server端從s.accept()得到返回值conn通道物件與client_adress,然後我們存起來
      • 現在開始我們的資料傳輸,寫web的時候,從來都是client攻,server受,這回逆轉一下!(๑•̀ㅂ•́)و✧
      • server:
        • while 1: msg=input(意思意思,就是接受終端輸入) s.send(msg)
      • client:
        • while 1: msg=s.resv() print msg //可能這就是虛擬碼吧 _(:з」∠)_
      • 這樣一來,我們就可以在服務端瘋狂輸出,然後客戶端就可以列印出我們傳遞的資訊了
    • 爽過之後我們再想,可是這樣只能由服務端單方面訪♂問客戶端,客戶端連點反應都沒有,沒意思,可是兩個人都在那堵的不亦樂乎,該怎麼辦呢.....
    • 別想了,一個程式肯定不夠用
    • 看如下程式碼
    # Server.py
    import socket
    import sys,os
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.bind(("127.0.0.1",8000))
    s.listen(5)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    conn,address=s.accept()
    #開啟子程式,找兒子幫忙
    pid=os.fork()
    if(pid>0):
        #讀取輸入,傳送給client
        while 1:
            msg=sys.stdin.readline()
            if msg == "quit":
                sys.exit()
            conn.send(bytes(msg,encoding="utf-8"))
    else:
        while 1:
            log_file=open('./client_input.log','a')
            msg=conn.recv(1024) 
            print ("client:"+msg.decode('utf-8'))
            log_file.write(msg.decode('utf-8'))
            if msg == "bye":
                log_file.close()
                sys.exit()
    # Client.py
    import socket
    import sys,os
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("127.0.0.1",8000))
    
    pid=os.fork()
    if(pid>0):
        while 1:
            msg=sys.stdin.readline()
            if msg == "quit":
                sys.exit()
            s.send(msg.encode('utf-8'))
    else:
        while 1:
            log_file=open('./server_input.log','a')
            msg=s.recv(1024) 
            print ("server:"+msg.decode('utf-8'))
            log_file.write(msg.decode('utf-8'))
            if msg == "bye":
                log_file.close()
                sys.exit()
    
    複製程式碼
    • 讓我們來看看使用效果

    較為原生的WebSocket服務端

    • oh,這可真是太妙了

    多人聊天室

    • 還是一點點分析現狀 1.所有人都知道我們的服務端地址和埠他們不知道彼此的地址和埠,所以,套接字的建立,只可能存在與伺服器與客戶端之間客戶端與客戶端之間是無法建立連線的 2.這樣我們就有了一個前提:我們的服務端可以與所有人建立連線,如果想要做一個聊天室,需要哪些功能呢?
      • 一個使用者發出訊息,發給了服務端,多人聊天室要幹什麼?當然是讓其他人接受到這個人發出的訊息,一句話概括,將一個人發給我們服務端的訊息,廣播給聊天室裡的其他人

      • 給單一客戶端傳送訊息需要我們儲存與這個客戶端的聊天通道,也就是socket,那廣播訊息呢?就需要我們把這些管道都給存起來,一條管道來了訊息,把訊息廣播出去

      • 好了,思路有了,我們來想一下有哪些問題,首先從聊天室的步驟說起,第一步是加入聊天室,我們之前的程式碼,accept之後就不會再呼叫這個方法,也就是說,服務端不會接受新的客戶端connect,怎麼解決這個問題的呢,當然是監聽accept(或者說不斷詢問)這裡,有返回值的時候就生成一個新的套接字,並把這個套接字存到我們的使用者列表裡,也就是把所有通道都進行記錄

      • 得到與所有使用者的聯絡通道之後,我們還要同時監聽所有的訊息傳送,然後進行我們之前說的步驟,接受使用者訊息,然後進行廣播

      • 下面是程式碼部分,由於要同時監聽accept與recv,我們選擇select這個庫(select可真是個神奇的東西)

        import socket, select
        
        #廣播函式
        def broadcast_data (sock, message):
         	#不給傳送訊息者和accept廣播   
            for socket in CONNECTION_LIST:
                if socket != server_socket and socket != sock :
                    try :
                        socket.send(message)
                    except :
                        socket.close()
                        CONNECTION_LIST.remove(socket)
        
        if __name__ == "__main__":
        
            #監聽列表,包括使用者列表和accept事件
            CONNECTION_LIST = []
            RECV_BUFFER = 4096 # Advisable to keep it as an exponent of 2
            PORT = 5000
        
            server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        
            server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            server_socket.bind(("0.0.0.0", PORT))
            server_socket.listen(10)
            #監聽accept的返回
            CONNECTION_LIST.append(server_socket)
        
            print "Chat server started on port " + str(PORT)
        
            while 1:
                #如果監聽列表裡有事件觸發,結果會返回到read_sockets裡,告知我們有哪些訊息來了
                read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])
         		 #然後我們就可以進行如下處理
                for sock in read_sockets:
                    #如果訊息來自server_socket,說明有新連線到來
                    if sock == server_socket:
                        sockfd, addr = server_socket.accept()
                        CONNECTION_LIST.append(sockfd)
                        print "Client (%s, %s) connected" % addr
                        broadcast_data(sockfd, "[%s:%s] entered room\n" % addr)
        
                    else:
                       #來訊息了
                        try:
                            data = sock.recv(RECV_BUFFER)
                            if data:
                                broadcast_data(sock, "\r" + '<' + str(sock.getpeername()) + '> ' + data)
                                #當client掉線時,recv會不斷受到空訊息,所以關閉socket   
                            if not data :
                                broadcast_data(sock, "\r" + '<' + str(sock.getpeername()) + '> ' + "下線了")
                                sock.close()
                        except:
                            broadcast_data(sock, "Client (%s, %s) is offline" % addr)
                            print "Client (%s, %s) is offline" % addr
                            sock.close()
                            CONNECTION_LIST.remove(sock)
                            continue
            server_socket.close()
        #client.py
        import socket, select, string, sys,signal
        def prompt() :
            sys.stdout.write('<You> ')
            sys.stdout.flush()
        def sighandler(signum,frame):
                sys.stdout.write("Shutdown Server......\n")
                #向已經連線客戶端傳送關係資訊,並主動關閉socket
                #關閉listen
                sys.stdout.flush()
                sys.exit()
        if __name__ == "__main__":
            signal.signal(signal.SIGINT,sighandler)
            signal.signal(signal.SIGHUP,sighandler)
            signal.signal(signal.SIGTERM, sighandler)
            if(len(sys.argv) < 3) :
                print('Usage : python telnet.py hostname port')
                sys.exit()
        	host = sys.argv[1]
            port = int(sys.argv[2])
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(2)
            try :
                s.connect((host, port))
            except :
                print('Unable to connect')
                sys.exit()
            print('Connected to remote host. Start sending messages')
            prompt()
            while 1:
                rlist = [sys.stdin,s]
                read_list, write_list, error_list = select.select(rlist , [], [])
                for sock in read_list:
                    if sock == s:
                        data = sock.recv(4096)
                        if not data :
                            print('\nDisconnected from chat server')
                            sys.exit()
                        else :
                            print (data.decode('utf-8'))
                            sys.stdin.flush()
                            prompt()
                    else :
                        msg = sys.stdin.readline()
                        s.send(msg.encode('utf-8'))
                        prompt()
        複製程式碼
    • 檢視程式碼之後你會發現,這個寫法是單程式的,一個select幫我們解決了堵塞的問題,他將許多個堵塞集中到了一個堵塞身上,使得功能得以實現
    • 不過這種單程式的模式,個人分析會有反應不及時的問題,畢竟它是一個程式負責轉發多個訊息,如果訊息多了,for迴圈的情況下響應速度會降下來
    • 所以還可以有另一種模式,做一下簡單設想:

多執行緒模式

  • 依然是一個程式負責不斷接受使用者的連線請求,但是當它接收到請求之後的處理方式發生變化,我們開一個執行緒來專門負責這個新通道的訊息監聽與傳送,之前那個程式接受到新的使用者連線之後將使用者列表儲存到一個所有執行緒都可以訪問的地方(我只知道redis,感覺這樣可行),這樣一來,我們為每一個使用者建立一個專屬執行緒,負責接發這個使用者的訊息接受和轉發,響應速度的問題也就解決了
參考文章:[Python Socket 程式設計——聊天室示例程式 By--hazir](https://www.cnblogs.com/hazir/p/python_chat_room.html)複製程式碼

相關文章