网络编程Socket简介
Python 中提供了socket.py 标准库,尤其是底层的socket 库。
Socket是通用的网络编程套接字,与网络层没有一一对应的关系。
契约族
AF表示AddressFamily,作为socket()的第一个参数。
名字含义
AF_INET
IPv4
AF_INET6
IPv6
AF_UNIX
UnixDomainSocket,不是 Windows
套接字类型
名字含义
SOCK_STREAM
面向链接的流套接字。默认,TCP 合约
SOCK_DGRAM
未连接的数据报套接字。 UDP 合约
TCP编程(CS编程)
Socket编程需要两端,通常需要一个服务器,一个客户端称为Server,客户端称为Client
TCP 服务器
服务器编程步骤
import socket
ipaddr = ("127.0.0.1", 9999)
with socket.socket() as server:
server.bind(ipaddr)
server.listen()
sl, ip = server.accept()
data = sl.recv(1024)
sl.send(data)
单向聊天的简单实现
import socket
ipaddr = ("127.0.0.1", 9999)
with socket.socket() as server:
server.bind(ipaddr)
server.listen()
s, raddr = server.accept() # 等待对方连接
with s as se:
while True:
try:
data = s.recv(1024) # 获取数据 等待数据
print('已接收到对方数据,信息如下')
print(data.decode(encoding='gbk'))
if data.decode('gbk') == 'exit':
break
data = input('回应对方数据:')
for i in range(2):
s.send(bytes(data, encoding='gbk')) # 回应数据
except ConnectionResetError:
print('对方已断开连接')
break
问题
如果你绑定同一个端口两次会发生什么
import socket
with socket.socket as server:
server.bind(('127.0.0.1', 9999))
server.listen()
s1, info = server.accept()
with s1:
data = s1.recv(1024)
print(data, info)
s1.send(b'okay1')
s2, info = server.accept()
with s2:
data = s2.recv(1024)
print(data, info)
s2.send(b'okay2')
上面accept和recv的列表被阻塞,逐渐从被阻塞到无法工作逐渐
练习
编写群聊程序
需求分析
聊天基础是CS程序,C是每个客户端,S是服务器端。
服务器应具备的功能:
服务器对应一个类
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 GlobalChatServer:
def __init__(self, ip: str = '127.0.0.1', port: int = 9999):
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() # 阻塞
self.clients[raddr] = s
logging.info(s)
logging.info(raddr)
threading.Thread(target=self.recv, name="recv", args=(s,)).start()
def recv(self, sock):
while not self.event.is_set():
try:
data = sock.recv(1024) # 阻塞
except Exception as e:
logging.info(e)
data = b'quit'
if data == b'quit':
self.clients.pop(sock.getpeername())
break
logging.info(data)
msg = 'ack{} {} {}'.format(
sock.getpeername(),
datetime.datetime.now().strftime("%Y/%m/%d-%H:%M:%S"),
data.decode()).encode('gbk')
for s in self.clients.values():
s.send(msg)
def stop(self):
for i in self.clients.values():
i.close()
self.sock.close()
self.event.set()
cs = GlobalChatServer()
cs.start()
while True:
cmd = input(">>>")
if cmd.strip() == 'exit':
cs.stop()
threading.Event.wait(3)
logging.info(threading.enumerate())
其他表示名称含义
socket.recv(bufsize[,flags])
获取数据,默认为阻塞方式
socket.recvfrom(bufsize[,flags])
获取数据,返回一个2元组(字节,地址)
socket.recv_into(buffer)[,nbytes[,flags]]
获取nbytes数据后,存入缓冲区。如果未指定 nbytes 或为 0应用编程接口和套接字,则缓冲区大小的数据存储在缓冲区中。返回接收到的字节数。
socket.recvfrom_into(buffer[,nbytes[,flags]])
获取数据并返回一个 2 元组(字节,地址)到缓冲区
socket.send(bytes[,flags])
TCP 发送数据
socket.sendall(bytes[,flags])
TCP发送所有数据,成功返回None
socket.sendfile(file,offset=0,count=None)
发送一个文件直到 EOF,使用高性能的 os.sendfile 机制,返回发送的字节数。如果win下不支持sendfile,或者不是普通文件,使用send()发送文件。偏移量告诉起始位置。 3.第 5 版开始
发送文件:
socket.makefile(mode='r', buffering=None, *, encofing=None, errors=None, newline=None)
创建一个与socket关联的文件对象,把recv当作读应用编程接口和套接字,把send当作写
# 使用makefile
import socket
sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
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(line)
f.write('Return your msg:{}'.format(line))
f.flush()
以上列表不能循环消息
import socket
import threading
sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
addr = (ip, port)
sockserver.bind(addr)
sockserver.listen()
print('-'*30)
event = threading.Event()
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': # 注意要发quitn
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()
返回连接socket的远程地址,返回值通常是一个元组(ipaddr,port)
socket.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr, port)
socket.setblocking(标志)
如果flag为0,则设置socket为非阻塞模式,否则设置socket为阻塞模式(默认)
在非阻塞模式下,如果调用recv()没有找到数据或者调用send()没有立即发送数据,会导致socket.error异常
socket.settimeout(值)
设置socket的超时时间,timeout是以秒为单位的浮点数。 None 值表示没有超时期限。一般来说,应该在第一次创建套接字时设置超时,因为它们可能与连接操作一起使用(例如 connect())
socket.setsockopt(level,optname,value)
设置套接字选项的值。例如缓冲区大小。更详细的查看相关文档,不同的系统,不同的版本不一样。
练习
使用makefile编译群聊
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 GlobalChatServer:
def __init__(self, ip: str = '127.0.0.1', port: int = 9999):
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() # 阻塞
f = s.makefile(mode='rw')
self.clients[raddr] = f
logging.info(f)
logging.info(s)
logging.info(raddr)
threading.Thread(target=self.recv, name="recv", args=(f, raddr)).start()
def recv(self, f, raddr):
while not self.event.is_set():
try:
# data = f.recv(1024) # 阻塞
data = f.readline() # string, 会等待换行符n
logging.info(data)
except Exception as e:
logging.error(e)
data = b'quit'
if data == b'quit':
self.clients.pop(raddr)
break
msg = 'ack{} {} {}'.format(
raddr,
datetime.datetime.now().strftime("%Y/%m/%d-%H:%M:%S"),
data)
for s in self.clients.values():
f.write(msg)
f.flush()
def stop(self):
for i in self.clients.values():
i.close()
self.sock.close()
self.event.set()
# cmd = input(">>>")
cs = GlobalChatServer()
cs.start()
while True:
cmd = input(">>>")
if cmd.strip() == 'exit':
cs.stop()
threading.Event.wait(3)
logging.info(threading.enumerate())
上面列出了基本功能,如果客户主动断开或者readline异常,无效的socket不会从客户中移除,可以通过异常处理来解决这个问题
全球聊天服务器
注意,这段代码是实验用的,代码还是有很多缺陷的。 Socket太低级了,实际开发中很少用到这么低级的socket。
添加一些异常处理