Socket介绍

Socket套接字
Python中提供socket.py标准库,非常底层的接口库
Socket是一种通用的网络编程接口,和网络层次没有--对应的关系

协议族
AF表示Address Family,用于socket()第一个参数

名称含义
AF_INETIPV4
AF_INET6IPV6
AF_UNIXUnix 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()