php+websocket 实现的聊天室功能详解


Posted in PHP onMay 27, 2020

本文实例讲述了php+websocket 实现的聊天室功能。分享给大家供大家参考,具体如下:

一、配置 

开启socket组建,否则会报 Fatal error: Call to undefined function socket_create() 错误

1、打开php.ini配置文件,搜索 extension=php_sockets.dll,把前面的‘;'分号删掉。修改之后重启服务。
注意:如果php版本多,一定要注意使用的哪个版本就要取修改哪个版本的php.ini文件,wamp开启socket需要apache和php下面的php.ini一起修改,而phpstudy只需要修改一个php.ini.

2、检查socket组建是否开启
运行phpinfo.php查看,如果Sockets Support => enabled,就说明开启成功了。

php+websocket 实现的聊天室功能详解

 3、设置cmd可以运行php文件

在“我的计算机->属性->高级系统设置->高级->环境变量”,在用户变量的PATH添加一条,指向php的路径(注意版本要一致),在环境变量里的Path也需要添加一条,跟上面一样

php+websocket 实现的聊天室功能详解

 4、测试socket和php是否配置成功

在项目下新建一个名叫start.php的文件

if(extension_loaded('sockets')){
   echo "1";
  }else{
   echo "0";
  }

在cmd里输入 php d:\phpstudy\www\start.php,如果输出1,则说明配置正确,如果输出0,则配置错误,需要仔细重新配置

二、实现流程

前端实现比较简单,难点在后台,其逻辑如下:php主要就是接收加密key并返回其中完成套接字的创建和握手操作

php+websocket 实现的聊天室功能详解

  服务端的流程:
1、挂起一个socket套接字进程,等待连接
2、有socket连接之后,遍历套接字数组
3、没有握手的,进行握手操作,已经握手的,则把接收的数据解析并写入缓冲区进行输出。

三、前端代码

<!DOCTYPE html>
  <html lang="en">
  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>websocket聊天室</title>
  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="external nofollow" rel="stylesheet">
  <link rel="stylesheet" href="/static/index/css/reset.min.css" rel="external nofollow" >
  <link rel="stylesheet" href="/static/index/css/chat.css" rel="external nofollow" >
  <link rel="stylesheet" href="/static/index/layui/layer/theme/default/layer.css" rel="external nofollow" >
  <style>
  .message img {
  float: left;
  width: 40px;
  height: 40px;
  margin-right: 12px;
  border-radius: 50%;
  }
  
  .you {
  margin-left: 60px;
  margin-top: -39px;
  }
  
  .me-header {
  float: right !important;
  margin-right: 0 !important;
  }
  
  .me {
  margin-right: 60px;
  margin-top: -39px;
  }
  
  .active-chat::-webkit-scrollbar, .left::-webkit-scrollbar {
  width: 2px;
  }
  </style>
  </head>
  <body>
  
  <div class="wrapper">
    <div class="container">
      <div class="left">
        <div class="top" style="padding: 20px 29px;height: auto;">
          <div class="" style="font: 400 13.3333px Arial;font-weight: 400;">在线人数:<span id="numbers">0</span> 人
          </div>
        </div>
        <ul class="people">
        </ul>
      </div>
      <div class="right">
        <div class="top"><span>Tips: <span class="name">PHP之websocket聊天室</span></span></div>
        <div class="chat active-chat" data-chat="person1"
         style="height: auto;border-width: 0px;padding: 10px;height: 483px; padding: 10px;overflow-y: auto;scrollTop: 100px">
        </div>
        <div class="write">
          <a href="javascript:;" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="write-link attach"></a>
          <input type="text" id="input-value" onkeydown="confirm(event)"/>
          <a href="javascript:;" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="write-link smiley"></a>
          <a href="javascript:;" rel="external nofollow" rel="external nofollow" rel="external nofollow" class="write-link send" onclick="send()"></a>
        </div>
      </div>
    </div>
  </div>
  
  <script src="/static/index/js/jquery-1.11.3.min.js"></script>
  <script src="/static/index/js/chat.js"></script>
  <script src="/static/index/layui/layer/layer.js"></script>
  <script>
  var uname = "user" + uuid(8, 11);
  layer.open({
    title: '您的用户名如下',
    content: uname,
    closeBtn: 0,
    yes: function (index, layero) {
      layer.close(index);
    }
  });
  // 随机选出一个头像
  var avatar = ['a1.jpg', 'a2.jpg', 'a3.jpg', 'a4.jpg', 'a5.jpg', 'a6.jpg', 'a7.jpg', 'a8.jpg', 'a9.jpg', 'a10.jpg'];
  if (avatar[Math.round(Math.random() * 10)]) {
    var headerimg = "img/" + avatar[Math.round(Math.random() * 10)];
  } else {
    var headerimg = "img/" + avatar[0];
  }
  
  var ws = null;
  // 创建websocket连接
  connect();
  function connect() {
    // 创建一个 websocket 连接 ws://ip:端口号
    ws = new WebSocket("ws://127.0.0.1:1234");
    
    // 连接状态 1已建立连接
    console.log(ws.readyState)
    
    // 连接建立时触发
    ws.onopen = onopen;
    
    // 客户端接收服务端数据时触发
    ws.onmessage = onmessage;
    
    // 连接关闭时触发
    ws.onclose = onclose;
    
    // 通信发生错误时触发
    ws.onerror = onerror;
  }
  
  // 通信建立成功 
  function onopen()
  {
    var data = "系统消息:建立连接成功";
    console.log(data);
  }
  
  // 接收客户端的数据,发送数据
  function onmessage(e)
  {
    var data = JSON.parse(e.data);
    console.log(data)
    
    switch (data.type) {
      case 'handShake':
        //首次登录,发送登陆数据
        var user_info = {'type': 'login', 'msg': uname, 'headerimg': headerimg};
        sendMsg(user_info);
        break;
      case 'login':
        userList(data.user_list);
        systemMessage('系统消息: ' + data.msg + ' 已上线');
        break;
      case 'logout':
        userList(data.user_list);
        if (data.msg.length > 0) {
        systemMessage('系统消息: ' + data.msg + ' 已下线');
        }
        break;
      case 'user':
        messageList(data);
        break;
      case 'system':
        systemMessage();
        break;
    }
  }
  function onclose()
  {
    console.log("连接关闭,定时重连");
    connect();
  }
  
  // websocket 错误事件
  function onerror()
  {
    var data = "系统消息 : 出错了,请退出重试.";
    console.log(data);
  }
  
  function confirm(event) {
    var key_num = event.keyCode;
    if (13 == key_num) {
      send();
    } else {
      return false;
    }
  }
  
  // 发送数据
  function send() {
    var msg = document.querySelector("input#input-value").value;
    var reg = new RegExp("\r\n", "g");
    msg = msg.replace(reg, "");
    sendMsg({type: "user", msg: msg});
    document.querySelector("input#input-value").value = "";
  }
  
  // 发送数据
  function sendMsg(msg) {
    var data = JSON.stringify(msg);
    ws.send(data);
  }
  
  
  // 追加数据 上下线的系统消息
  function systemMessage(msg) {
    var html = `<div class="conversation-start">
        <span>` + msg + `</span>
        </div>`;
    var active_chat = document.querySelector('div.active-chat');
    var oldHtml = active_chat.innerHTML;
    active_chat.innerHTML = oldHtml + html;
    active_chat.scrollTop = active_chat.scrollHeight;
  }
  
  // 追加从服务端返回的数据 左侧在线人数列表
  function userList(user) {
    var html = '';
    for (var i = 0; i < user.length; i++) {
      html += `<li class="person" data-chat="person1">
        <img src="` + user[i].headerimg + `" alt=""/>
        <span class="name">` + user[i].username + `</span>
        <span class="time">` + user[i].login_time + `</span>
        <span class="preview" style="color: green;font-size: 7px;">在线</span>
        </li>`;
    }
    document.querySelector('ul.people').innerHTML = html;
    document.querySelector('span#numbers').innerHTML = user.length;
  }
  
  // 右侧聊天记录列表
  function messageList(data) {
  
    // 判读是不是自己发送的消息,对应的样式不同
    if (data.from == uname) {  
      // 如果当前用户名和feom的用户名相同,就说明时自己发送的消息
      var html = `<div class="message">
          <img class="me-header" src="` + data.headerimg + `" alt=""/>
          <div class="bubble me">` + data.msg + `</div>
          </div>`;
    } else {
      // 别人发送的信息列表
      var html = `<div class="message">
          <img src="` + data.headerimg + `" alt=""/>
          <div class="bubble you">` + data.msg + `</div>
          </div>`;
    }
    var active_chat = document.querySelector('div.active-chat');
    var oldHtml = active_chat.innerHTML;
    active_chat.innerHTML = oldHtml + html;
    active_chat.scrollTop = active_chat.scrollHeight;
  }
  
  /**
   * 生产一个全局唯一ID作为用户名的默认值;
   *
   * @param len
   * @param radix
   * @returns {string}
   */
  function uuid(len, radix) {
    var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
    var uuid = [], i;
    radix = radix || chars.length;
    
    if (len) {
      for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
    } else {
      var r;
      
      uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
      uuid[14] = '4';
      
      for (i = 0; i < 36; i++) {
        if (!uuid[i]) {
          r = 0 | Math.random() * 16;
          uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
        }
      }
    }
    return uuid.join('');
  }
  </script>
  </body>
  </html>

 四、后端代码

<?php
/**
 * Created by PhpStorm.
 * User: 25754
 * Date: 2019/4/23
 * Time: 14:13
 */

class socketServer
{

  const LISTEN_SOCKET_NUM = 9;
  const LOG_PATH = "./log/"; //日志
  private $_ip = "127.0.0.1"; //ip
  private $_port = 1234; //端口 要和前端创建WebSocket连接时的端口号一致
  private $_socketPool = array(); //socket池,即存放套接字的数组
  private $_master = null;  //创建的套接字对象

  public function __construct()
  {
    $this->initSocket();
  }

  // 创建WebSocket连接
  private function initSocket()
  {
    try {
      //创建socket套接字
      $this->_master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
      // 设置IP和端口重用,在重启服务器后能重新使用此端口;
      socket_set_option($this->_master, SOL_SOCKET, SO_REUSEADDR, 1);
      //绑定地址与端口
      socket_bind($this->_master, $this->_ip, $this->_port);
      //listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,其中的能存储的请求不明的socket数目。
      socket_listen($this->_master, self::LISTEN_SOCKET_NUM);
    } catch (Exception $e) {
      $this->debug(array("code: " . $e->getCode() . ", message: " . $e->getMessage()));
    }
    //将socket保存到socket池中 (将套接字放入数组)默认把当前用户放在第一个
    $this->_socketPool[0] = array('resource' => $this->_master);
    $pid = getmypid();
    $this->debug(array("server: {$this->_master} started,pid: {$pid}"));
  }

  // 挂起进程遍历套接字数组,对数据进行接收、处理、发送
  public function run()
  {
    // 死循环 直到socket断开
    while (true) {
      try {
        
        $write = $except = NULL;
        // 从数组中取出resource列
        $sockets = array_column($this->_socketPool, 'resource');

        /* 
        $sockets 是一个存放文件描述符的数组。
        $write 是监听是否客户端写数据,传入NULL是不关心是否有写变化
        $except 是$sockets里面要派粗话的元素,传入null是监听全部
        最后一个参数是超时时间,0立即结束 n>1则最多n秒后结束,如遇某一个连接有新动态,则提前返回 null如遇某一个连接有新动态,则返回
        */
        // 接收套接字数字,监听他们的状态就是有新消息到或有客户端连接/断开时,socket_select函数才会返回,继续往下执行
        $read_num = socket_select($sockets, $write, $except, NULL);
        if (false === $read_num) {
          $this->debug(array('socket_select_error', $err_code = socket_last_error(), socket_strerror($err_code)));
          return;
        }

        // 遍历套接字数组
        foreach ($sockets as $socket) {

          // 如果有新的连接进来
          if ($socket == $this->_master) {

            // 接收一个socket连接
            $client = socket_accept($this->_master);
            if ($client === false) {
              $this->debug(['socket_accept_error', $err_code = socket_last_error(), socket_strerror($err_code)]);
              continue;
            }
            //连接 并放到socket池中
            $this->connection($client);
          } else {

            //接收已连接的socket数据,返回的是从socket中接收的字节数。
            // 第一个参数:socket资源,第二个参数:存储接收的数据的变量,第三个参数:接收数据的长度
            $bytes = @socket_recv($socket, $buffer, 2048, 0);

            // 如果接收的字节数为0
            if ($bytes == 0) {

              // 断开连接
              $recv_msg = $this->disconnection($socket);
            } else {

              // 判断有没有握手,没有握手进行握手,已经握手则进行处理
              if ($this->_socketPool[(int)$socket]['handShake'] == false) {
                // 握手
                $this->handShake($socket, $buffer);
                continue;
              } else {
                // 解析客户端传来的数据
                $recv_msg = $this->parse($buffer);
              }
            }

            // echo "<pre>";
            // 业务处理,组装返回客户端的数据格式
            $msg = $this->doEvents($socket, $recv_msg);
            // print_r($msg);

            socket_getpeername ( $socket , $address ,$port );
            $this->debug(array(
              'send_success',
              json_encode($recv_msg),
              $address,
              $port
            ));
            // 把服务端返回的数据写入套接字
            $this->broadcast($msg);
          }
        }
      } catch (Exception $e) {
        $this->debug(array("code: " . $e->getCode() . ", message: " . $e->getMessage()));
      }

    }

  }

  /**
   * 数据广播
   * @param $data
   */
  private function broadcast($data)
  {
    foreach ($this->_socketPool as $socket) {
      if ($socket['resource'] == $this->_master) {
        continue;
      }
      // 写入套接字
      socket_write($socket['resource'], $data, strlen($data));
    }
  }

  /**
   * 业务处理,在这可以对数据库进行操作,并返回客户端数据;根据不同类型,组装不同格式的数据
   * @param $socket
   * @param $recv_msg 客户端传来的数据
   * @return string
   */
  private function doEvents($socket, $recv_msg)
  {
    $msg_type = $recv_msg['type'];
    $msg_content = $recv_msg['msg'];
    $response = [];
    //echo "<pre>";
    switch ($msg_type) {
      case 'login':
      // 登陆上线信息
        $this->_socketPool[(int)$socket]['userInfo'] = array("username" => $msg_content, 'headerimg' => $recv_msg['headerimg'], "login_time" => date("h:i"));
        // 取得最新的名字记录
        $user_list = array_column($this->_socketPool, 'userInfo');
        $response['type'] = 'login';
        $response['msg'] = $msg_content;
        $response['user_list'] = $user_list;
        //print_r($response);

        break;
      case 'logout':
      // 退出信息
        $user_list = array_column($this->_socketPool, 'userInfo');
        $response['type'] = 'logout';
        $response['user_list'] = $user_list;
        $response['msg'] = $msg_content;
        //print_r($response);
        break;
      case 'user':
      // 发送的消息
        $userInfo = $this->_socketPool[(int)$socket]['userInfo'];
        $response['type'] = 'user';
        $response['from'] = $userInfo['username'];
        $response['msg'] = $msg_content;
        $response['headerimg'] = $userInfo['headerimg'];
        //print_r($response);
        break;
    }

    return $this->frame(json_encode($response));
  }

  /**
   * socket握手
   * @param $socket
   * @param $buffer 客户端接收的数据
   * @return bool
   */
  public function handShake($socket, $buffer)
  {
    $acceptKey = $this->encry($buffer);
    $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
      "Upgrade: websocket\r\n" .
      "Connection: Upgrade\r\n" .
      "Sec-WebSocket-Accept: " . $acceptKey . "\r\n\r\n";

    // 将socket写入缓冲区
    socket_write($socket, $upgrade, strlen($upgrade));
    // 标记握手已经成功,下次接受数据采用数据帧格式
    $this->_socketPool[(int)$socket]['handShake'] = true;
    socket_getpeername ( $socket , $address ,$port );
    $this->debug(array(
      'hand_shake_success',
      $socket,
      $address,
      $port
    ));
    //发送消息通知客户端握手成功
    $msg = array('type' => 'handShake', 'msg' => '握手成功');
    $msg = $this->frame(json_encode($msg));
    socket_write($socket, $msg, strlen($msg));
    return true;
  }

  /**
   * 帧数据封装
   * @param $msg
   * @return string
   */
  private function frame($msg)
  {
    $frame = [];
    $frame[0] = '81';
    $len = strlen($msg);
    if ($len < 126) {
      $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
    } else if ($len < 65025) {
      $s = dechex($len);
      $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
    } else {
      $s = dechex($len);
      $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
    }
    $data = '';
    $l = strlen($msg);
    for ($i = 0; $i < $l; $i++) {
      $data .= dechex(ord($msg{$i}));
    }
    $frame[2] = $data;
    $data = implode('', $frame);
    return pack("H*", $data);
  }

  /**
   * 解析客户端的数据
   * @param $buffer
   * @return mixed
   */
  private function parse($buffer)
  {
    $decoded = '';
    $len = ord($buffer[1]) & 127;
    if ($len === 126) {
      $masks = substr($buffer, 4, 4);
      $data = substr($buffer, 8);
    } else if ($len === 127) {
      $masks = substr($buffer, 10, 4);
      $data = substr($buffer, 14);
    } else {
      $masks = substr($buffer, 2, 4);
      $data = substr($buffer, 6);
    }
    for ($index = 0; $index < strlen($data); $index++) {
      $decoded .= $data[$index] ^ $masks[$index % 4];
    }
    return json_decode($decoded, true);
  }

  //提取 Sec-WebSocket-Key 信息并加密
  private function encry($req)
  {
    $key = null;
    if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) {
      $key = $match[1];
    }
    // 加密
    return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
  }

  /**
   * 连接socket
   * @param $client
   */
  public function connection($client)
  {
    socket_getpeername ( $client , $address ,$port );
    $info = array(
      'resource' => $client,
      'userInfo' => '',
      'handShake' => false,
      'ip' => $address,
      'port' => $port,
    );
    $this->_socketPool[(int)$client] = $info;
    $this->debug(array_merge(['socket_connect'], $info));
  }

  /**
   * 断开连接
   * @param $socket
   * @return array
   */
  public function disconnection($socket)
  {
    $recv_msg = array(
      'type' => 'logout',
      'msg' => @$this->_socketPool[(int)$socket]['userInfo']['username'],
    );
    unset($this->_socketPool[(int)$socket]);
    return $recv_msg;
  }

  /**
   * 日志
   * @param array $info
   */
  private function debug(array $info)
  {
    $time = date('Y-m-d H:i:s');
    array_unshift($info, $time);
    $info = array_map('json_encode', $info);
    file_put_contents(self::LOG_PATH . 'websocket_debug.log', implode(' | ', $info) . "\r\n", FILE_APPEND);
  }
}

// 类外实例化
$sk = new socketServer();
// 运行
$sk -> run();

五、运行php

建立start.bat文件,运行php,也可以在cmd里输入命令运行php

php ./socketServer.php
pause

运行结果如下:

php+websocket 实现的聊天室功能详解

php+websocket 实现的聊天室功能详解

注意:start.bat要一直运行,如果关了,就表示socket也关了,就不能通信了,所有需要start.bat一直运行

 项目地址:https://github.com/zhxiangfei/php-websocket-

希望本文所述对大家PHP程序设计有所帮助。

PHP 相关文章推荐
PHP 中dirname(_file_)讲解
Mar 18 PHP
php array_intersect比array_diff快(附详细的使用说明)
Jul 03 PHP
php-cli简介(不会Shell语言一样用Shell)
Jun 03 PHP
对于PHP 5.4 你必须要知道的
Aug 07 PHP
thinkphp普通查询与表达式查询实例分析
Nov 24 PHP
php使用CURL伪造IP和来源实例详解
Jan 15 PHP
PHP中把错误日志保存在系统日志中(Windows系统)
Jun 23 PHP
php实现数组纵向转横向并过滤重复值的方法分析
May 29 PHP
php数据库的增删改查 php与javascript之间的交互
Aug 31 PHP
php 提交表单 关闭layer弹窗iframe的实例讲解
Aug 20 PHP
php实现网页上一页下一页翻页过程详解
Jun 28 PHP
如何在Laravel5.8中正确地应用Repository设计模式
Nov 26 PHP
php+js实现的拖动滑块验证码验证表单操作示例【附源码下载】
May 27 #PHP
PHP code 验证码生成类定义和简单使用示例
May 27 #PHP
PHP 计算至少是其他数字两倍的最大数的实现代码
May 26 #PHP
tp5.1 框架数据库-数据集操作实例分析
May 26 #PHP
tp5.1 框架路由操作-URL生成实例分析
May 26 #PHP
tp5.1 框架join方法用法实例分析
May 26 #PHP
tp5.1框架数据库子查询操作实例分析
May 26 #PHP
You might like
类的另类用法--数据的封装
2006/10/09 PHP
提高PHP编程效率 引入缓存机制提升性能
2010/02/15 PHP
Yii 2.0如何使用页面缓存方法示例
2017/05/23 PHP
jquery 指南/入门基础
2007/11/30 Javascript
jQuery ajax BUG:object doesn't support this property or method
2010/07/06 Javascript
jQuery在vs2008及js文件中的无智能提示的解决方法
2010/12/30 Javascript
调试Javascript代码(浏览器F12及VS中debugger关键字)
2013/01/25 Javascript
通过js获取div的background-image属性
2013/10/15 Javascript
jquery 绑定回车动作扑捉回车键触发的事件
2014/03/26 Javascript
JavaScript实现在标题栏上显示当前日期的方法
2015/03/19 Javascript
基于JS实现省市联动效果代码分享
2016/06/06 Javascript
浅谈javascript中遇到的字符串对象处理
2016/11/18 Javascript
Vue方法与事件处理器详解
2016/12/01 Javascript
微信小程序 scroll-view实现上拉加载与下拉刷新的实例
2017/01/21 Javascript
Canvas 绘制粒子动画背景
2017/02/15 Javascript
Bootstrap table简单使用总结
2017/02/15 Javascript
nodejs mysql 实现分页的方法
2017/06/06 NodeJs
深入理解基于vue-cli的vuex配置
2017/07/24 Javascript
Angular CLI 安装和使用教程
2017/09/13 Javascript
JS在if中的强制类型转换方式
2018/07/15 Javascript
Angularjs Ng_repeat中实现复选框选中并显示不同的样式方法
2018/09/12 Javascript
Vue中使用JsonView来展示Json树的实例代码
2020/11/16 Javascript
python中的sort方法使用详解
2014/07/25 Python
从django的中间件直接返回请求的方法
2018/05/30 Python
树莓派使用USB摄像头和motion实现监控
2019/06/22 Python
使用matlab 判断两个矩阵是否相等的实例
2020/05/11 Python
加拿大票务网站:Ticketmaster加拿大
2017/07/17 全球购物
Foot Locker加拿大官网:美国知名运动产品零售商
2019/07/21 全球购物
将"引用"作为函数返回值类型的格式、好处和需要遵守的规则
2016/02/09 面试题
感恩节活动方案
2014/01/27 职场文书
行政部经理助理岗位职责
2014/06/15 职场文书
五四演讲稿范文
2014/09/03 职场文书
小学教师师德师风自我评价
2015/03/04 职场文书
服装区域经理岗位职责
2015/04/10 职场文书
初中英语教学随笔
2015/08/15 职场文书
Django数据库(SQlite)基本入门使用教程
2022/07/07 Python