TCP编程
Socket介绍
Socket套接字
Python中提供socket.py标准库,非常底层的接口库
Socket是一种通用的网络编程接口,和网络层次没有--对应的关系
协议族
AF表示Address Family,用于socket()第一个参数
名称 | 含义 |
---|---|
AF_INET | IPV4 |
AF_INET6 | IPV6 |
AF_UNIX | Unix Domain Socket,windows没有 |
socket类型
名称 | 含义 |
---|---|
SOCK_STREAM | 面向连接的流套接字。默认值,TCP协议 |
SOCK_DGRAM | 无连接的数据报文套接字。TCP协议 |
TCP编程
socket编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server,客户端为Client
TCP服务端编程
服务器端编程步骤
- 创建Socket对象
- 绑定IP地址Address和端口Port。bind()方法
- 开始监听,将在指定的IP的端口上监听。listen()方法
- 获取用于传送数据的Socket对象
socket.accept()->(socket object, address info)
accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组
地址是远程客户端的地址,IPv4中他是一个二元组(clientaddr, port)
- 接收数据
recv(bufsize[, flag])使用缓冲区接收数据
- 发送数据
send(bytes)发送数据
问题
两次绑定同意了一个监听端口会怎么样?
import socket
s = socket.socket() # 创建socket对象
s.bind(('127.0.0.1', 9999)) # 一个二元组
s.listen() # 开始监听
# 开启一个连接
s1, info = s.accept() # 阻塞直到和客户端成功建立连接,返回一个socket对象和客户端地址
# 使用缓冲区获取数据
data = s1.recv(1024)
print(data, info)
s1.send(b'baidu.com ack')
# 开启另外一个连接
s2, _ = s.accept()
data = s2.recv(1024)
s2.send(b'hello python')
s.close()
上例accept和recv是阻塞的,主线程经常被阻塞住而不能工作,怎么办
写一个群聊程序
需求分析
聊天工具是CS程序,C是每一个客户端,S是服务器端
服务器应该具有的功能
启动服务,包括绑定地址和端口,监听
建立连接,能和多个客户端建立连接
接收不同用户的信息
分发,将接收的某个用户的信息转发到已连接的所有客户端
停止服务
记录连接的客户端
代码实现
服务端
from concurrent.futures import thread
import logging
import socket
import threading
import datetime
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(thread)d %(message)s")
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999) -> None:
self.sock = socket.socket()
self.addr = (ip, port)
self.client = {} # 客户端
self.event = threading.Event()
def start(self): # 启动监听
self.sock.bind(self.addr) # 绑定
self.sock.listen() # 监听
# accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept).start()
def accept(self):
while not self.event.is_set():
sock, client = self.sock.accept()
self.clients[client] = sock
# 准备接收数据,recv是阻塞的,开启新的线程
threading.Thread(target=self.recv, args=(sock, client)).start()
def recv(self, sock: socket.socket, client): # 接收客户端数据
while not self.event.is_set():
data = sock.recv(1024)
msg = data.decode().strip()
# 客户端退出命令
if msg == 'quit':
self.clients.pop(client)
sock.close()
logging.info('{} quits'.format(client))
break
msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now())
logging.info(msg)
msg = msg.encode()
for s in self.clients.values():
s.send(msg)
def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self.sock.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()) # 用来观察断开后线程的变化
socket常用方法
名称 | 含义 |
---|---|
socket.recv(bufsize[, flag]) | 获取数据。默认是阻塞的方式 |
socket.recvfrom(bufsize[, flag]) | 获取数据,返回一个二元组(bytes, address) |
socket.recv_into(bufsize[, nbytes[, flag]]) | 获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或0,将buffer大小的数据存入buffer中。返回接收的字节数 |
socket.recvfrom_into(buffer[, nbytes[, flag]]) | 获取数据。返回一个二元组(bytes, address)到buffer中 |
socket.send(bytes[, flag]) | TCP发送数据 |
socket.sendall(bytes[, flag]) | TCP发送全部数据,成功返回None |
socket.sendto(bytes[, flag]) | 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
socket.makefile(mode='r', buffering=None, *, encoding=None, errors=None, newline=None)
创建一个该套接字相关连的文件对象,将recv方法看做读方法,将send方法看做写方法
from concurrent.futures import thread
import logging
import socket
import threading
import datetime
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(thread)d %(message)s")
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999) -> None:
self.sock = socket.socket()
self.addr = (ip, port)
self.client = {} # 客户端
self.event = threading.Event()
def start(self): # 启动监听
self.sock.bind(self.addr) # 绑定
self.sock.listen() # 监听
# accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept).start()
def accept(self):
while not self.event.is_set():
sock, client = self.sock.accept()
f = sock.makefile('rw') # 支持读写
self.clients[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()
msg = data.decode().strip()
# 客户端退出命令
if msg == 'quit':
self.clients.pop(client)
f.close()
logging.info('{} quits'.format(client))
break
msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now())
logging.info(msg)
for s in self.clients.values():
s.write(msg)
s.flush()
def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self.sock.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()) # 用来观察断开后线程的变化
TCP客户端编程
客户端编程步骤
- 创建Socket对象
- 连接到远端服务端的ip和port,connect()方法
- 传输数据
- 使用send、recv方法发送、接收数据
- 关闭连接,释放资源
import socket
client = socket.socket()
ipaddr = ('127.0.0.1', 9999)
client.connect(ipaddr) # 直接连接服务器
client.send(b'abcd\n')
data = client.recv(1024) # 阻塞等待
print(data)
client.close()
开始编写客户端类
import socket
import threading
import datetime
import logging
FORMAT = "%(asctime)s %(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.sock = socket.socket()
self.addr = (ip, port)
self.event = threading.Event()
def start(self): # 启动对远端服务器的连接
self.socket.connect(self.addr)
self.send("I'm ready.")
# 准备接收数据,recv是阻塞的,开启新的线程
threading.Thread(target=self.recv, name="recv").start()
def recv(self): # 接收客户端的数据
while not self.event.is_set():
try:
data = self.sock.recv(1024) # 阻塞
except Exception as e:
logging.error(e)
break
msg = "{"/%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *self.addr, data.strip())
logging.info(msg)
def send(self, msg: str):
data = "{}\n".format(msg.strip()).encode() # 服务端需要一个换行符
self.sock.send(data)
def stop(self):
self.sock.close()
self.event.wait(3)
self.event.set()
logging.info('Client stops.')
def main():
cc = ChatClient()
cc.start()
while True:
cmd = input('>>>')
if cmd.strip() == 'quit':
cc.stop()
break
cc.send(cmd) # 发送消息
if __name__ == '__main__':
main()