更新时间:2022-09-06 18:15:25
开发者学堂课程【Python网络编程:TCP 群聊服务端实现 】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/602/detail/8778
内容介绍
一、TCP 编程
二、TCP 服务端
三、练习一
四、其他方法
五、练习二
六、ChatServer 实验用完整代码
Socket 编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server ,客户端称为 Client
(1)服务器端编程步骤
●创建 Socket 对象
● 绑定 IP 地址 Address 和端口 Port.bind() 方法
IPv4 地址为一个二元组(('IP地址字符串', Port)
● 开始监听,将在指定的IP的端口上监听。 listen() 方法
●获取用于传送数据的 Socket 对象
socket.accept()->(socket object, address info)
accept方法阻塞等待客户端建立连接,返回一个新的 Socket 对象和客户端地址的二元组地址是远程客户端的地址,IPv4 中它是一个二元组(clientaddr, port)
>接收数据
recv(bufsize【, flags使用缓冲区接收数据
> 发送数据
send((bytes)发送数据
(2)问题:两次绑定同一个监听端口会怎么样?
import socket
s = socket.socket()#创建socket对象
s.bind(('127.0.0.1',9999))#一个二元组s.listen()# 开始监听
# 开启一个连接
s1,info= s.accept()# 阻塞直到和客户端成功建立连接,返回一个 socket 对象和客户端地址
# 使用缓冲区获取数据
data = s1.recv(1024)
print(data,info)
s1.send(b'magedu.com ack')
# 开启另外一个连接
s2,_= s.accept()
data = s2.recv(1024)s2.send(b'hello python')
s.close()
上例 accept 和 recv 是阻塞的,主线程经常被阻塞住而不能工作。怎么办?
写一个群聊程序
需求分析
聊天工具是 CS 程序,C 是每一个客户端,S 是服务器端。
服务器应该具有的功能∶
1.启动服务,包括绑定地址和端口,监听
2.建立连接,能和多个客户端建立连接
3.接收不同用户的信息
4.分发,将接收的某个用户的信息转发到已连接的所有客户端
5.停止服务
6.记录连接的客户端
部分代码如下:
import socket
import threading
import logging
import datetime
FORMAT= %(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO)
# TCP Server
class ChatServer:
def _init_(self,ip='127.0.0.1',port=9999):
self.addr =(ip,port)
self.sock = socket.socket ()
self.clients ={}
def start(self):
self.sock.bind(self.addr)
self.sock.listen()# 服务启动了
threading.Thread(target=self.accept,name='accept').start()
def accept(self):
while True∶# 一个线程
s
,raddr = self.
s
ock.accept()
# 阻塞
logging.info (raddr)logging.info(s)
self.clients[raddr] = s
threading.Thread(target=self.recv,name='recv',args=(s,)).start()
def start(self):
self.sock.bind(self.addr)
self.sock.listen
()
# 服务启动了
threading.Thread(target=self.accept,name='accept').start()
def accept (self):
while True:
s,raddr= self.ock.accept()#
阻塞
threading.Thread(target=self.recv,name='re
c
v',args=(s,)).start()
def recv(self,sock):
while True:
data = sock.recv(1024)# 阻塞,bytes
logging.info (data)
sock.send('ack {}'.format (data.decode()).encode())
#
TCP Server
class ChatServer:
d
ef
_init_(self,ip='127.0.0.1',port=9999):
self.addr =(ip,port)
self.sock = socket.socket ()
def start(self):
self.sock.bind (self.addr)
self.sock.listen
()
# 服务启动了
threading.Thread(target=self.accept,name='accept').start()
def accept(self):
while True:
s
,raddr = self.
s
ock.accept() # 阻塞
logging.info (raddr)
logging.info(s)
threading.Thread(target=self.recv,name='recv',args=(s,)).star
()
def recv (self,sock):
while True:
data = sock.recv(1024)# 阻塞,bytes
logging.info (data)
sock.send('ack{}'.format(data.decode()).encode())
def stop (self):
self.sock.close ()
def stop(self):
for s in self.clients.values():
s.close ()
self.sock.close ()
cs = ChatServer ()
cs.start ()
while True:
cmd = input(">>>")
if cmd.strip()== 'quit':
cs.stop ()
threading.Event.wait(3)
break
logging.info(threading.enumerate())
def accept(self)∶# 多人连接
while not self.event.is_set():
sock,client = self.sock.accept()#阻塞
self.clients【client】= sock # 添加到客户端字典
# 准备接收数据,recy是阻塞的,开启新的线程
threading.Thread(target=self.recv,args=(sock, client)).start()
def recv(self,sock∶socket.socket,client)∶#接收客户端数据
while not self.event.is_set():
data = sock.recv(1024)# 阻塞到数据到来
msg= "{:%
Y
/%m/%d %H:%M:%S}{}:{}\n{}\n".format(
datetime.datetime.now()
,
*client,data.decode())
def recv(self,sock∶socket.socket,client)∶#接收客户端数据
while not self.event.is_set():
data = sock
,
recv(1024)# 阻塞到数据到来
msg = data.decode().strip()
#客户端退出命令
if msg == 'quit':
Self.clients.pop(client)
sock.close()
logging.info('{} quits'.format(client))
break
msg="{:%Y/%m/%d %H:%
M
:%S}{}:{}\n{}\n".format(datetime.datetime.now()
,
*client,data.decode())
logging.info(msg)
msg = msg.encode()
for s in self.clients.values():
cs = ChatServer()
cs.start()
while True:
cmd =input('>>').strip(
)
if cmd == 'quit':
cs.stop()
threading.Event().wait(3)
break
这一版基本测试通过。但是还有要完善的地方。
例如各种异常的判断,客户端断开连接后字典中的移除客户端数据等。
客户端主动断开带来的问题
程序还有瑕疵,但是业务功能基本完成了
名称 |
含义 |
socket.recv(bufsizel, flags])
|
获取数据。默认是阻塞的方式
|
socket.recvfrom(bufsize) |
获取数据,返回一个二元组(bytes address) flags) |
socket.recv_into(buffer[,nbytes, flags]l)
|
获取到 nbytes 的数据后,存储到 buffer 中。如果 nbytes 没有指定或0,将 buffer 大小的数据存入 buffer 中。返回接收的字节 数。 |
socket.recvfrom_into(buffer[nbytes, flgs) |
获取数据,返回一个二元组((bytes address)到 buffer中 |
socket.send(bytes[,flags) |
TCP发送数据 |
socket.sendallbytes[, flagsl) |
TCP 发送全部数据,成功返回None |
s.sendto(stringl,flag],address) |
UDP 发送数据 |
socket.sendfile(file, offset=0,count=None)
|
发送一个文件直到 EOF,使用高性能的 os.sendfile 机制,返回 发送的字节数。如果 win 下不支持 sendfile,或者不是普通文 件,使用 send()发送文件。offset告诉起始位置。3.5版本开始 |
socket.makefile(mode='r',buffering=None,*,encoding=None,errors=None,newline=None)
创建一个与该套接字相关连的文件对象,将 recv 方法看做读方法,将 send 方法看做写方法。
重点代码如下:
# 使用 makefile
import socket
sockserver= socket.socket()
ip = '127.0.0.1'
port = 999
addr =(ip,port)
sockserver.bind(addr)
sockserver.listen()
print('-'*30)
s,__= sockserver,accept()
print('-'*30)
f = s.makefile(mode='rw')
line = f.read(10)# 阻塞等
print('-'*30)
print(1Line)
f.write('Return your msg:{}'.format(line))
def accept(sock:socket.socket,e:threading.Event):
s,_ = sock.accept()
f = s.makefile(mode='rw')
while True:
line = f.readline()
print(line)
if line.strip()= "quit"∶#注意要发quit\n
break
f.write('Return your msg: {'.format(line))
f.flush()
f.close()
sock.close()
e.wait(3)
t =threading.Thread(target=accept,args=(sockserver,event))
t.start()
t.join()
print(sockserver)
名称 |
含义 |
socket.getpeername() |
返回连接套接字的远程地址。返回值通常是元组 (ipadd,port)
|
socket.getsockname() |
返回套接字自己的地址。通常是一个元组 (ipaddr,port) |
socket.setblocking(flag)
|
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起 socket.error 异常 |
socket.settimeout(value) |
设置套接字操作的超时期, timeout是一个浮点数,单位是秒。值为 None 表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect0) |
socket.setsockopt(level,optname,value)
|
设置套接字选项的值。比如缓冲区大小。太多了,去看文档。不同系统,不同版本都不尽相同 |
使用makefile改写群聊类
import logging
self.addr =(ip,port)self.sock = socket.socket ()self.clients ={}
self.event = threading.Event ()
def start(self):
self.sock.bind(self.addr)
self.sock.listen()#服务启动了
threading.Thread(target=self.accept,name='accept').start()
def accept (self):
while not self.event.is set ()∶#—个线程
s,raddr = self.sock.accept () #阻塞logging.info (raddr)logging.info(s)
self.clients[raddr] = s
threading.Thread(target=self.recv,name='recv
args=(3,).start()
def recv(self,sock∶socket.socket)∶#很多线程
while not self.event.is_set():
tzy:
data = sock.recV(1024)# 阻塞,bytes
logging.info (data)except Exception as e:
logging.error(e)data = b'quit'if data == b'quit':
self.clients.pop(sock.getpeername ())sock.close ()break
msg = "ack{).{}{}".format(sock.getpeername (),
datetime.datetime.now().strftime("sY/sm/sd-8H:8M:S"), data.decode ()).encode ()
For s in self.clients.values():
s.send (msg)
while True:
cmd = input(">>').strip()
if cmd == 'quit":
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate())
# 用来观察断开后线程的变化
上例完成了基本功能,但是,如果客户端主动断开,或者 readline 出现异常,就不会从 clients 中移除作废的 socket。
可以使用异常处理解决这个问题。
注意,这个代码为实验用,代码中瑕疵还有很多。 Socket 太底层了,实际开发中很少使用这么底层的接口。增加一些异常处理
部分代码如下:
def main ():
cs = ChatServer()
cs.start ()
while True:
cmd = input (">>>")
if cmd.strip ()=='quit':
cs.stop ()
threading.Event.wait(3)break
logging.info(threading.enumerate())
if_name_=='main 一'