zoukankan      html  css  js  c++  java
  • Python Socket 编程——聊天室演示样例程序

    上一篇 我们学习了简单的 Python TCP Socket 编程,通过分别写服务端和client的代码了解主要的 Python Socket 编程模型。本文再通过一个样例来加强一下对 Socket 编程的理解。

    聊天室程序需求

    我们要实现的是简单的聊天室的样例,就是同意多个人同一时候一起聊天。每一个人发送的消息全部人都能接收到,类似于 QQ 群的功能,而不是点对点的 QQ 好友之间的聊天。例如以下图:

    图来自:http://www.ibm.com/developerworks/linux/tutorials/l-pysocks/

    我们要实现的有两部分:

    • Chat Server:聊天server。负责与用户建立 Socket 连接。并将某个用户发送的消息广播到全部在线的用户。

    • Telnet Client:用户聊天client,能够输入聊天的内容并发送,同一时候能够显示其它用户的消息记录。

    相同,我们的消息通信採用 TCP 连接保证可靠性。在分别对服务端和client进行程序设计之前。首先要学习一下 Python 中实现异步 I/O 的一个函数 —— select

    Python 异步 I/O

    Python 在 select 模块中提供了异步 I/O(Asynchronous I/O),这与 Linux 下的 select 机制相似,但进行一些简化。

    我首先介绍一下 select,然后告诉你在 Python 中怎样使用它。

    前面文章使用多线程来并行处理多路 socket I/O,这里介绍的select 方法同意你响应不同 socket 的多个事件以及其他不同事件。比如你能够让 select 在某个 socket 有数据到达时。或者当某个 socket 能够写数据时,又或者是当某个 socket 错误发生时通知你。优点是你能够同一时候响应非常多 socket 的多个事件。

    Linux 下 C 语言的 select 使用到位图来表示我们要关注哪些文件描写叙述符的事件,Python 中使用 list 来表示我们监控的文件描写叙述符,当有事件到达时,返回的也是文件描写叙述符的 list。表示这些文件有事件到达。以下的简单程序是表示等待从标准输入中获得输入:

    rlist, wlist, elist = select.select( [sys.stdin], [], [] )
    
    print sys.stdin.read()

    select 方法的三个參数都是 list 类型。分别代表读事件、写事件、错误事件,相同方法返回值也是三个 list,包括的是哪些事件(读、写、异常)满足了。上面的样例,因为參数仅仅有一个事件 sys.stdin,表示仅仅关心标准输入事件,因此当 select 返回时 rlist 仅仅会是 [sys.stdin]。表示能够从 stdin 中读入数据了。我们使用 read 方法来读入数据。

    当然 select 对于 socket 描写叙述符也是有效的。以下的一个样例是创建了两个 socket client连接到远程server。select 用来监控哪个 socket 有数据到达:

    import socket
    import select
    
    sock1 = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    sock2 = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    
    sock1.connect( ('192.168.1.1', 25) )
    sock2.connect( ('192.168.1.1', 25) )
    
    while 1:
    
        # Await a read event
        rlist, wlist, elist = select.select( [sock1, sock2], [], [], 5 )
    
        # Test for timeout
        if [rlist, wlist, elist] == [ [], [], [] ]:
            print "Five seconds elapsed.
    "
    
        else:
            # Loop through each socket in rlist, read and print the available data
            for sock in rlist:
                print sock.recv( 100 )

    好了,有了上面的基础,我们就能够来设计聊天室的server和client了。

    聊天室server

    聊天室server主要完毕以下两件事:

    • 接收多个client的连接
    • 从每一个client读入消息病广播到其他连接的client

    我们定义一个 list 型变量 CONNECTION_LIST 表示监听多个 socket 事件的可读事件,那么利用上面介绍的我们的server使用 select 来处理多路复用 I/O 的代码例如以下:

    # Get the list sockets which are ready to be read through select
    read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])

    当 select 返回时,说明在 read_sockets 上有可读的数据,这里又分为两种情况:

    1. 假设是主 socket(即server開始创建的 socket,一直处于监听状态)有数据可读,表示有新的连接请求能够接收。此时须要调用 accept 函数来接收新的client连接,并将其连接信息广播到其他client。
    2. 假设是其他 sockets(即与client已经建立连接的 sockets)有数据可读,那么表示client发送消息到server端,使用 recv 函数读消息,并将消息转发到其他全部连接的client。

    上面两种情况到涉及到广播消息的过程。广播也就是将从某个 socket 获得的消息通过 CONNECTION_LIST 的每一个 socket (除了自身和主 socket)一个个发送出去:

    def broadcast_data (sock, message):
        #Do not send the message to master socket and the client who has send us the message
        for socket in CONNECTION_LIST:
            if socket != server_socket and socket != sock :
                try :
                    socket.send(message)
                except :
                    # broken socket connection may be, chat client pressed ctrl+c for example
                    socket.close()
                    CONNECTION_LIST.remove(socket)

    如果发送失败,我们如果某个client已经断开了连接。关闭该 socket 病将其从连接列表中删除。

    完整的聊天室server源码例如以下:

    # Tcp Chat server
     
    import socket, select
     
    #Function to broadcast chat messages to all connected clients
    def broadcast_data (sock, message):
        #Do not send the message to master socket and the client who has send us the message
        for socket in CONNECTION_LIST:
            if socket != server_socket and socket != sock :
                try :
                    socket.send(message)
                except :
                    # broken socket connection may be, chat client pressed ctrl+c for example
                    socket.close()
                    CONNECTION_LIST.remove(socket)
     
    if __name__ == "__main__":
         
        # List to keep track of socket descriptors
        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)
        # this has no effect, why ?

    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind(("0.0.0.0", PORT)) server_socket.listen(10) # Add server socket to the list of readable connections CONNECTION_LIST.append(server_socket) print "Chat server started on port " + str(PORT) while 1: # Get the list sockets which are ready to be read through select read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[]) for sock in read_sockets: #New connection if sock == server_socket: # Handle the case in which there is a new connection recieved through server_socket sockfd, addr = server_socket.accept() CONNECTION_LIST.append(sockfd) print "Client (%s, %s) connected" % addr broadcast_data(sockfd, "[%s:%s] entered room " % addr) #Some incoming message from a client else: # Data recieved from client, process it try: #In Windows, sometimes when a TCP program closes abruptly, # a "Connection reset by peer" exception will be thrown data = sock.recv(RECV_BUFFER) if data: broadcast_data(sock, " " + '<' + str(sock.getpeername()) + '> ' + data) 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()

    在控制台下执行该程序:

    $ python chat_server.py 
    Chat server started on port 5000

    聊天室client

    我们写一个client程序能够连接到上面的server,完毕发送消息和接收消息的过程。主要做以下两件事:

    • 监听server是否有消息发送过来
    • 检查用户的输入,假设用户输入某条消息,须要发送到server

    这里有两个 I/O 事件须要监听:连接到server的 socket 和标准输入。相同我们能够使用 select 来完毕:

    rlist = [sys.stdin, s]
             
    # Get the list sockets which are readable
    read_list, write_list, error_list = select.select(rlist , [], [])

    那逻辑就非常easy了,假设是 sys.stdin 有数据可读,表示用户从控制台输入数据并按下回车,那么就从标准输入读数据,并发送到server;假设是与server连接的 socket 有数据可读,表示server发送消息给该client。那么就从 socket 接收数据。加上一些提示信息及异常处理的完整client代码例如以下:

    # telnet program example
    import socket, select, string, sys
     
    def prompt() :
        sys.stdout.write('<You> ')
        sys.stdout.flush()
     
    #main function
    if __name__ == "__main__":
         
        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)
         
        # connect to remote host
        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]
             
            # Get the list sockets which are readable
            read_list, write_list, error_list = select.select(rlist , [], [])
             
            for sock in read_list:
                #incoming message from remote server
                if sock == s:
                    data = sock.recv(4096)
                    if not data :
                        print '
    Disconnected from chat server'
                        sys.exit()
                    else :
                        #print data
                        sys.stdout.write(data)
                        prompt()
                 
                #user entered a message
                else :
                    msg = sys.stdin.readline()
                    s.send(msg)
                    prompt()

    能够在多个终端下执行该代码:

    $ python telnet.py localhost 5000
    Connected to remote host. Start sending messages
    <You> hello
    <You> I am fine
    <('127.0.0.1', 38378)> ok good
    <You>

    在还有一个终端显示的信息:

    <You> [127.0.0.1:39339] entered room
    <('127.0.0.1', 39339)> hello
    <('127.0.0.1', 39339)> I am fine
    <You> ok good

    总结

    上面的代码注意两点:

    1. 聊天室client代码不能在 windows 下执行,由于代码使用 select 同一时候监听 socket 和输入流,在 Windows 下 select 函数是由 WinSock 库提供,不能处理不是由 WinSock 定义的文件描写叙述符。
    2. client代码还有个缺陷是,当某个client在输入消息但还未发送出去时,server也发送消息过来,这样会冲刷掉client正在输入的消息。这眼下来看没办法解决的,唯一的解决方法是使用像 ncurses 终端库使用户输入和输出独立开,或者写一个 GUI 的程序。

    那么本文通过一个聊天室的范例进一步学习了 Python 下 Socket 编程。

  • 相关阅读:
    BFS visit tree
    Kth Largest Element in an Array 解答
    Merge k Sorted Lists 解答
    Median of Two Sorted Arrays 解答
    Maximal Square 解答
    Best Time to Buy and Sell Stock III 解答
    Best Time to Buy and Sell Stock II 解答
    Best Time to Buy and Sell Stock 解答
    Triangle 解答
    Unique Binary Search Trees II 解答
  • 原文地址:https://www.cnblogs.com/mfmdaoyou/p/7301028.html
Copyright © 2011-2022 走看看