目录
Socket介绍
socket套接字:Python中提供socket.py标准库,非常底层的接口库
Socket是一种通用的网络编程接口,和网络层次没有一一对应的关系
Socket文件本身就是传输用, 传输的话就要有一个东西来缓冲,说到地就是BUffer,BUffer其实就是个队列协议族:AF表示Address Family,用于socket() 第一个参数
名称 含义 AF_INET IPV4 AF_INET6 IPV6 AF_UNIX Unix Domain Socket,window没有 Socket类型
名称 含义 SOCK_STREAM 面向连接的流套接字。默认值,TCP协议 SOCK_DGRAM 无连接的数据报文套接字。UDP协议
TCP编程
Socket编程,需要两端,一般来说需要一个服务端,一个客户端,服务端称为Server,客户端称为Client
TCP服务端编程【服务端编程步骤】
- 创建Socket对象
- 绑定IP地址Address和端口Port。bind()方法;IPv4地址为一个二元组("IP地址字符串",Port)
- 开始监听,将在指定的IP的端口上监听。listen()方法
- 获取用户传送数据的Socket对象
- socket.accept() ----> (socket objcet,address info);accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组,地址是远程客户端的地址,IPv4中它是一个二元组(clientaddr,port)
- 接受数据:recv(bufsize,[, flags]) 使用缓冲区接收数据
- 发送数据:send(bytes) 发送数据
问题:两次绑定同一个监听端口会怎么样?
- accept,recv是阻塞的,主线程经常被阻塞而不能工作
import logging import socket import time #-*- coding:utf-8 -*- FORMAT='%(asctime)-15s\t [%(processName)s:%(threadName)s,%(process)d:%(thread)8d] %(message)s' logging.basicConfig(format=FORMAT,level=logging.INFO) socket_ob = socket.socket() #创建socket对象 socket_ob.bind(('127.0.0.1',9999)) #创建一个二元组 socket_ob.listen() #开始监听 #开启一个连接 socket_cob,info=socket_ob.accept() #阻塞直到和客户端成功建立连接,返回一个socket对象和客户端地址 #适用一个缓冲区获取数据 data_buff = socket_cob.recv(1024) #1024的倍数 logging.info(data_buff.decode("gbk")) socket_cob.send(b"day day up up") #开启另外一个连接 socket_cob2 ,info =socket_ob.accept() data_buff2 =socket_cob2.recv(1024) logging.info(data_buff2.decode("gbk")) socket_cob2.send(b"hello python") socket_ob.close() 结果: 2021-06-28 01:11:57,044 [MainProcess:MainThread,28864: 28184] 我有来了 2021-06-28 01:11:57,044 [MainProcess:MainThread,28864: 28184] 我来了
结果:
练习----写一个群聊程序
需求分析:
- 聊天工具是CS程序,C是每一个客户端,S是服务端
- 服务器应具有的功能
- 启动服务,包括绑定端口,监听
- 建立连接,能和多个客户端建立连接
- 接受不同用户的信息
- 分发,将接受的某个用户的信息转发到已连接的所有客户端
- 停止分发
- 记录连接的客户端
代码实现
1、基本框架实现
class ChatServer: def __init__(self): def start(self): pass def accept(self): pass def recv(self): pass def stop(self): pass
2、扩展代码,实现基本功能
import socket import threading import logging import datetime FORMAT ='%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO) class ChatServer: def __init__(self,ip='127.0.0.1',port=9999): #启动服务 self.socket_server =socket.socket() self.addr_server = (ip,port) self.client = {} #客户端 def start(self): #启动监听 self.socket_server.bind(self.addr_server) #启动服务 self.socket_server.listen() #监听服务 #accept会阻塞主线程,所以开一个新线程 threading.Thread(target=self.accept,name="accept").start() def accept(self): #多人连接 while True: client_socket ,client=self.socket_server.accept() #阻塞 self.client[client] = client_socket #添加到客户端字典 #准备接受数据,recv 是阻塞的,开启新的线程 threading.Thread(target=self.recv,args=(client_socket,client),name="recv").start() def recv(self,clent_socket:socket.socket,client): while True: recv_data = clent_socket.recv(1024) #阻塞接受 recv_msg = '{:%Y%m%d %H:%M:%S} {}:{}\n{}\n '.format(datetime.datetime.now(),*client,recv_data.decode("gbk")) logging.info(recv_msg) logging.info(threading.enumerate()) recv_msg_en = recv_msg.encode() for s in self.client.values(): s.send(recv_msg_en) def stop(self): for s in self.client.values(): s.close() self.socket_server.close() cs = ChatServer() cs.start() 结果: D:\Python3.7.5\python.exe D:/rsa_code/tvp_chart_server.py INFO:root:20210703 17:45:39 127.0.0.1:52012 no1 INFO:root:[<_MainThread(MainThread, stopped 7288)>, <Thread(accept, started 5584)>, <Thread(recv, started 10264)>] INFO:root:20210703 17:45:58 127.0.0.1:52013 no2 INFO:root:[<_MainThread(MainThread, stopped 7288)>, <Thread(accept, started 5584)>, <Thread(recv, started 10264)>, <Thread(recv, started 25544)>] INFO:root:20210703 17:48:18 127.0.0.1:52034 no3 INFO:root:[<_MainThread(MainThread, stopped 7288)>, <Thread(accept, started 5584)>, <Thread(recv, started 10264)>, <Thread(recv, started 25544)>, <Thread(recv, started 6136)>] INFO:root:20210703 17:49:31 127.0.0.1:52034 中途有一个客户端退出,所有的都会报错 INFO:root:[<_MainThread(MainThread, stopped 7288)>, <Thread(accept, started 5584)>, <Thread(recv, started 10264)>, <Thread(recv, started 25544)>, <Thread(recv, started 6136)>] Exception in thread recv: Traceback (most recent call last): File "D:\Python3.7.5\lib\threading.py", line 926, in _bootstrap_inner self.run() File "D:\Python3.7.5\lib\threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "D:/rsa_code/tvp_chart_server.py", line 35, in recv recv_data = clent_socket.recv(1024) #阻塞接受 ConnectionAbortedError: [WinError 10053] 你的主机中的软件中止了一个已建立的连接。 INFO:root:20210703 17:49:43 127.0.0.1:52012 no1 INFO:root:[<_MainThread(MainThread, stopped 7288)>, <Thread(accept, started 5584)>, <Thread(recv, started 10264)>, <Thread(recv, started 25544)>] Exception in thread recv: Traceback (most recent call last): File "D:\Python3.7.5\lib\threading.py", line 926, in _bootstrap_inner self.run() File "D:\Python3.7.5\lib\threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "D:/rsa_code/tvp_chart_server.py", line 41, in recv s.send(recv_msg_en) ConnectionAbortedError: [WinError 10053] 你的主机中的软件中止了一个已建立的连接。
3、基本功能的完成, 但是有问题,适用Event改进【用Evetn确保程序退出的时候,全部退出】
import socket import threading import logging import datetime FORMAT ='%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO) class ChatServer: def __init__(self,ip='127.0.0.1',port=9999): #启动服务 self.socket_server =socket.socket() self.addr_server = (ip,port) self.client = {} #客户端 self.event=threading.Event() def start(self): #启动监听 self.socket_server.bind(self.addr_server) #启动服务 self.socket_server.listen() #监听服务 #accept会阻塞主线程,所以开一个新线程 threading.Thread(target=self.accept,name="accept").start() def accept(self): #多人连接 while not self.event.is_set(): client_socket ,client=self.socket_server.accept() #阻塞 self.client[client] = client_socket #添加到客户端字典 #准备接受数据,recv 是阻塞的,开启新的线程 threading.Thread(target=self.recv,args=(client_socket,client),name="recv").start() def recv(self,clent_socket:socket.socket,client): while not self.event.is_set(): recv_data = clent_socket.recv(1024) #阻塞接受 recv_msg = '{:%Y%m%d %H:%M:%S} {}:{}\n{}\n '.format(datetime.datetime.now(),*client,recv_data.decode("gbk")) logging.info(recv_msg) logging.info(threading.enumerate()) recv_msg_en = recv_msg.encode() for s in self.client.values(): s.send(recv_msg_en) def stop(self): for s in self.client.values(): s.close() self.socket_server.close() self.event.set() cs = ChatServer() cs.start() while True: cmd = input(">>>").strip() if cmd == "quit": cs.stop() threading.Event().wait(3) break
4、这一版本基本能用了,测试通过,但是还需要完善的地方:增加客户端退出命令
- 例如:各种异常的处理,客户端断开后字典中的移除客户端数据等
- 客户端主动断开带来的问题
- 服务端知道合适断开,如果客户端断开,服务器不知道
- 所以好的做法是,客户端开发出特殊消息通知服务器端,断开连接,但是,如果客户端主动断开,服务端主动发出空的消息,超时发出异常,捕获异常并处理异常
- 即使为客户端提供断开命令,也不能保证客户端会使用它断开连接,但是还是要增加这个退出功能
- 程序还有瑕疵,但是业务能力基本完成了
import socket import threading import logging import datetime FORMAT ='%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO) class ChatServer: def __init__(self,ip='127.0.0.1',port=9999): #启动服务 self.socket_server =socket.socket() self.addr_server = (ip,port) self.client = {} #客户端 self.event=threading.Event() def start(self): #启动监听 self.socket_server.bind(self.addr_server) #启动服务 self.socket_server.listen() #监听服务 #accept会阻塞主线程,所以开一个新线程 threading.Thread(target=self.accept,name="accept").start() def accept(self): #多人连接 while not self.event.is_set(): client_socket ,client=self.socket_server.accept() #阻塞 self.client[client] = client_socket #添加到客户端字典 #准备接受数据,recv 是阻塞的,开启新的线程 threading.Thread(target=self.recv,args=(client_socket,client),name="recv").start() def recv(self,clent_socket:socket.socket,client): while not self.event.is_set(): recv_data = clent_socket.recv(1024).decode("gbk").strip() #阻塞接受 #客户端退出 if recv_data == "quit": self.client.pop(client) clent_socket.close() logging.info("{} quits".format(client)) break recv_msg = '{:%Y%m%d %H:%M:%S} {}:{}\n{}\n'.format(datetime.datetime.now(),*client,recv_data) logging.info(recv_msg) logging.info(threading.enumerate()) for s in self.client.values(): s.send(recv_msg.encode()) def stop(self): #停止服务 for s in self.client.values(): s.close() self.socket_server.close() self.event.set() cs = ChatServer() cs.start() while True: cmd = input(">>>").strip() if cmd == "quit": cs.stop() threading.Event().wait(3) break logging.info(threading.enumerate()) #用来观察断开后线程的变化 结果: D:\Python3.7.5\python.exe D:/rsa_code/tvp_chart_server.py >>>INFO:root:20210703 22:31:36 127.0.0.1:62110 no1 INFO:root:[<_MainThread(MainThread, started 24548)>, <Thread(accept, started 5840)>, <Thread(recv, started 26420)>] INFO:root:20210703 22:31:46 127.0.0.1:62112 no2 INFO:root:[<_MainThread(MainThread, started 24548)>, <Thread(accept, started 5840)>, <Thread(recv, started 26420)>, <Thread(recv, started 24008)>] INFO:root:('127.0.0.1', 62112) quits INFO:root:20210703 22:32:13 127.0.0.1:62113 no3 INFO:root:[<_MainThread(MainThread, started 24548)>, <Thread(accept, started 5840)>, <Thread(recv, started 26420)>, <Thread(recv, started 26196)>] INFO:root:('127.0.0.1', 62113) quits
Socket常用方法
名称 含义 socket.recv(bufsize [ ,flags]) 获取数据。默认是阻塞方式 socket.recvfrom(bufsize[, flags]) 获取数据,返回一个二元组(bytes,address) sockt.recv_into(buffer[,nbytes[,flags]]) 获取到nbytes的数据后,存储到buffer中,如果nbytes没有指定或0,将buffer大小的数据存入buffer中,返回接受到字节数 socket.recvfrom_into(buffer[,nbytes[,flags]]) 获取数据,返回一个二元组(bytes,addres)到buffer中 socket.send(bytes,[, flags]) TCP发送数据 socket.sendall(bytes[,flags]) TCP发送全部数据,成功返回None socket.sendto(string[,flag],address) UDP发送数据 socket.sendfile(file,offset=0,count=None) 发送一个文件直到EOF,适用高性能的os.sendfile机制,返回发送的字节数,如果win下不支持sendfile,或者不是普通文件,使用send() 发送文件,offset高速起始位置,。3.5版本开始 socket.getpeername() 返回连接套接字的远程地址,返回值通常是元组(ipaddr,port) socket.getsockname() 返回套接字自己的地址,通常是一个元组(ipaddr,port) socket.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)
非阻塞模式下,如果调用recv() 没有发现任何数据,或send() 调用无法立即发送数据,那么将引起socket.error异常
socket.settimeout(value) 设置套接字的朝时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,朝时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect() ) socket.setsockopt(level,optname,value) 设置套接字的值,比如缓冲区大小,太多了,去看文档。不同系统,不同版本都不尽相同
MakeFile
def makefile(self, mode="r", buffering=None, *,encoding=None, errors=None, newline=Non
创建一个与改套接字相关的文件对象,将recv方法看作读方法,将send方法看作写方法
使用makefile 改写群聊
import socket import threading import logging import datetime FORMAT ='%(asctime)s %(thread)d %(message)s' logging.basicConfig(level=logging.INFO) class ChatServer: def __init__(self,ip='127.0.0.1',port=9999): #启动服务 self.socket_server =socket.socket() self.addr_server = (ip,port) self.client = {} #客户端 self.event=threading.Event() def start(self): #启动监听 self.socket_server.bind(self.addr_server) #启动服务 self.socket_server.listen() #监听服务 #accept会阻塞主线程,所以开一个新线程 threading.Thread(target=self.accept,name="accept").start() def accept(self): #多人连接 while not self.event.is_set(): client_socket ,client=self.socket_server.accept() #阻塞 #转杯接受数据,recv时阻塞的,开启新的线程 f = client_socket.makefile("rw") self.client[client] = f #添加到客户端字典 #准备接受数据,recv 是阻塞的,开启新的线程 threading.Thread(target=self.recv,args=(f,client),name="recv").start() def recv(self,f,client): while not self.event.is_set(): data = f.readline().strip() #阻塞换行符 #客户端退出 if data == "quit": self.client.pop(client) f.close() logging.info("{} quits".format(client)) break recv_msg = '{:%Y%m%d %H:%M:%S} {}:{}\n{}\n'.format(datetime.datetime.now(),*client,data) logging.info(recv_msg) logging.info(threading.enumerate()) for s in self.client.values(): s.write(recv_msg) s.flush() def stop(self): #停止服务 for s in self.client.values(): s.close() self.socket_server.close() self.event.set() cs = ChatServer() cs.start() while True: cmd = input(">>>").strip() if cmd == "quit": cs.stop() threading.Event().wait(3) break logging.info(threading.enumerate()) #用来观察断开后线程的变化
上例完成了基本功能,但是如果客户端主动断开,或者readline出现异常,就不会从clients中移除作废的socket,可以使用异常处理解决这个问题
ChatServer实验用完整代码
import socket
import threading
import logging
import datetime
FORMAT ='%(asctime)s %(thread)d %(message)s'
logging.basicConfig(level=logging.INFO)
class ChatServer:
def __init__(self,ip='127.0.0.1',port=9999): #启动服务
self.socket_server =socket.socket()
self.addr_server = (ip,port)
self.client = {} #客户端
self.event=threading.Event()
def start(self): #启动监听
self.socket_server.bind(self.addr_server) #启动服务
self.socket_server.listen() #监听服务
#accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept,name="accept").start()
def accept(self): #多人连接
while not self.event.is_set():
client_socket ,client=self.socket_server.accept() #阻塞
#转杯接受数据,recv时阻塞的,开启新的线程
f = client_socket.makefile("rw")
self.client[client] = f #添加到客户端字典
#准备接受数据,recv 是阻塞的,开启新的线程
threading.Thread(target=self.recv,args=(f,client),name="recv").start()
def recv(self,f,client):
while not self.event.is_set():
try:
data = f.readline() #阻塞换行符
except Exception as e:
logging.error(e) #有任何异常退出
data = "quit"
data = data.strip()
#客户端退出
if data == "quit":
self.client.pop(client)
f.close()
logging.info("{} quits".format(client))
break
recv_msg = '{:%Y%m%d %H:%M:%S} {}:{}\n{}\n'.format(datetime.datetime.now(),*client,data)
logging.info(recv_msg)
logging.info(threading.enumerate())
for s in self.client.values():
s.write(recv_msg)
s.flush()
def stop(self): #停止服务
for s in self.client.values():
s.close()
self.socket_server.close()
self.event.set()
def main():
cs = ChatServer()
cs.start()
while True:
cmd = input(">>>").strip()
if cmd == "quit":
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate()) #用来观察断开后线程的变化
if __name__ == '__main__':
main()