V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tanszhe
V2EX  ›  PHP

Tcp, WebSocket 和 http 之间的通讯

  •  
  •   tanszhe · 2019-05-07 10:09:09 +08:00 · 4474 次点击
    这是一个创建于 2033 天前的主题,其中的信息可能已经有所发展或是发生改变。

    这个列子主要讨论TcpWebSockethttp之间的通讯。长连接和长连接通讯,长连接和短连接通讯。其他协议同理可得。
    本列子是基于 one 框架 ( https://github.com/lizhichao/one ) 开发.

    配置协议 监听端口

    由于 swoole 的模型 WebSocket server 包含 http server , http server 包含 tcp server。

    所以我们配置主服务为 WebSocket server ,添加两个 http 和 tcp 监听。配置文件如下:

    return [
        'server' => [
            'server_type' => \One\Swoole\OneServer::SWOOLE_WEBSOCKET_SERVER,
            'port' => 8082,
            'action' => \App\Test\MixPro\Ws::class,
            'mode' => SWOOLE_PROCESS,
            'sock_type' => SWOOLE_SOCK_TCP,
            'ip' => '0.0.0.0',
            'set' => [
                'worker_num' => 5
            ]
        ],
        'add_listener' => [
            // http 监听
            [
                'port' => 8081,
                'action' => \App\Server\AppHttpPort::class, 
                'type' => SWOOLE_SOCK_TCP,
                'ip' => '0.0.0.0',
                'set' => [
                    'open_http_protocol' => true,
                    'open_websocket_protocol' => false
                ]
            ],
            // tcp 监听
            [
                'port' => 8083,
                'pack_protocol' => \One\Protocol\Text::class, // tcp 打包,解包协议,方便在终端调试 我们使用 text 协议. 换行符 表示一个包的结束
                'action' => \App\Test\MixPro\TcpPort::class,
                'type' => SWOOLE_SOCK_TCP,
                'ip' => '0.0.0.0',
                'set' => [
                    'open_http_protocol' => false,
                    'open_websocket_protocol' => false
                ]
            ]
        ]
    ];
    
    

    接下来去 \App\Test\MixPro\Ws\App\Test\MixPro\TcpPort 实现各种事件处理。 \App\Server\AppHttpPort 是框架内置的,通过路由处理 http 请求的,配置路由即可。

    配置路由

    
    // 首页
    Router::get('/mix', [
        'use'    => HttpController::class . '@index',
        'middle' => [\App\Test\MixPro\TestMiddle::class . '@isLogin'] // 中间件 如果用户登录了 直接跳转到相应的页面
    ]);
    
    Router::group([
            'middle' => [\App\Test\MixPro\TestMiddle::class . '@checkSession'] // 中间件 让用户登录后 才能进入聊天页面 http websocket 都能获取到这个 session
        ], function () {
    
        // websocket 页面
        Router::get('/mix/ws', HttpController::class . '@ws');
        
        // http 页面
        Router::get('/mix/http', HttpController::class . '@http');
        
        // http 轮训消息接口
        Router::post('/mix/http/loop', HttpController::class . '@httpLoop');
        
        // http 发送消息接口
        Router::post('/mix/http/send', HttpController::class . '@httpSend');
    
    });
    
    

    配置的都是 http 协议路由。websocket 和 tpc 我们直接在回调action处理。如果你的项目复杂也可以配置相应的路由。one 框架的路由支持任何协议,使用方法也是统一的。

    处理 tcp 协议

    其中__construct,onConnect,onClose 不是必须的。
    如果你想在服务器运行开始时最一些事情就写到 __construct里面。
    onConnect 当有客户端连接时触发,每个客户端触发一次
    onClose 当有客户端连接断开时触发,每个客户端触发一次

    class TcpPort extends Tcp
    {
        use Funs;
    
        private $users = [];
    
        /**
         * @var Ws
         */
        protected $server;
    
        /**
         * @var Client
         */
        protected $global_data;
    
        public function __construct($server, $conf)
        {
            parent::__construct($server, $conf);
            $this->global_data = $this->server->global_data;
        }
    
        // 终端连接上服务器时
        public function onConnect(\swoole_server $server, $fd, $reactor_id)
        {
            $name             = uuid();
            $this->users[$fd] = $name;
            $this->sendTo('all', json_encode(['v' => 1, 'n' => $name]));
            $this->sendToTcp($fd, json_encode(['v' => 4, 'n' => $this->getAllName()]));
            $this->global_data->bindId($fd, $name);
            $this->send($fd, "你的名字是:" . $name);
        }
    
        // 消息处理 像某个 name 发送消息
        public function onReceive(\swoole_server $server, $fd, $reactor_id, $data)
        {
            $arr = explode(' ', $data);
            if (count($arr) !== 3 || $arr[0] !== 'send') {
                $this->send($fd, "格式不正确");
                return false;
            }
            $n = $arr[1];
            $d = $arr[2];
            $this->sendTo($n, json_encode(['v' => 3, 'n' => $d]));
        }
    
        // 下线 通知所有其他终端,解除与 fd 的关系绑定。
        public function onClose(\swoole_server $server, $fd, $reactor_id)
        {
            echo "tcp close {$fd} \n";
            $this->global_data->unBindFd($fd);
            $this->sendTo('all', json_encode(['v' => 2, 'n' => $this->users[$fd]]));
            unset($this->users[$fd]);
        }
    
    }
    
    

    定义了一个公共的 traitFuns主要实现两个方法,获取所有的终端( tcp,ws,http ),和向某个用户发送消息 。在 ws、http 都会用到这个
    在构造函数我们初始化了一个 global_data 用来保存,名称和 fd 的关系。你也可以使用方式储存。因为 fd 没次连接都不同。global_data 是 one 框架内置的。
    终端连接上服务器时触发事件 onConnect ,我们给这个终端取个名字,并把关系保存在 global_data。 通知所有终端有个新终端加入,并告诉刚加入的终端当前有哪些终端在线。

    处理 websocket 协议

    其中__construct,onHandShake,onOpenonClose 不是必须的。

    onHandShake,onOpen 是配合使用的,如果onOpen返回 false 服务器会拒绝连接。 在 onOpenonMessageonClose可以拿到当前用户的 session 信息和 http 是相通的。

    class Ws extends WsServer
    {
        use Funs;
    
        private $users = [];
    
        /**
         * @var Client
         */
        public $global_data = null;
    
        public function __construct(\swoole_server $server, array $conf)
        {
            parent::__construct($server, $conf);
            $this->global_data = new Client();
        }
        
        // 初始化 session
        public function onHandShake(\swoole_http_request $request, \swoole_http_response $response)
        {
            return parent::onHandShake($request, $response);
        }
    
        // ws 发送消息
        public function onMessage(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
        {
            $data = $frame->data;
            $arr  = json_decode($data, true);
            $n    = $arr['n'];
            $d    = $arr['d'];
            $this->sendTo($n, json_encode(['v' => 3, 'n' => $d]));
    
        }
    
        // 判断用户是否登录 如果没有登录拒绝连接
        public function onOpen(\swoole_websocket_server $server, \swoole_http_request $request)
        {
            $name = $this->session[$request->fd]->get('name');
            if ($name) {
                $this->users[$request->fd] = $name;
                $this->sendTo('all', json_encode(['v' => 1, 'n' => $name]));
                $this->global_data->bindId($request->fd, $name);
                return true;
            } else {
                return false;
            }
        }
    
        // ws 断开清除信息
        public function onClose(\swoole_server $server, $fd, $reactor_id)
        {
            echo "ws close {$fd} \n";
            $this->global_data->unBindFd($fd);
            $this->sendTo('all', json_encode(['v' => 2, 'n' => $this->users[$fd]]));
            unset($this->users[$fd]);
        }
    }
    
    

    处理 http 协议

    主要是 httpLoop 方法,轮训获取消息。因为 http 是短连接,发给 http 的信息我们是先存放在$global_data,然后直接这里读取。防止连接间隙丢信息。

    
    class HttpController extends Controller
    {
    
        use Funs;
    
        /**
         * @var Ws
         */
        protected $server;
    
        /**
         * @var Client
         */
        protected $global_data;
    
    
        public function __construct($request, $response, $server = null)
        {
            parent::__construct($request, $response, $server);
            $this->global_data = $this->server->global_data;
        }
    
        /**
         * 首页
         */
        public function index()
        {
            $code = sha1(uuid());
            $this->session()->set('code', $code);
            return $this->display('index', ['code' => $code]);
        }
    
        /**
         * ws 页面
         */
        public function ws()
        {
            $name = $this->session()->get('name');
            if (!$name) {
                $name = uuid();
                $this->session()->set('name', $name);
            }
            return $this->display('ws',['users' => $this->getAllName(),'name' => $name]);
        }
    
        /**
         * http 页面
         */
        public function http()
        {
            $name = $this->session()->get('name');
            if (!$name) {
                $name = uuid();
                $this->session()->set('name', $name);
            }
            $this->global_data->set("http.{$name}", 1, time() + 60);
            $this->sendTo('all', json_encode(['v' => 1, 'n' => $name]));
            return $this->display('http', ['list' => $this->getAllName(), 'name' => $name]);
        }
    
        /**
         * http 轮训
         */
        public function httpLoop()
        {
            $name = $this->session()->get('name');
            $this->global_data->set("http.{$name}", 1, time() + 60);
            $i = 0;
            do {
                $data = $this->global_data->getAndDel("data.{$name}");
                $i++;
                \co::sleep(0.1);
            } while ($data === null && $i < 300);
            if ($data) {
                foreach ($data as &$v) {
                    $v = json_decode($v, true);
                }
            } else {
                $data = [];
            }
            return $this->json($data);
        }
    
        /**
         * http 发送消息
         */
        public function httpSend()
        {
            $n = $this->request->post('n');
            $d = $this->request->post('d');
            if ($n && $d) {
                $this->sendTo($n, json_encode(['v' => 3, 'n' => $d]));
                return '1';
            }
            return '0';
        }
    
        public function __destruct()
        {
    
        }
    
        public function __call($name, $arguments)
        {
            return $this->server->$name(...$arguments);
        }
    
    }
    
    

    到此基本就完成了。你可以去看完整的代码 : 点这里

    其他的一些列子 : https://github.com/lizhichao/one-demo

    8 条回复    2019-05-07 17:30:39 +08:00
    TeslaLyon
        1
    TeslaLyon  
       2019-05-07 10:21:28 +08:00
    支持!
    funlee
        2
    funlee  
       2019-05-07 12:48:39 +08:00
    v 站还可以用来写博客?
    Hzsgg0624
        3
    Hzsgg0624  
       2019-05-07 16:32:31 +08:00
    这是什么语言写的?
    knva
        4
    knva  
       2019-05-07 16:35:06 +08:00
    Hzsgg0624
        5
    Hzsgg0624  
       2019-05-07 16:41:42 +08:00
    @knva 这是和硬件互联交互的吗
    qieqie
        6
    qieqie  
       2019-05-07 16:53:51 +08:00
    宣传自己的项目前,建议先回去补习下计算机网络相关常识和术语,看了下开头几段话几乎挑不出几句正确的句子。
    tanszhe
        7
    tanszhe  
    OP
       2019-05-07 17:28:32 +08:00
    @qieqie 欢迎大神指正。
    列子是经过实测 完全可以跑起来的。
    tanszhe
        8
    tanszhe  
    OP
       2019-05-07 17:30:39 +08:00
    @Hzsgg0624 完全可以
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2793 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 14:51 · PVG 22:51 · LAX 06:51 · JFK 09:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.