2.网络编程:练习socket,逐步实现丰富功能的聊天室

一、半双工聊天:
教材中的socket等示例都是半双工的聊天,就是两者建立了连接,你发一句,再我发一句,和上节的示例是一样的。代码如下:
server:
#!/usr/bin/python
# coding: utf8
import time
import socket

HOST = ""
PORT = 52000
BUFF_SIZE = 512
ADDR = (HOST, PORT)     # 本机的地址与端口
tcpSerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 1.创建套接字
tcpSerSocket.bind(ADDR)     # 2.把套接字绑定到端口和服务器的主机IP
tcpSerSocket.listen(1)      # 3.监听TCP请求,监听多少个TCP请求
print 'here, starting tcp server...'
while True:   # 等待连接的循环
    print 'here, waiting for connection...'
    # 4.接受请求:等待客户端的TCP请求,阻塞式的;
    # 返回:服务器为客户端请求创建的套接字(也经常用con表示)和客户端地址
    tcpCliSocket, addr = tcpSerSocket.accept()
    print '.....address requested by client... ', addr
    while True:  # 5.某连接数据交换的循环
        data = tcpCliSocket.recv(BUFF_SIZE)  #
        if not data:
            break
        tcpCliSocket.send('[%s] %s' % (time.ctime(), data))
    tcpCliSocket.close()    # 6.关闭某客户端的连接
client:
#!/user/bin/env python
# coding: utf-8
import socket
import time

HOST = '127.0.0.1'
PORT = 52000
BUFF_SIZE = 512
ADDR = (HOST, PORT)     # 服务器的地址与端口
tcpClieSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # 1.在客户端创建套接字
tcpClieSocket.connect(ADDR)     # 2.套接字与服务器建立连接
while True:   # 3.数据交换
    send_data = raw_input('here, please input: ')
    tcpClieSocket.send('[%s] %s' % (time.ctime(), send_data))  
    if not send_data:   # 当发送一个空的数据给服务器的时侯,退出数据交换循环,接着执行后面的关闭连接语句
        break
    receive_data = tcpClieSocket.recv(BUFF_SIZE)
    print 'receive data from server: ', receive_data
tcpClieSocket.close()   # 4.退出连接
这样,有几个缺点,就是只能client先发数据,然后就是server接收,又发....只能按次序来发。
如果要克服上面的缺点,那就要实现全双工聊天,就是可以想怎么发就怎么发。这里我用的是select的模块。。select 模块通常在底层套接字程序中与socket 模块联合使用。它提供的select()函数可以同时管理多个套接字对象。它最有用的功能就是同时监听多个套接字的连接。select()函数会阻塞,直到有至少一个套接字准备好要进行通讯的时候才退出。select可以同时监听套接字与系统的标准输入,用阻塞的方式。下面每次轮询时都会运行到select的函数,阻塞,直到有输入时才进行下面的操作。
二、使select实现全双工聊天室:全双工服务器
server端:
#!/usr/bin/python
# coding: utf8
import time
import socket
import select
import sys

HOST = ""
PORT = 52002
BUFF_SIZE = 512
ADDR = (HOST, PORT)
tcpSerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 1
tcpSerSocket.bind(ADDR)     # 2
tcpSerSocket.listen(1)      # 3
input = [tcpSerSocket, sys.stdin]   # 一、初始化input,将服务器的套接字加入input
while True:   # 等待连接的循环
    tcpCliSocket, addr = tcpSerSocket.accept()    # 4
    input.append(tcpCliSocket)  # 二、当有客户端连接时,将客户端的套接字加入input
    while True:  # 某连接,数据交换的循环
        readyInput, readyOutput, readyException = select.select(input, [], []) # 三、每次循环都会阻塞在这里,只有当有数据输入时才会执行下面的操作
        for indata in readyInput:
            if indata is tcpCliSocket:  # 四、当客户端有数据输入时,input中的对象为服务器为客户端创建的套接字tcpCliSocket
                # 接收客户端的数据
                receive_data = tcpCliSocket.recv(BUFF_SIZE)  # 从客户端接收数据
                if not receive_data:   # 当客户端发送一个空的消息时,退出数据交换循环,接着执行后面的关闭某客户端连接
                    break
                print 'data from client %s: %s' % (addr, receive_data)  # 在服务器上显示客户端发送的数据
                # tcpCliSocket.send('[%s] %s' % (time.ctime(), input_data))  # 取消原样返回来自客户端的数据
            else:  # 五、当服务端有数据输入时,input中的对象为服务器的sys.stdin文件对象
                # 服务端输入数据,发送给客户端
                print indata
                input_data = raw_input('here, pls input: ')
                tcpCliSocket.send('[%s] %s' % (time.ctime(), input_data))
                if not input_data:   # 当从服务端发送一个空消息时,将退出数据交换循环,接着执行后面的关闭某客户端连接
                    break
    tcpCliSocket.close()

 

 客户端代码和服务器端基本上差不多:

#!/user/bin/env python
# coding: utf-8
import socket
import time
import select
import sys

HOST = '127.0.0.1'
PORT = 52002
BUFF_SIZE = 512
ADDR = (HOST, PORT)     # 服务器的地址与端口
tcpCliSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # 1.在客户端创建套接字
tcpCliSocket.connect(ADDR)     # 2.套接字与服务器建立连接
input = [tcpCliSocket, sys.stdin]   # 一、不同的就在这里,当有客户端连接时,要把他加进input;服务器(套接字)同样要执行
while True:   # 3.数据交换
    readyInput, readyOutput, readyException = select.select(input, [], [])
    for indata in readyInput:
        if indata is tcpCliSocket:
            # 接收服务端的数据
            receive_data = tcpCliSocket.recv(BUFF_SIZE)
            if not receive_data:  # 收到服务端的空消息时,退出数据交换循环,接着执行后面的关闭某客户端连接
                break
            print 'data from server %s : %s' % (ADDR, receive_data)
        else:
            # 发送给数据给服务端
            input_data = raw_input('here, pls input: ')
            tcpCliSocket.send('[%s] %s' % (time.ctime(), input_data))
            if not input_data:  # 当从客户端发送一个空消息时,将退出数据交换循环,接着执行后面的关闭某客户端连接
                break
tcpCliSocket.close()   # 4.退出连接

 

input.append(tcpCliSock):
server端在有client端connect它时,接收其连接要求(具体内容详见tcp建立连接的过程),即accept()函数,将这个连接交给一个新的socket,名称为tcpCliSock。将这个socket加入到等待输入的列表中,以便select检测其发送来的消息
inputready,outputready,exceptready = select.select(input,[],[])
这个才是用单线程来实现全双工的异步通信的关键所在,每次循环都会轮循输入(其中这儿有两种可能的输入,一个是来自stdin,默认是服务端的键盘;另一个来自client链接发送来的消息,即client这个socket)

 

另外一个版本的socket,select实现TCP服务器:

#!/usr/bin/python
# coding: utf8
import time
import socket
import select
import sys
import Queue

HOST = ""
PORT = 52000
BUFF_SIZE = 512
ADDR = (HOST, PORT)
tcpSerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 1
tcpSerSocket.setblocking(False)
tcpSerSocket.bind(ADDR)     # 2
tcpSerSocket.listen(5)      # 3
# Sockets from which we expect to read
inputs = [tcpSerSocket, sys.stdin]
# 处理要发送的消息
outputs = []
# Outgoing message queues (socket: Queue)
message_queues = {}
while True:
    print ('waiting for the next event')
    # 开始select 监听, 对input_list 中的服务器端server 进行监听
    # 一旦调用socket的send或recv函数,将会再次调用此模块
    readable, writable, exceptional = select.select(inputs, outputs, inputs)
    # 循环判断是否有客户端连接进来, 当有客户端连接进来时select 将触发
    for sock in readable:
        # 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时,说明有新客户端连接进来了;当触发的对象是客户端对象时,说明是已连接的客户端有消息到来
        # 表示有新用户来连接
        if sock is tcpSerSocket:
            connection, client_address = tcpSerSocket.accept()
            connection.setblocking(False)
            # 将客户端对象也加入到监听的列表中, 当客户端发送消息时 select 将触发
            inputs.append(connection)
            # 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
            message_queues[connection] = Queue.Queue()
        else:
            # 有老用户发消息, 处理接受
            # 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list), 客户端发送消息将触发
            # 所以判断是否是客户端对象触发
            data = sock.recv(BUFF_SIZE)
            if data != '':              # 客户端是否断开
                print ('received "%s" from %s' % (data, sock.getpeername()))
                # 将收到的消息放入到相对应的socket客户端的消息队列中
                message_queues[sock].put(data)
                # 将需要进行回复操作socket放到output 列表中, 让select监听
                if sock not in outputs:
                    outputs.append(sock)
            else:
                # 客户端断开了连接, 将客户端的监听从input列表中移除
                print ('closing ', client_address)
                if sock in outputs:
                    outputs.remove(sock)
                inputs.remove(sock)
                sock.close()
                # 移除对应socket客户端对象的消息队列
                del message_queues[sock]
    # 如果现在没有客户端请求, 也没有客户端发送消息时, 开始对发送消息列表进行处理, 是否需要发送消息
    # 存储哪个客户端发送过消息
    for sock in writable:
        try:
            # 如果消息队列中有消息,从消息队列中获取要发送的消息
            message_queue = message_queues.get(sock)
            send_data = ''
            if message_queue is not None:
                send_data = message_queue.get_nowait()
            else:
                # 客户端连接断开了
                print "%s has closed " % sock
        except Queue.Empty:
            # 客户端连接断开了
            print "%s" % (sock.getpeername(),)
            outputs.remove(sock)
        else:
            # print "sending %s to %s " % (send_data, s.getpeername)
            # print "send something"
            if message_queue is not None:
                sock.send(send_data)
            else:
                print "%s has closed " % sock
            # del message_queues[s]

 

两个问题, 一是如何判断客户端已经关闭了socket连接, 后来自己分析了下, 如果关闭了客户端socket, 那么此时服务器端接收到的data就是'', 加个这个判断。二是如果服务器端关闭了socket, 一旦在调用socket的相关方法都会报错, 不管socket是不是用不同的容器存储的(意思是说list_1存储了socket1, list_2存储了socket1, 我关闭了socket1, 两者都不能在调用这个socket了)

客户端:

import socket


messages = ['This is the message ', 'It will be sent ', 'in parts ', ]
server_address = ('localhost', 8090)
# Create aTCP/IP socket
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM), socket.socket(socket.AF_INET,  socket.SOCK_STREAM), ]

# Connect thesocket to the port where the server is listening

print ('connecting to %s port %s' % server_address)
# 连接到服务器
for s in socks:
    s.connect(server_address)

for index, message in enumerate(messages):
    # Send messages on both sockets
    for s in socks:
        print ('%s: sending "%s"' % (s.getsockname(), message + str(index)))
        s.send(bytes(message + str(index)).decode('utf-8'))
    # Read responses on both sockets

for s in socks:
    data = s.recv(1024)
    print ('%s: received "%s"' % (s.getsockname(), data))
    if data != "":
        print ('closingsocket', s.getsockname())
        s.close()

 三、用多线程实现多用户全双工聊天:

思路:

我们将服务器做为中转站来处理信息,一方面与客户端互动,另一方面进行消息转发。

需要确定一些通信规则:

1. 客户端与服务器建立连接后,需要输入用户名登入,若用户名已存在,将reuse反馈给用户,用户输出错误信息,退出

2. 用户输入正确的用户名后,即可进行通信了。如果未选择通信对象,则服务器会反馈信息,提示用户选择通信对象

3. 选择通信对象的方法为,输入to:username,如果所选择的对象不存在,反馈错误信息,重新输入

4.当正确选择通信对象后,双方建立连接,通过服务器中转信息进行通信

5.在通信中,若发送‘quit',则结束发送消息的线程,并指示服务器该用户准备登出,服务器删除该用户后,反馈消息给用户,用户结束接收消息的线程并退出

6.如果A正在与C通信,此时B向A发送信息,则A的通信窗口变为与B的通信窗口,即接收到B消息后,A发出的消息不再是给C,而是默认给B

server:
from socket import *
from time import ctime
import threading  #多线程模块
import re   #正则表达式模块
HOST = ''
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
def Deal(sock, user):
  while True:
    data = sock.recv(BUFSIZ)  #接收用户的数据
    if data == 'quit':  #用户退出
      del clients[user]
      sock.send(data)
      sock.close()
      print '%s logout' %user
      break
    elif re.match('to:.+', data) is not None:  #选择通信对象
      data = data[3:]
      if clients.has_key(data):
        chatwith[sock] = clients[data]
        chatwith[clients[data]] = sock
      else:
        sock.send('the user %s is not exist' %data)
    else:
      if chatwith.has_key(sock):  #进行通信
        chatwith[sock].send("[%s] %s: %s" %(ctime(), user, data))
      else:
        sock.send('Please input the user who you want to chat with')
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5)
clients = {}  #提供 用户名->socket 映射
chatwith = {}  #提供通信双方映射
while True:
  print 'waiting for connection...'
  tcpCliSock, addr = tcpSerSock.accept()
  print '...connected from:',addr
  username = tcpCliSock.recv(BUFSIZ)  #接收用户名
  print 'The username is:',username
  if clients.has_key(username):  #查找用户名
    tcpCliSock.send("Reuse")  #用户名已存在
    tcpCliSock.close()
  else:
    tcpCliSock.send("Welcome!")  #登入成功
    clients[username] = tcpCliSock
    chat = threading.Thread(target = Deal, args = (tcpCliSock,username))  #创建新线程进行处理
    chat.start()  #启动线程
tcpSerSock.close()

客户端:

#!/usr/bin/python
'test tcp client'
from socket import *
import threading
HOST = 'localhost'
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
threads = []
def Send(sock, test):  #发送消息
  while True:
    data = raw_input('>')
    tcpCliSock.send(data)
    if data == 'quit':
      break
def Recv(sock, test):   #接收消息
  while True:
    data = tcpCliSock.recv(BUFSIZ)
    if data == 'quit':
      sock.close()   #退出时关闭socket
      break
    print data
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
print 'Please input your username:',
username = raw_input()
tcpCliSock.send(username)
data = tcpCliSock.recv(BUFSIZ)
if data == 'Reuse':
  print 'The username has been used!'
else:
  print 'Welcome!'
  chat = threading.Thread(target = Send, args = (tcpCliSock,None))  #创建发送信息线程
  threads.append(chat)
  chat = threading.Thread(target = Recv, args = (tcpCliSock,None))  #创建接收信息线程
  threads.append(chat)
  for i in range(len(threads)):  #启动线程
    threads[i].start()
  threads[0].join()  #在我们的设计中,send线程必然先于recv线程结束,所以此处只需要调用send的join,等待recv线程的结束。

不足之处,比如通信双方中A退出时,另一方B的通信列表中仍然又A,此时如果B再向已经登出的B发送消息,就会出错

 

四、多用户多房间(群聊)全双工聊天:

server:

#!/usr/bin/env python
# coding: utf-8
from socket import *
import re
import threading
HOST = ''
PORT = 21568
BUFSIZE = 1024
ADDR = (HOST, PORT)
rooms = {}
conns = {}
REFUSE = u'对方正在聊天,您的请求拒绝连接'.encode('utf-8')
WAITING = u'请等待对方上线'.encode('utf-8')
INPUT_ERROR = u'您的输入有误'.encode('utf-8')

def deal_conn(name):
    while True:
        conn_from = conns[name]
        data = conn_from.recv(BUFSIZE)
        if not data:break
        if rooms[name] in conns:
            conn_to = conns[rooms[name]]
            conn_to.send(data)
        else:
            conn_from.send(WAITING)

server = socket(AF_INET, SOCK_STREAM)
server.bind(ADDR)
server.listen(10)
while True:
    conn, addr = server.accept()
    settings = conn.recv(BUFSIZE)
    patt = r'name:(.+)\sto:(.+)'
    m = re.match(patt,settings.decode('utf-8'))
    if not m:conn.send(INPUT_ERROR)
    name = m.group(1)
    if name not in conns:
        conns[name] = conn

    to = m.group(2)
    if name not in rooms and to not in rooms:
        rooms[name] = to
        rooms[to] = name
    elif name in rooms and rooms[name] != to:
        del conns[name]
        if name in rooms:
            del rooms[name]
        conn.send(REFUSE)
        conn.close()
    t = threading.Thread(target=deal_conn,args=(name,))
    t.start()

 

 

 

客户端:

#!/usr/bin/env python
# coding: utf-8
from socket import *
from select import *
import sys

HOST = 'localhost'
POST = 21568
BUFSIZE = 1024
ADDR = (HOST, POST)

server = socket(AF_INET, SOCK_STREAM)
server.connect(ADDR)
inpu = [server,sys.stdin]
print u"请输入你的姓名和对方的姓名(格式name:你的姓名 to:对方姓名)"
data = input('')
server.send(data.encode('utf-8'))
while True:
    r_list, w_list, e_list = select(inpu,[],[])
    for s in r_list:
        if s == server:
            data = server.recv(BUFSIZE)
            if not data: break
            print((data.decode('utf-8')))
        else:
            data = input('>>> ')
            if not data:break
            server.send(data.encode('utf-8'))

 

 

 

未整理:

#!/usr/bin/env python 
# _*_ coding: utf8 _*_ 
from socket import * 
from time import ctime 
import threading 
from string import split 
HOST = '' 
PORT = 21567 
BUFSIZE = 1024 
ADDR = (HOST, PORT) 
def Deal(sck, username, room): 
while True: 
data = sck.recv(BUFSIZE) 
for i in clients[room].iterkeys(): 
if i <> username: 
if data <>"quit": 
clients[room][i].send("[%s] %s: %s"%(ctime(), username, data)) 
else: 
clients[room][i].send("用户%s在%s退出房间%s"%(username, ctime(), room )) 
if data =="quit": 
del clients[room][username] 
sck.send(data) 
sck.close() 
breakchatSerSock = socket(AF_INET, SOCK_STREAM) 
chatSerSock.bind(ADDR) 
chatSerSock.listen(5) 
clients = {"":{},} 
while True: 
print 'waiting for connection...' 
chatCliSock, addr = chatSerSock.accept() 
print"...connected romt:", addr 
data = chatCliSock.recv(BUFSIZE) 
username, room = split(data) 
print username 
if not clients.has_key(room): 
clients[room] = {} 
if clients[room].has_key(username): 
chatCliSock.send("reuse") 
chatCliSock.close() 
else: 
chatCliSock.send("success") 
clients[room][username] = chatCliSock 
t = threading.Thread(target=Deal, args=(chatCliSock, username, room)) 
t.start() 
chatSerSock.close() 
#!/usr/bin/env python 
# _*_ coding: utf8 _*_ 
from socket import * 
from time import ctime 
import threading 
import random 
from sys import argv, exit, stdout 
from getopt import gnu_getopt, GetoptError 
help_info = ["cs.py [ -h | --help | -u | --username] username", 
"t-h or --helpt显示帮助信息", 
"t-u or --username指定用户名", 
"t-r or --roomt指定房间"] 
def help(): 
for i in help_info: 
print idef Send(sck, test): 
while True: 
data = raw_input('>') 
sck.send(data) 
if data =="quit": 
break 
def Recieve(sck, test): 
while True: 
data = sck.recv(BUFSIZ) 
if data =="quit": 
sck.close() 
break 
str ="n"+ data +"n>" 
stdout.write(str) 
HOST = 'localhost' 
PORT= 21567 
BUFSIZ = 1024 
ADDR = (HOST, PORT) 
threads = [] 
if __name__ =="__main__": 
# 解析命令行参数 
try: 
opts, args = gnu_getopt(argv[1:],"hu:r:", ["help","username=","room="]) 
except GetoptError, err: 
print str(err) 
help() 
exit(2) 
username ="" 
room ="" 
for o, a in opts: 
if o in ("-h","--help"): 
help() 
exit(0) 
elif o in ("-u","--username"): 
username = a 
elif o in ("-r","--room"): 
room = a 
else: 
print"未知选项" 
help() 
exit(2) 
if not username or not room: 
help() 
exit(2) 
chatCliSock = socket(AF_INET, SOCK_STREAM) 
chatCliSock.connect(ADDR) 
chatCliSock.send("%s %s"%(username, room)) 
data = chatCliSock.recv(BUFSIZ) 
if data =="reuse": 
print"用户%s已登录房间%s"%(username, room) 
raw_input() 
exit(1) 
elif data =="success": 
print"用户%s成功登录房间%s"%(username, room) 
t = threading.Thread(target=Send, args = (chatCliSock, None)) 
threads.append(t) 
t = threading.Thread(target=Recieve, args = (chatCliSock, None)) 
threads.append(t) 
for i in range(len(threads)): 
threads[i].start() 
threads[0].join() 

posted on 2018-04-27 11:46  myworldworld  阅读(304)  评论(0)    收藏  举报

导航