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) 收藏 举报