TCP编程 - C/S
Server端
import socket
import logging
import threading
import time
logging.basicConfig(format='%(thread)s %(threadName)s %(message)s',level=logging.INFO,)
# 两次绑定同一个监听端口会造成端口冲突,抛OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
s = socket.socket() # 缺省TCP协议,IP v4adressd
addr = ('127.0.0.1',9999) # 二元组,IP v4为字符串,Port为数值,65535
# '127.0.0.1'本地回环地址,当前主机设置成回环地址,其他主机无法访问当前主机,因为其他主机设置成回环地址,只能在自己的主机内一直回环
# 当前主机使用回环地址,只要是当前操作系统的资源,端口对应就可以访问,且不需要路由
s.bind(addr) # 绑定addr
s.listen() # server端开始监听
data = s.accept() # 阻塞等待Client端建立连接,用于获取和Client传送数据的Socket对象
s_connect,c_info = data
logging.info((type(s_connect),s_connect)) # laddr(local addr,本地地址);raddr(remote addr,远程地址,也称对端地址)
logging.info((type(c_info),c_info))
c_data = s_connect.recv(1024) # 接收Client端消息,阻塞,设置缓冲区大小,1024字节
print(c_data)
s_connect.send(c_data) # 向Client端发送消息,send方法也有缓冲区,如果缓冲区满了,也会阻塞
s_connect.close() # 连接Client端通信的socket对象,也需要归还
s.close() # socket占用文件描述符,一定要归还资源
聊天软件
初版
- 实现多个Client端和Sever端的通信,且故不干扰
- 主线程的socket对象,主要负责监听
- accept线程,无限循环与Client端建立连接
- 为每一个建立连接的Client创建进行,互不干扰的和Sever通信
注意:每次accept,就会生成一个socket对象与Client端进行连接通信,和主线程的socket不是一个对象
import logging
import threading
import socket
import datetime
import time
logging.basicConfig(format='%(asctime)s %(thread)d : %(message)s',level=logging.INFO)
class ChatServer:
def __init__(self,ip,port): # 启动聊天服务
self._addr = ip,port
self._server = socket.socket()
self.clients = {} # 存放客户端信息
def start(self): # 启动监听
self._server.bind(self._addr)
self._server.listen()
threading.Thread(target=self.s_accept,name='accept').start() # 创建一个线程,无限循环与Client端建立连接
def s_accept(self): # 无限连接线程
while True:
s_conn,c_addr = self._server.accept() # 与Client端建立链接阻塞主进程,所以创建一个线程
self.clients[c_addr] = s_conn
threading.Thread(target=self.s_recv,args=(s_conn,c_addr)).start() # 建立一个连接,就创建一个线程专门为Client端服务
def s_recv(self,sock:socket.socket,client): # 接收客户端数据
while True: # 每个线程中的Client端,可以循环发送和接收信息,直至退出
c_info = sock.recv(1024) # 多个Client端的时候,阻塞主线程,为每一个Client端创建一个线程
time.sleep(1)
msg = '{:%Y/%m/%d %H:%M:%S} {}:{} {}'.format(datetime.datetime.now(),*client,c_info).encode()
sock.send(msg)
def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self._server.close()
c_s = ChatServer(ip='127.0.0.1',port=9999)
c_s.start()
改进版
- 遍历字典实现多个用户群聊
- 使用字典遍历,给每一个Client端发送信息
- Client主动退出的时候,会发一个b’’,且抛异常
- 根据这个特性,当c_inof为b’‘或者b’quit’,退出循环
- 聊天服务退出的时候,抛异常需要捕获记录且打印
import logging
import threading
import socket
import datetime
logging.basicConfig(format='%(asctime)s %(thread)d : %(message)s',level=logging.INFO)
class ChatServer:
def __init__(self,ip,port): # 启动聊天服务
self._addr = ip,port
self._server = socket.socket()
self.clients = {} # 存放客户端信息
self.event = threading.Event() # 线程同步
def start(self): # 启动监听
self._server.bind(self._addr)
self._server.listen()
threading.Thread(target=self.s_accept,name='accept').start()
def s_accept(self): # 无限连接线程
while not self.event.is_set(): # 主线程退出,self.event置1,循环终止
try:
s_conn,c_addr = self._server.accept() # 注意,s_conn如果设置为self.s_conn,会出现死循环
except Exception as e: # 当self._server关闭,抛异常,捕获记录且终止循环
logging.info(e)
break
self.clients[c_addr] = s_conn
threading.Thread(target=self.s_recv,args=(s_conn,c_addr)).start()
def s_recv(self,sock:socket.socket,client): # 接收客户端数据
while not self.event.is_set(): # 如果使用self.conn,停止了刚创建的连接,而自己的没停止,造成死循环
c_info = sock.recv(1024) # 如果Client端主动断开,此时sock连接断开,抛异常
if c_info == b'' or c_info == b'quit':
break
logging.info(('c_info is ',c_info))
msg = '{:%Y/%m/%d %H:%M:%S} {}:{} {}'.format(datetime.datetime.now(),*client,c_info).encode()
for c in self.clients.values(): # 遍历,实现多个Client端互通
c.send(msg)
def stop(self): # 停止服务
self.event.set() # 停止服务,线程时间置1,s_accept和s_recv线程循环退出
for s in self.clients.values():
s.close()
self._server.close()
c_s = ChatServer(ip='127.0.0.1',port=9999)
c_s.start()
while True:
cmd = input('>>>>>>').lower().strip()
if cmd == 'quit':
c_s.stop()
break
print(threading.enumerate())
print(threading.enumerate())
线程安全
- CPtyhon中,GIL保证了python的读写操作为原子操作
- 单独操作字典的某一项item是安全的
- 但是遍历过程是不安全的,可以被时间片打断
- 如果字典正在遍历,其他线程尝试修改字典的SIZE,就会崩掉正在遍历字典的线程
- 加锁,虽然解决了线程安全问题,但是当字典Client较多的时候,这不是一个好的选择
import logging
import threading
import socket
import datetime
logging.basicConfig(format='%(asctime)s %(thread)d : %(message)s',level=logging.INFO)
class ChatServer:
def __init__(self,ip,port): # 启动聊天服务
self._addr = ip,port
self._server = socket.socket()
self.clients = {} # 存放客户端信息
self.event = threading.Event() # 线程同步
self.lock = threading.Lock()
def start(self): # 启动监听
self._server.bind(self._addr)
self._server.listen()
threading.Thread(target=self.s_accept,name='accept').start()
def s_accept(self): # 无限连接线程
while not self.event.is_set(): # 主线程退出,self.event置1,循环终止
try:
s_conn,c_addr = self._server.accept() # 注意,s_conn如果设置为self.s_conn,当某个Client端退出,会终止self.s_conn,且死循环
except Exception as e: # 当self._server关闭,抛异常,捕获记录且终止循环
logging.info(e)
break
with self.lock: # 确保字典增加的时候,其他线程不会遍历
self.clients[c_addr] = s_conn
threading.Thread(target=self.s_recv,args=(s_conn,c_addr)).start()
def s_recv(self,sock:socket.socket,client): # 接收客户端数据
while not self.event.is_set(): # 如果使用self.conn,造成死循环
c_info = sock.recv(1024) # 如果Client端主动断开,此时sock连接断开,抛异常
if c_info == b'' or c_info == b'quit':
with self.lock: # 确保字典的删除
self.clients.pop(client) # Client退出,并退出
sock.close() # 与其通信的socket对象关闭
break
logging.info(('c_info is ',c_info))
msg = '{:%Y/%m/%d %H:%M:%S} {}:{} {}'.format(datetime.datetime.now(),*client,c_info).encode()
with self.lock: # 确保字典遍历完成,但是字典越大,效率越高
for c in self.clients.values(): # 遍历,实现多个Client端互通
c.send(msg)
def stop(self): # 停止服务
self.event.set() # 停止服务,线程时间置1,s_accept和s_recv线程循环退出
with self.lock:
for s in self.clients.values(): # 确保字典遍历
s.close()
self._server.close()
c_s = ChatServer(ip='127.0.0.1',port=9999)
c_s.start()
while True:
cmd = input('>>>>>>').lower().strip()
if cmd == 'quit':
c_s.stop()
break
print(threading.enumerate()) # 测试用
print(threading.enumerate())
尝试
此处存疑,线程退出会进入死循环,暂时未找到原因
import logging
import threading
import socket
logging.basicConfig(format='%(asctime)s <%(thread)d %(threadName)s>, %(message)s',level=logging.INFO,
datefmt='%Y/%m/%d %H:%M:%S')
class ChatServer: # Server端
def __init__(self,ip='127.0.0.1',port=9999): # Server端需要ip地址和port端口
self.addr = ip,port
self._server = socket.socket() # 创建好server端的socket对象,不着急监听;TCP协议,IP v4地址
self.event = threading.Event() # 用于进程间同步
self.s_pool = {} # 使用字典,key为Client的addr,value为Server端专门连接Client端的socket对象
self.lock = threading.Lock()
def start(self):
self._server.bind(self.addr)
self._server.listen() # 主线程的Server端socket对象,只用来监听
threading.Thread(target=self.s_accept,name='s_listen',daemon=True).start() # accept阻塞主线程,开启工作线程专门用来接收Client的连接请求
# deamon线程,可以不用设置,服务器退出,监听线程退出
def s_accept(self):
while not self.event.is_set():
try:
self.s_connect,self.c_addr = self._server.accept() # rece接收Client端的信息,会阻塞
except Exception as e:
logging.info(e) # 如果服务器关闭,self._server不存在,导致赋值语句失败,捕获异常并记录
break
with self.lock:
self.s_pool[self.c_addr] = self.s_connect # 字典的增加
threading.Thread(target=self.s_recv,args=(self.s_connect,self.c_addr)).start() # 每连接一个Client端,创建一个工作线程,为每个Client端服务
def s_recv(self,connect,addr):
while not self.event.is_set(): # 线程同步,self.event置1,退出循环
c_data = connect.recv(1024)
with self.lock:
if c_data == b'' or c_data == b'quit':
self.s_pool.pop(addr) # 字典的减少
break
logging.info(c_data)
# CPython解释器中,GIL保证了多线程中的内建数据结构的读写操作是原子性的,
# 但是,循环遍历不是原子操作,有可能遍历的时候,其他线程在修改字典的大小,且字典遍历的时候,不能改变size,然后崩掉Client端的线程
for c in self.s_pool.values(): # 遍历字典,给每一个Client端发送信息
c.send(c_data)
def stop(self):
with self.lock:
for s in self.s_pool: # 字典的遍历
s.close() # 关闭每一个和Cilent端连接的socket对象
self.event.set() # 同步事件,置1
self._server.close() # 关闭服务器
pass
chat_s = ChatServer()
chat_s.start()
print('~~~~~~~~')
while True:
s_stop = input('>>>>>>>').lower().strip()
if s_stop == 'quit':
threading.Event().wait(3)
print('clear is over')
chat_s.stop()
break
print(threading.enumerate())
print(threading.enumerate())
测试多线程中,字典的遍历、字典size的变化(减少、增加)、字典值修改的安全性
- 创建两个deamon工作线程
- 一个线程无限循环向字典中增加元素
- 一个线程无限循环遍历字典且同时修改元素的值
- 结论
- 字典的每次读 / 写都是原子操作,循环不是原子操作
- 多线程中,使用字典增删遍历,是线程不安全的,需要锁
- 但是上锁,其他线程会一直阻塞,当字典中元素过多,遍历时间较长的时候,不是一个好选择
import logging
import time
import threading
import random
logging.basicConfig(format='%(asctime)s %(thread)s %(threadName)s %(message)s',level=logging.INFO)
target = {}
def additem(d:dict):
count = 1
while True:
time.sleep(0.1)
d[count] = random.randrange(1,101)
count += 1
def iterdict(d:dict):
while True:
for k,v in d.items():
time.sleep(0.1)
logging.info((k,v))
d[k] = random.randint(1,100)
# 使用daemon线程,主线程退出,kill所有daemon线程
threading.Thread(target=additem,args=(target,),name='add_thread',daemon=True).start()
threading.Thread(target=iterdict,args=(target,),name='iter_thread',daemon=True).start()
while True:
time.sleep(1)
print(threading.enumerate()) # 每隔一秒,打印一次,正常3个线程
if threading.active_count() <= 2: # 字典遍历的时候,不能修改size,崩掉遍历的线程
keys = list(target.keys())
print('=====end=====')
print(keys)
break
Client端
- Client端不需要bind(),不需要listen(),不需要accept()
- 只需要创建socket对象之后connect()
- 需要两个线程,主线程用来send()信息;工作线程一直recv(),阻塞等待Server端的回复。
import socket
import threading
import logging
import datetime
logging.basicConfig(format='%(asctime)s -- %(thread)d : %(message)s',level=logging.INFO,datefmt='%Y/%m/%d %H:%M:%S')
class Client:
def __init__(self,ip='0.0.0.0',port=9999):
self._addr = (ip,port)
self._client = socket.socket()
self.event = threading.Event()
def start(self):
self._client.connect(self._addr) # 注意:Client端不需要bind(),不需要listen(),不需要accept()
threading.Thread(target=self.recv,daemon=True).start() # 主线程退出,工作线程也退出
def send(self,info):
self._client.send(info.encode())
def recv(self):
while not self.event.is_set():
msg = '{:%Y/%m/%d %H:%M:%S} {} {}'.format(datetime.datetime.now(),*self._addr)
data = self._client.recv(1024)
logging.info(data)
def stop(self):
self._client.close()
def main():
c = Client('127.0.0.1')
c.start()
while True:
cmd = input('>>>>>')
if cmd.lower().strip() == 'quit':
c.stop()
break
c.send(cmd)
if __name__ == '__main__':
main()