使用websocket构建实时通知程序或者web聊天程序

先搞清楚几个概念 


1、WebSocket是什么?

WebScoket是一种让客户端和服务器之间能进行双向实时通信的技术。它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTPS协议发送一条特殊的请求进行握手后创建了一个TCP连接,此后浏览器/客户端和服务器之间便可以通过此连接来进行双向实时通信。

2、为什么要用WebSocket?

1)一直以来,HTTP协议是无状态、单向通信的,即客户端请求一次,服务器回复一次。如果想让服务器消息及时下发到客户端,需要采用类似于轮询的机制,即客户端定时频繁的向服务器发出请求,这样效率很低,而且HTTP数据包头本身的字节量较大,浪费了大量带宽和服务器资源;

2)为提高效率,出现了AJAX/Comet技术,它实现了双向通信且节省了一定带宽,但仍然需要发出请求,本质上仍然是轮询;

3)新一代HTML标准HTML5推出了WebSocket技术,它使客户端和服务器之间能通过HTTP协议建立TCP连接,之后便可以随时随地进行双向通信,且交换的数据包头信息量很小;

Azure 应用程式闸道中的WebSocket 支援| Microsoft Docs

3、如何使用WebSocket?

在支持WebSocket的浏览器中,创建Socket之后,通过onopen、onmessage、onclose、onerror四个事件的实现来处理Socket的响应;

4、WebSocket与HTTP、TCP的关系

WebSocket和HTTP都属于应用层协议,且都是基于TCP的,它们的send函数最终也是通过TCP系统接口来做数据传输。那么WebSocket和HTTP的关系呢?WebSocket在建立握手连接时,数据是通过HTTP协议传输的,但是在连接建立后,真正的数据传输阶段则不需要HTTP协议的参与。中间重叠的部分是表示升级到websocket协议,它们之间的关系如下图:

WebSocket 协议简介- Aidan's personal website

5、什么情况下使用WebSocket?

如果游戏需要同时支持手机端、Web端,那毫无疑问应该使用WebSocket,现在各个平台都提供了相应的WebSocket实现。如果游戏不需要支持Web端,且对实时性要求比较高,如多人射击、MMORPG之类,那么使用TCP/UDP结合的原生Socket会比较好。

6、SocketIO

WebSocket是HTML5最新提出的规范,虽然主流浏览器都已经支持,但仍然可能有不兼容的情况,为了兼容所有浏览器,给程序员提供一致的编程体验,SocketIO将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口,也就是说,我们在使用SocketIO时,不用担心兼容问题,底层会自动选用最佳的通信方式。因此说,WebSocket是SocketIO的一个子集。

首先用composer程序中加入workman依赖

如果你对composer不熟悉,可以移步这里, https://blog.csdn.net/robinhunan/article/details/106377501 文章详细介绍了composer的配置,使用教程。

composer require  walkor/workerman

 

Server端程序,编辑server端程序ws.php 

<?php
/**
 * websocket server程序,监听端口19988
 */
use Workerman\Worker;
require_once __DIR__.'/vendor/autoload.php';

//初始化一个worker容器,监听19988端口
$worker = new Worker('websocket://0.0.0.0:19988');

/**
 * 这里进程数必须设置为1,否则会报端口占用错误
 * (php7 可以设置进程数大于1,前提是$inner_worker->reusePort=true)
 */
$worker->count = 1;

//$worker进程启动后创建一个text Worker,以便打开一个内部通讯端口
$worker->onWorkerStart = function($worker)
{
    //开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符
    $inner_text_worker = new Worker('text://0.0.0.0:19989');
    $inner_text_worker->onMessage=function($connection,$buffer)
    {
        //$data数组格式,里面有uid,表示向那个uid的页面推送数据
        $data = json_decode($buffer,true);
        $uid = $data['uid']; 

        //通过workerman,向uid的页面推送数据
        $ret = sendMessageByUid($uid,$buffer);

        //返回推送结果
        $connection->send($ret?'ok':'fail');
    };
    $inner_text_worker->listen();
};

//新增一个属性,用来保存uid到connection的映射
$worker->uidConnections = array();

//当有客户端发来消息时执行的回调函数
$worker->onMessage = function($connection,$data)
{
    global $worker;
    $date = date('Y-m-d H:i:s',time());
    file_put_contents('./workerman.log',$date.' :  '.$data.PHP_EOL,FILE_APPEND );
    $data = json_decode($data,true);

    if(!isset($connection->uid))
    {
        //正式环境需要根据$data里面的信息验证用户身份,演示用直接根据客户端第一次发来的uid直接绑定了用户
        $connection->uid = $data['uid']; 

        //保存uid到connection映射,这样可以方便的通过uid查找connection,实现针对特定的uid推送数据
        $worker->uidConnections[$connection->uid] = $connection;
        return;
    }
};

//当有客户端连接断开时,删除映射
$worker->onClose = function($connection)
{
    global $worker;
    if(isset($connection->uid))
    {
        //连接断开时,删除映射
        unset($worker->uidConnections[$connection->uid]);
    }
};

//向所有验证的用户推送数据
function broadcast($message)
{
    global $worker;
    foreach($worker->uidConnections as $connection)
    {
        $connection->send($message);
    }
}

//针对uid推送数据
function sendMessageByUid($uid,$message)
{
    global $worker;
    if(isset($worker->uidConnections[$uid]))
    {
        $connection = $worker->uidConnections[$uid];
        $connection->send($message);
        return true;
    }
    return false;
}

//运行所有worker
Worker::runAll();

    启动websocket的server程序 

     浏览器端程序或者客户端程序 client.html ,通过浏览器,建议是chrome或者firefox浏览器访问 http://localhost/client.html, 并打开控制台,我这里是简化了流程,正常的话,还是需要登录的,可以在下面的send消息里面,加上用户信息,server端判断是否正确,再绑定,为了简化理解,我就去掉了websocket登录部分。

<script>
ws = new WebSocket("ws://127.0.0.1:19988/");
ws.onopen = function() {
    console.log("连接成功");
    ws.send('{"uid":"yubing"}'); //这里放登录信息
    console.log("给服务器发送uid信息:yubing");
};
ws.onmessage = function(e) {
    console.log("收到服务端的消息:" + e.data);
};
</script>

打开控制台后,可以发现浏览器已经连接上websoket服务器,并且给服务器发送了一个消息。

 

第三方客户端,直接调用tcp发送消息,下面的例子client.php 演示了通过php直接发送推送消息,执行php client.php ,浏览器对应的上个用户我这里设置的是yubing,就能收到消息了。

<?php
//建立socket连接到内部推送端口
$client = stream_socket_client('tcp://127.0.0.1:19989',$errno,$errmsg,1);

//推送的数据,包含uid字段,表示是给这个uid推送
$data = array('uid'=>'yubing','status' => 'success','msg' => date('Y-m-d H:i:s'));

//发送数据,注意19989端口是text协议的端口,text协议需要在数据末尾加上换行符
fwrite($client,json_encode($data)."\n");

//读取推送结果
echo fread($client,8192);

 切换到浏览器,会发现浏览器已经收到了,php 客户端发送过来的消息。

 

原理

一般我们开发的WebSocket服务程序使用ws协议,明文的。但是怎样让它安全的通过互联网传输呢?这时候可以通过nginx在客户端和服务端直接做一个转发了, 客户端通过wss访问,然后nginx和服务端通过ws协议通信。如下图所示:

 nginx代理websocket服务配置文件

upstream websocket1{
    ip_hash;
    server localhost:19988 weight=50 fail_timeout=10s;
    server localhost:29988 weight=50 fail_timeout=10s;
}
 
server
{
	listen 80;
	listen 443;
	#listen [::]:80;
	server_name test.com.cn;
	index index.html index.htm index.php;
	root  /data/www/web/public;

	charset utf-8;

	ssl on;
	ssl_certificate /usr/local/nginx/conf/cert/test.com.cn.crt;
	ssl_certificate_key /usr/local/nginx/conf/cert/test.com.cn.key;
	ssl_session_timeout 5m;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
	ssl_prefer_server_ciphers on;



	location /wss
	{
		proxy_pass http://websocket1;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		proxy_set_header X-real-ip $remote_addr;
		proxy_set_header HOST $host;
		proxy_set_header X-Forwarded-For $remote_addr;
	}
}

这时候客户端通过wss://test.com.cn/wss,就可以加密连接了。

<script>
ws = new WebSocket("wss://test.com.cn/wss");
ws.onopen = function() {
}
</script>

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章