较为原生的WebSocket服务端

1,414 阅读6分钟
  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()
    
    
    • 让我们来看看使用效果

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