1. socket介绍
1.1 socket套接字
python中提供socket.py标准库,非常底层的接口库
socket是一种通用的网络编程接口,和网络层次没一一对应的关系
1.2 协议族
AF表示Address Family,用于socket()第一个参数
1.3 Socket类型
2. TCP编程
socket编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server,客户端称为Client。这种编程模式也称为CS编程。
2.1 TCP服务端编程
2.1.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)发送数据
TCP server端开发:
# TCP server 端开发
import socket
# import time
server = socket.socket() # TCP 连接 IPv4
ip = '127.0.0.1' # 本机回环地址,永远指向本机
port = 9999 # 建议使用1000以上; TCP 65536种状态
server.bind((ip, port)) # address,此方法只能绑定一次
server.listen() # 真正的显示出端口,监听不是阻塞函数
# time.sleep(100)
print(server)
# print(server.accept()) # 默认阻塞,不懂千万不要修改
new_socket, client_info = server.accept()
print(new_socket)
print('new_socket', client_info)
while True:
# new_socket.send(b'server ack')
data = new_socket.recv(1024) # 缺省情况下是阻塞的
print(data)
new_socket.send('server ack. data={}'.format(data.decode()))
new_socket.close()
# new2, client2 = server.accept() # 新的连接,之前的连接已经关闭,并且两次连接可能在不同的进程
# print(new2)
# print('new2', client2)
# data = new2.recv(1024)
# print(data)
# new2.send('server new2 ack. data={}'.format(data.encode()))
# new2.close()
server.close()
实战——写一个群聊程序
需求分析
聊天工具是CS程序,C是每一个客户端client,S是服务器端server
服务器应该具有的功能:
- 启动服务,包括绑定地址和端口,并监听
- 建立连接,能和多个客户端建立连接
- 接收不同用户的信息
- 分发,将接收的 某个用户的信息转发到已连接的所有客户端
- 停止服务
- 记录连接的客户端
代码实现
# tcp server 端开发
import socket
import threading
import logging
from datetime import datetime
FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.address = ip, port
self.event = threading.Event()
self.lock = threading.Lock()
self.clients = {}
def start(self):
self.sock.bind(self.address)
self.sock.listen()
threading.Thread(target=self.accept, name='accept').start()
def accept(self):
while not self.event.is_set():
try:
new_sock, client_info = self.sock.accept() # 阻塞等待连接
except Exception as e:
logging.error(e)
with self.lock:
self.clients[client_info] = new_sock
threading.Thread(target=self.rec, name='rec', args=(new_sock, client_info)).start()
def rec(self, sock, client):
while not self.event.is_set():
try:
data = sock.recv(1024) # 阻塞等待接收信息,接收信息也可能出现异常
except Exception as e:
logging.error(e)
data = b''
print(data.decode(encoding='cp936'))
if data.strip() == b'quit' or data.strip() == b'': # 客户端主动断开连接
with self.lock:
self.clients.pop(client) # 将断开连接的客户ip和端口从字典中移除,因为下文还要遍历字典
sock.close() # 此句比较耗时,可以放在锁的外面
break
msg = "{:%Y/%m/%d %H:%M:%S} [{}:{}] - {}".format(datetime.now(), *client, data.decode(encoding='cp936'))
exc = []
exs = []
with self.lock:
for c, s in self.clients.items(): # 遍历的是时候不允许别人pop和add,所以加锁
try:
s.send(msg.encode(encoding='cp936')) # 给所有的new_sock发送的信息。注意此句可能会出现异常,如网络断了
except Exception as e:
logging.error(e)
exc.append(c)
exs.append(s)
for c in exc:
self.clients.pop(c)
for s in exs: # 此句比较耗时,所以放在锁外面
s.close()
def stop(self):
self.event.set()
with self.lock:
values = list(self.clients.values())
self.clients.clear() # 这是一个好习惯
for s in values:
s.close()
self.sock.close()
cs = ChatServer()
cs.start()
while True:
cmd = input(">>>").strip()
if cmd == 'quit':
cs.stop()
break
logging.info(threading.enumerate())
logging.info(cs.clients)
socket常用方法
makefile
socket.makefile(mode=‘r’, buffering=None, *, encoding=None, error=None, newline=None)
创建一个与该套接字相关联的文件对象,将recv方法看做读方法,将send方法看做写方法。
import socket
s = socket.socket()
s.bind(('127.0.0.1', 9999))
s.listen()
s1, info = s.accept()
print(s1.getpeername()) # ('127.0.0.1', 55934)
print(s1.getsockname()) # ('127.0.0.1', 9999)
f = s1.makefile('rw')
data = f.read(10) # 一次读取10个字节
print(data)
msg = 'server rec {}'.format(data)
f.write(msg)
f.flush()
print(f, s1, sep='\n')
f.close()
s1.close()
s.close()
makefile练习
使用makefile改写群聊类
import datetime
import threading
import socket
import logging
FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.address = ip, port
self.event = threading.Event()
self.clients = {}
self.lock = threading.Lock()
def start(self):
self.sock.bind(self.address)
self.sock.listen()
threading.Thread(target=self.accept, name='accept').start()
def accept(self):
while not self.event.is_set():
new_sock, client_info = self.sock.accept()
new_file = new_sock.makefile('rw')
with self.lock:
self.clients[client_info] = new_file, new_sock # 增加了item,所以必须加锁,多线程处理同一个资源
threading.Thread(target=self.rec, name='rec', args=(new_file, client_info)).start()
def rec(self, f, client):
while not self.event.is_set():
line = f.readline() # 阻塞等一行来,输入数据的时候要加换行符
print(line)
if line.strip() == 'quit' or line.strip() == '': # line为字符串。不能再写成b''和b'quit'了
with self.lock:
_, s = self.clients.pop(client)
f.close()
s.close()
break
msg = '{:%Y/%m/%d %H:%M:%S} [{}: {}] {}'.format(datetime.datetime.now(), *client, line)
# 此处的line不用解码了,因为readline读取的是字符串
with self.lock:
for ff, _ in self.clients.values(): # 注意ff不能与上面的f重复
ff.write(msg)
ff.flush()
# def rec(self, f, client):
# while not self.event.is_set():
# try:
# line = f.readline()
# except Exception as e:
# logging.error(e)
# line = 'quit'
# msg = line.strip()
# if msg == 'quit' or msg == '':
# with self.lock:
# _, sock = self.clients.pop(client)
# f.close()
# sock.close()
# logging.info('{} quits.'.format(client))
# break
# msg = '{:%Y/%m/%d %H:%M:%S} [{}: {}] {}'.format(datetime.datetime.now(), *client, line)
# logging.info(msg)
# with self.lock:
# for ff, _ in self.clients.values():
# ff.write()
# ff.flush()
def stop(self):
self.event.set()
# keys = []
with self.lock:
for f, s in self.clients.values():
f.close()
s.close()
self.sock.close()
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())
logging.info(cs.clients)
if __name__ == '__main__':
main()
2.2 客户端编程
2.2.1 客户端编程步骤
- 创建socket对象
- 连接到远端服务端的ip和port,connect()方法
- 传输数据:使用send、recv方法发送、接收数据
- 关闭连接,释放资源
import socket
client = socket.socket()
ip_address = ('127.0.0.1', 9999)
client.connect(ip_address) # 直接连接服务器
client.send(b'abc\n')
data = client.recv(1024) # 阻塞等待
client.close()
import socket
import threading
import datetime
import logging
FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatClient:
def __init__(self, ip='127.0.0.1', port=9999):
self.client = socket.socket()
self.address = ip, port
self.event = threading.Event()
def start(self):
self.client.connect(self.address)
self.send("I'm ready.")
threading.Thread(target=self.rec, name='rec').start()
def rec(self):
while not self.event.is_set():
try:
data = self.client.recv(1024) # 此句可能会出现异常,如网络中断
except Exception as e:
logging.error(e)
break
msg = "{:%Y/%m/%d %H:%M:%S} [{}:{}] {}".format(datetime.datetime.now(), *self.address, data)
logging.info(msg)
def send(self, msg: str):
data = "{}\n".format(msg.strip()).encode()
self.client.send(data)
def stop(self):
self.client.close()
self.event.wait(3)
self.event.set()
logging.info('Client stops')
cc = ChatClient()
cc.start()
while True:
cmd = input(">>>").strip()
if cmd == 'quit':
cc.stop()
break
cc.send(cmd)