socket是非常底层的接口库,它是一种通用的网络编程接口(意思就是java/c++/python都一样),和网络层次没有一一对应关系,而是看你在哪一层进行编程。
cs编程:client-server,bs(browser-server)就是一种cs。只做c开发,就是前端开发,只做s后端开发
socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
socket.AF_UNIX:
我们都知道socket本来是为网络通讯设计,可以通过socket方便的实现不同机器之间的通信。当然通过socket也可以实现同一台主机之间的进程通信。但是通过socket网络协议实现进程通信效率太低,后来就出现了IPC通信协议,UNIX Domain Socket (UDS)就是其中之一,而且用于IPC更有效率,比如:数据不再需要经过网络协议栈,也不需要进行打包拆包、计算校验和、维护序列号和及实现应答机制等,要做的只是将应用层数据从一个进程拷贝到另一个进程。
类似于Socket的TCP和UDP,UNIX Domain Socket也提供面向流和面向数据包两种实现,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。UNIX Domain Socket是全双工的,在实现的时候可以明显的发现客户端与服务端通信是通过文件读写进行的通信,而且可以同时读写。相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,如果用过supervisor的同学会发现在配置supervisor的时候需要设置一个supervisor.sock的问题地址,不难猜出,supervisor即使用unix socket来进行通信的。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。但是我们通过Golang或者其他高级语言实现的时候,会掩盖很多底层信息。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。所以在实现的时候,可以再启动的时候删掉sock文件,也可以在程序的signal捕获到退出时删除sock文件。
type:
socket.SOCK_STREAM tcp协议
socket.SOCK_DGRAM UDP协议
1. 创建一个socket对象(假设叫listen_sock
)。socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
2. 绑定ip地址和端口,bind()
方法: ip地址与internet层绑定。为什么要绑定端口,端口是应用层协议,一个应用程序对应一个端口,如果不绑定端口,消息发过来后怎么知道是与哪个应用程序通信呢?
3. 打开监听,listen()
方法: 光绑定没用啊,不监听服务就起不来。所以第三步是打开监听。监听后就在这个地址的这个端口上开启了一个应用程序服务,占用一个socket。用户根据这个地址端口请求服务,相当于在请求背后的应用程序,这个应用程序进程中的线程在实际处理A端口上的请求。
监听:像门童,等待顾客。顾客到了:你好,顾客请跟我来。并招呼某某服务员(新的Socket对象),请带客户到餐桌,服务员为客户服务-点餐、吃饭
4. 接受用户连接请求,返回与用户建立连接的socket对象,传送数据,listen_sock.accept()
: 用户发起请求建立一个连接,就占用一个socket资源,而一个socket占用一个文件描述符。所以每一个用户请求的连接需要建立一个新的socket对象。
listen_sock.accept()
返回一个socket对象(假设叫new_sock)和客户端IP二元组。用户的请求在这个新的socket对象中处理。这个新的Socket对象就是领顾客到餐桌的服务员。
在新的socket对象new_sock接受数据:new_sock.recv(bufsize[,flag])
在新的socket对象new_sock发送数据:new_sock.send(bytes)
;
sendfile()
方法,python3.5之后引入:在内核态就把数据发送了,减少用户态和内核态的拷贝过程,提高性能,new_sock.sendfile(file,offset=0,count=None)
示例:
import socket
import time
import logging
import datetime
def server_foo():
listen_sock=socket.socket() # 1、建立socket对象
ip='127.0.0.1'
port=9998
addr=(ip,port) # 注意,每个addr只能bind一次
listen_sock.bind(addr) # 2、绑定IP和端口
listen_sock.listen() # 3、监听端口(至此服务器就启起来了)。注意,监听服务只管监听新的请求,不与请求建立连接
time.sleep(1)
logging.info('socket:{}'.format(listen_sock))
conn, client_addr = listen_sock.accept() # 4、接受连接请求,建立连接,传输数据。允许别人连接,有人来请求连接,就派发一个新的socket伙伴conn手拉手玩去。
# sock.accept()默认阻塞的,所以,执行程序后,这一行日志一直不打印。阻塞着一直等用户请求。当用户发来请求后,返回一个新的socket,用户的请求在新的socket对象处理。sock.accept()继续阻塞监听其他的用户请求。
logging.info('accept:{}'.format(conn))
logging.info('client address:{}'.format(client_addr))
data=conn.recv(1024) # 接收数据。参数`bufsize`--个缓冲区队列,一般取1024的倍数
logging.info('data:{}'.format(data))
conn.send('ack }}: {}'.format(data.decode(),datetime.datetime.now()).encode())#发送数据
listen_sock.close() # 5、释放资源。为什么,因为占用了一个文件描述符,得释放呀
conn.close()
# accept和recv都是阻塞的。等IO、等文件描述符、等网络,都是IO请求,IO密集型。线程被阻塞了,这种就不适合放在主线程中,而应该开工作线程。
if __name__ == "__main__":
# netstat -anp tcp |find "9998"
server_foo()
import socket
import logging
def client_foo():
client_sock=socket.socket() # 1、建立socket对象
ip='127.0.0.1'
port=9998
addr=(ip,port) # 注意,每个addr只能bind一次
client_sock.connect(addr) # 2、连接服务器
data = client_sock.recv(1024) # 3、接收数据
logging.info('accept:{}'.format(data.decode()))
client_sock.send(data) # 发送数据
client_sock.close() # 释放资源
属性 | 含义 |
---|---|
socket.makefile() |
把一个socket对象包装成一个文件对象fileObj,然后把这个socket对象的数据收、发过程当文件处理 |
sock.getpeername() |
获取socket远端的地址(address, port) |
sock.getsockname() |
获取socket的本段自己的地址(address, port) |
socket.recvfrom() |
返回一个二元组,数据和远端的地址。 |
socket.sendfile(file,offset=0,count=None) |
在内核态就把数据发送了,减少用户态和内核态的拷贝过程,提高性能 |
写一个群聊工具server端。 要求:
代码实现
import socket
import threading
import time
from tool.logger_define import LoggerDefine
logger = LoggerDefine(__file__).get_logger
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9998):
self.addr = (ip, port)
self.clients = dict()
self.listen_sock = socket.socket()
self.event = threading.Event()
self.worker_num = 1
def start(self):
logger.info('Start server')
self.listen_sock.bind(self.addr)
self.listen_sock.listen()
threading.Thread(target=self._accept).start()
def _accept(self):
while not self.event.is_set():
try:
conn, client_addr = self.listen_sock.accept()
except OSError as e:
logger.error("connection closed. error: {}".format(e))
break
self.clients[client_addr] = conn
logger.info('Start e worker thread:worker_{} to handler request:{}'.format(self.worker_num, client_addr))
threading.Thread(
target=self._recv, args=(conn, client_addr), name='worker_{}'.format(self.worker_num)).start()
self.worker_num += 1
def _recv(self, conn: socket.socket, client_addr):
while not self.event.is_set():
try:
data = conn.recv(1024)
except ConnectionAbortedError as e:
logger.error('break connection:{}, error:{}'.format(client_addr[0], e))
self.clients.pop(client_addr)
break
trans_data = data.decode()
logger.info("{} Recv Data:{}".format(threading.current_thread().name, trans_data))
if trans_data.strip() == 'quit':
self.clients.pop(client_addr)
logger.info('close connection:{}'.format(client_addr[0]))
conn.close()
break
new_data = "{} say:{}".format(client_addr[0], trans_data)
logger.info("{} Ack:{}".format(threading.current_thread().name, new_data))
for tmp_key in list(self.clients.keys()):
self._send(tmp_key, new_data)
def _send(self, addr, data):
conn = self.clients.get(addr)
try:
conn.send(data.encode())
except ConnectionError as e:
logger.error("Error:{}, ip:{}, delete it".format(e, addr[0]))
self.clients.pop(addr)
def stop(self):
logger.info('Stop connection client.')
for addr in list(self.clients.keys()):
conn = self.clients.get(addr)
try:
conn.close()
except ConnectionAbortedError as e:
logger.error('Error:{}, pass'.format(e))
self.event.wait(3)
self.event.set()
logger.info('stop listen socket.')
self.listen_sock.close()
def show_thread(_event: threading.Event):
while not _event.is_set():
logger.info("Current thread:{}".format(threading.enumerate()))
time.sleep(6)
if __name__ == "__main__":
# netstat -anp tcp |find "9998"
my_chat = ChatServer()
my_chat.start()
event = threading.Event()
threading.Thread(target=show_thread, args=(event,), daemon=True).start()
while True:
cmd = input(">>>").strip()
if cmd == "quit":
my_chat.stop()
break
time.sleep(2)
logger.info('exist server')
实现TCP client端:与服务器建立连接、发生数据、接收数据、主动断开连接
代码实现
import socket
import threading
from tool.logger_define import LoggerDefine
logger = LoggerDefine(__file__).get_logger
class MyTCPClient:
def __init__(self, addr='127.0.0.1', port=9998):
self.addr = (addr, port)
self.sock = socket.socket()
self.ev = threading.Event()
def start(self):
logger.info("start client")
self.sock.connect(self.addr)
threading.Thread(target=self.recv_data, name="recv_data").start()
def recv_data(self):
while not self.ev.is_set():
try:
data = self.sock.recv(1024)
except ConnectionError as e:
logger.error("ERROR:{}".format(e))
self.sock.close()
break
logger.info("Recv:{}".format(data.decode()))
def send_data(self, data: bytes = b"quit"):
try:
self.sock.send(data)
except OSError as e:
self.stop()
logger.error("ERROR:{}".format(e))
raise e
def stop(self):
self.sock.close()
self.ev.wait(3)
self.ev.set()
if __name__ == '__main__':
my_client = MyTCPClient()
my_client.start()
while True:
cmd = input(">>>").strip()
try:
my_client.send_data(cmd.encode())
except OSError as e:
my_client.stop()
logger.error("ERROR:{}".format(e))
break
if cmd == "quit":
my_client.stop()
break
logger.info("Stop client")
UDP是无连接协议,它基于一个假设:
但是,即使是在局域网,也不能保证不丢包,而且包的到达不一定高序.
应用场景
一般来说, UDP性能优于TCP,但是可靠性要求高的场合的还是要选择TCP协议.
server端:
socket.recvfrom(1024)
, socket.sendto(addr)
socket.close()
client端:
import socket
import logging
def server_foo():
client_sock=socket.socket() # 1、建立socket对象
ip='127.0.0.1'
port=9998
addr=(ip,port) # 注意,每个addr只能bind一次
client_sock.connect(addr) # 2、连接服务器
data = client_sock.recv(1024) # 3、接收数据
logging.info('accept:{}'.format(data.decode()))
client_sock.send(data) # 发送数据
client_sock.close() # 释放资源
def client_foo():
client_sock=socket.socket() # 1、建立socket对象
ip='127.0.0.1'
port=9998
addr=(ip,port) # 注意,每个addr只能bind一次
client_sock.connect(addr) # 2、连接服务器
data = client_sock.recv(1024) # 3、接收数据
logging.info('accept:{}'.format(data.decode()))
client_sock.send(data) # 发送数据
client_sock.close() # 释放资源
代码实现
import datetime
import socket
import threading
from tool.logger_define import LoggerDefine
logger = LoggerDefine(__file__).get_logger
class MyUDPChatServer:
def __init__(self, addr='127.0.0.1', port=9998):
self.addr = (addr, port)
self.sock = socket.socket(type=socket.SOCK_DGRAM)
self.ev = threading.Event()
self.clients = dict()
self.interval = 10
def start(self):
logger.info("start server")
self.sock.bind(self.addr)
threading.Thread(target=self.recv_data, name='recv_data').start()
def recv_data(self):
while not self.ev.is_set():
try:
data, client_addr = self.sock.recvfrom(1024)
except OSError as e:
logger.error("ERROR:{}".format(e))
continue
reg_time = datetime.datetime.now().timestamp()
msg = data.decode()
if msg == "quit":
if self.clients.get(client_addr):
self.clients.pop(client_addr)
continue
elif msg == "^o^" or msg == "reg":
logger.info("first reg, pass")
self.clients[client_addr] = reg_time
continue
if not self.clients.get(client_addr):
self.clients[client_addr] = reg_time
self.send_data(msg)
def send_data(self, data: str = "quit"):
cur_time = datetime.datetime.now().timestamp()
ack_msg = "ack:{}".format(data)
logger.info(ack_msg)
for client_addr in list(self.clients.keys()):
reg_time = self.clients.get(client_addr)
if cur_time - reg_time > 10:
self.clients.pop(client_addr)
continue
try:
self.sock.sendto(ack_msg.encode(), client_addr)
except OSError as e:
logger.error("error:{}".format(e))
self.clients.pop(client_addr)
def stop(self):
self.send_data()
self.ev.set()
self.sock.close()
self.ev.wait(3)
def show_thread():
ev = threading.Event()
while not ev.wait(6):
logger.info(threading.enumerate())
if __name__ == '__main__':
my_server = MyUDPChatServer()
my_server.start()
threading.Thread(target=show_thread, name="show_thread", daemon=True).start()
while True:
cmd = input(">>>").strip()
if cmd == "quit":
my_server.stop()
break
logger.info('Stop server')
实现TCP client端:与服务器建立连接、发生数据、接收数据、主动断开连接
ack机制和心跳heartbeat.
心跳,就是一端定时发往另一端的信息,一般每次数据越少越好。心跳时间间隔约定好就行。ack即响应,一端收到另一端的消息后返回的信息。
import socket
import threading
from tool.logger_define import LoggerDefine
logger = LoggerDefine(__file__).get_logger
class MyUDPClient:
def __init__(self, addr='127.0.0.1', port=9998, interval=3):
self.addr = (addr, port)
self.sock = socket.socket(type=socket.SOCK_DGRAM)
self.ev = threading.Event()
self.interval = interval
def start(self):
self.sock.connect(self.addr)
# self.sock.sendto("reg".encode(), self.addr) # 使用sendto替换connect,目的都是初始注册。使用sendto时,sever端要增加判断,不用ack注册信息,使用sendto时,
threading.Thread(target=self.recv_data, name='recv_data').start()
threading.Thread(target=self.send_heartbeat, name='heartbeat', daemon=True).start() # 心跳线程最适合用daemon了
def recv_data(self):
while not self.ev.is_set():
try:
data = self.sock.recv(1024)
except ConnectionError as e:
logger.error("Error:{}".format(e))
self.stop()
raise e
except Exception as e:
logger.error("Error:{}".format(e))
self.stop()
raise e
logger.info("recv:{}".format(data.decode()))
def send_heartbeat(self):
while not self.ev.wait(self.interval):
try:
self.sock.send("^o^".encode()) # 心跳包不宜过长,且不能是常用发送信息
except Exception as e:
logger.error("Error:{}".format(e))
self.stop()
break
def send_data(self, data: str = "quit"):
try:
self.sock.send(data.encode())
except OSError as e:
logger.error("Error:{}".format(e))
self.stop()
raise e
except Exception as e:
logger.error("Error:{}".format(e))
self.stop()
raise e
def stop(self):
self.ev.set()
self.sock.close()
logger.info("stop client")
if __name__ == '__main__':
logger.info("start")
my_client = MyUDPClient()
my_client.start()
while True:
cmd = input(">>>").strip()
if cmd == "quit":
my_client.send_data()
my_client.stop()
break
my_client.send_data(cmd)
logger.info('end')
socketserver模块,是对底层接口socket的封装。帮助用户写出健壮的代码。用户不用关心怎么去实现socket编程,而是专注于利用socket编程。
socketserver
模块提供的不同的类,但是编程接口是一样的,即使是多进程、多线程的类也是一样,大大减少了编程的难度。socketserver简化了网络服务器的编写。
BaseServer下,有四个子类:TCPServer、UDPServer、UnixStreamServer、UnixDatagramServer
通过两个Mixin类(ThreadingMixIn和ForkingMixIn)来支持异步:ForkingTCPServer(ForkingMixIn,TCPServer)…
编程接口:socketserver.BaseServer(server_address,RequestHandlerClass)
:用户提供服务器需要绑定的地址和端口,以及用于处理请求的RequestHandlerClass类,即可。
BaseServer.finish_request()
,这个方法会实例化处理请求方法。BaseServer.serve_forever
方法:建立长连接,忽略超时。而handle_request方法有超时现在BaseServer.shutdown
: stops the serve_foreverBaseServer.server_close
:关闭socketRequestHandlerClass
:处理用户请求的处理类。 BaseServer实例接收用户请求后,最后会实例化这个类。
创建server一般步骤:
使用
server_forever
,时,释放资源先调用shutdown
停止server_foreve
r,再调用server_close
。而使用handler_request
时,就不用调用shutdown
,直接server_close
socketserver
模块实现群聊import socket
import socketserver
import threading
from socketserver import BaseRequestHandler, BaseServer
from tool.logger_define import get_log
from tool.show_thread import show_thread
logger = get_log(__name__) # 自定义日志模块
class MyHandler(BaseRequestHandler):
clients = dict()
def __init__(self, request: socket.socket, client_address: tuple, server: BaseServer) -> None:
self.ev = threading.Event()
super().__init__(request, client_address, server)
def setup(self) -> None:
self.clients[self.client_address] = self.request
super().setup()
def handle(self) -> None:
while not self.ev.is_set():
try:
data = self.request.recv(1024).decode().strip()
except Exception as e:
logger.error("error:{}".format(e))
break
if data == "quit":
logger.info("exit")
break
msg = "ack:{}".format(data).encode()
logger.info("recv:{}".format(data))
for client in list(self.clients.keys()):
conn = self.clients.get(client)
try:
conn.send(msg)
except Exception as e:
logger.error("Error:{}".format(e))
self.clients.pop(client)
def finish(self) -> None:
self.ev.set()
self.clients.pop(self.client_address)
super().finish()
def my_server(addr='127.0.0.1', port=9998):
address = (addr, port)
_tcp_server = socketserver.ThreadingTCPServer(address, MyHandler)
threading.Thread(target=_tcp_server.serve_forever, name="myserver").start()
return _tcp_server
if __name__ == '__main__':
logger.info("start server")
tcp_server = my_server()
show_thread() # 自定义查看线程工具
while True:
cmd = input(">>>").strip()
if cmd == "quit":
tcp_server.shutdown()
tcp_server.server_close()
break
logger.info("end")
总结:
ThreadingTCPServer
为每一个连接提供RequestHandleClass
类的实例, 一次调用setup、handle、finish方法,且使用了try... finally
结构保证finish方法一定被调用;