关于TCP的一些自我理解 发布日期:2017-02-05 11:33:34     博主推荐★

博主菜鸟一枚,总是听人说tcp是长连接,httpgt;那么菜鸟的博主就想了,既然tcp可以长连接,那么是否就可以用tcp实现持久的通信呢。永远不断开呢?于是博主不断的尝试php中各种实现tcp通信的函数。最后博主失望了,都不行什么socket_create什么 stream_socket_server啊!都尝试了,不断的失败让博主明白了一个道理

注(1)长连接和持久连接不是一回事,基于http协议下的tcp连接永远是会通信完毕就断的


这个理论的详细解说请参考①部分的解说:

要理解为什么会出现HTTP,WebSocket,可以做这样一个假设。
假设平行宇宙1984年(我懒的查数据,所以就说一个平行宇宙了,随便看看吧),人们使用TCP协议进行通讯,假设那时候还没有网页浏览器这一说法,大家都是通过各种软件直接通讯。
假设到了1986年,人们使用浏览器来浏览网页,假设当时电脑时100MHz,100M内存。对TCP协议熟悉一点,大概也能猜到一个TCP链接,会消耗一点点内存,假设是32k(具体我也不知道),那么如果一台几万块钱的服务器最大能支持100M/32k=3200个连接。显然,如果一个公司,面向全世界提供网页服务,如果使用TCP,最多也就3200个人同时看网页。
于是服务器要求“所有客户端,打开网页之后,必须关闭TCP连接”。这就是(猜测的)HTTP的初衷了。
按照这个协议,服务器接受TCP连接,几秒钟之内读取数据,检验之后,回复数据,断开连接。所谓的节省“资源”也没说明白到底节省了什么“资源”。
等到二十年后,平行宇宙的2004年,QQ桌面版好好的,QQ网页版用的越来越多。由于浏览器都是连接之后很快断开,QQ网页版,只能靠各种polling方式持续交互数据(HTTP keep-alive也有自己的缺点,其他答主讲的很好),浪费大量的带宽(这时候带宽的费用就大了),同时客户端收到消息也不及时,还有各种其它问题。
QQ网页版想直接用TCP协议长时间连接,但是QQ网页版能做的,都是浏览器允许做的。可以说,websocket的出现,就是因为浏览器不支持TCP直连,不给开后门。
于是“希望所有的浏览器都能够直接进行TCP连接”,于是浏览器出现了websocket协议。
所以,因为某些原因,人们在TCP上面弄了一个HTTP协议,把TCP支持的一些特性删除了,然后若干年之后想要那些被删除的特性,返回TCP,于是出现了WebSocket。
WebSocket实际上可以看作HTTP的升级!“不是WebSocket基于HTTP,而是可以看成HTTP基于WebSocket”。


好了,基于以上的理论知识我们知道了,长连接并不等于持久连接,长连接只是说我在发送的过程我可以不断的发,没事!也就是说如果你断的从客户端写消息到服务器,这个连接不会断的,但是不能说我停下来接收消息,这时候连接就断了。再也接不上了。(socket_write或者fputs、fwrite之类的写,但是不能socket_read、fgets、fread)

好了,那么怎么干,keep-alive不就好了,但是又有人说这个浪费资源,什么C10K问题、占用资源理论,总之这玩意不好,也不是我们想要的方法,那么接着看,好了,有了websocket是什么呢?是协议,是把当初HTTP上出TCP的部分给添加回来了,这就是websocket的来源,那么基于网络编程中,一切编程皆协议的理论来说,我们只需要完成握手协议即可,那么说干咱就干,搞起,上代码!


服务端:

<?php
    class SocketServer
{
private $_host;
private $_port;
private $_socket;
private $_maxuser = 0;
private $_socket_connect = [];//socket存储的数组
private $_isHand = [];
public function __construct($host,$port){
$this -> _host = $host;
$this -> _port = $port;
}
/**
*socket连接
*/
public function socketConnect(){
//创建一个socket对象
$this -> _socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//允许使用本地地址
            socket_set_option($this->_socket, SOL_SOCKET, SO_REUSEADDR, TRUE);
socket_bind($this->_socket, $this->_host, $this->_port);
            //监听端口,并且达到一定的数量后,就会停止
            socket_listen($this->_socket,$this->_maxuser);
            $this->_socket_connect[] = $this->_socket;
            echo "listen is success\n\n";
}
private function socketAccept(){
//因为后续执行socket_select()将会只剩下活跃的socket,将不活跃的进程从数组中拿掉,所以不能拿连接数组去做,做一次copy赋值
$activity_socket_connect = $this->_socket_connect;
//第一参数,所有连接中的socket,
//$write表示监听写操作,如果传空代表不关心写操作,
//$except代表监听从socket数组中去掉部分,null代表监听全部
//最后一个参数,0代表立马结束,>0代表多少秒之后结束,null代表监听到才结束,做阻塞进程
$write  = null;
$except = null;
socket_select($activity_socket_connect,$write, $except, null);
//下来了,那么说明这时候已经读到消息了,但是不知道哪些消息下来了,所以需要遍历所有活跃中的socket
//这时候有一个说法:1、如果新连接到来的时候,那么将是监听的socket活跃;2、如果是新的数据或者客户端断开了,那么活跃的将是客户端的socket;
            var_dump(count($activity_socket_connect));
            //如果监听socket活跃,那么必然有新的连接到来
            if(in_array($this->_socket,$activity_socket_connect)){
echo "listen socket is activity,so your should create a new socket connect\n\n";
$accept = @socket_accept($this->_socket);
if($accept === false){
//说明没有收到消息,那么就是一次没意义的操作
//to do something
echo "new socket connect empty\n\n";
}
if($accept < 0){
echo "new socket connect error: ".socket_strerror($connection)."\n\n"; 
}
if($accept > 0){
    if(!@socket_recv($accept,$data, 1024, 0) || !$data){
   echo "because the data is empty so don't to connect\n\n";
}else{
//创建一个新的连接
$res = $this->add_accept($accept,$data);
if($res){
    echo "new socket connect success \n\n";
}else{
echo "new socket connect error \n\n";
}
}
}
//将自己从数组中剔除
$key = array_search($this->_socket,$activity_socket_connect);
                unset($activity_socket_connect[$key]);
}
var_dump(count($activity_socket_connect));
            //接下来就是一系列连接的循环
            foreach($activity_socket_connect as $client_connect){
//判断是否异常的连接出现,以后多进程,更有可能会出现这种状况的,所以做一次防止操作
if(!in_array($client_connect,$this->_socket_connect)){
//这里就是一个新的,数据没有及时统计进来的一个连接,这时候无法处理,按道理是不可能出现的
//所以为了避免异常,将其关闭,另外就是continue执行即可
//1、关闭
$this->close($client_connect);
continue;
}
//以下连接就属于正常的连接了
//1、接收数据
if(!@socket_recv($client_connect,$data, 1024, 0) || !$data){
$this->close($client_connect);
echo "socket connect close\n\n";
continue;
}else{
   //输出得到的数据
echo "get data:\n\n";
var_dump($this->decode($data));
echo "----------success-----------\n\n";
$this->send($client_connect,'hello world');
}
}
}
//增加一个初次连接的用户
private function add_accept($accept,$data) {
//握手
if($this->upgrade($accept,$data)){
$this->_socket_connect[] = $accept;
return true;
}
echo "client num is ".(count($this->_socket_connect)-1)."\n";
return false;
}
//响应升级协议
private function upgrade($accept, $data) {
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$data,$match)) {
$key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$upgrade  = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $key . "\r\n\r\n";  //必须以两个回车结尾
socket_write($accept, $upgrade, strlen($upgrade));
echo "upgrade is success\n\n";
return true;
}else{
//握手失败
echo "upgrade is error\n\n";
return false;
}
}
//关闭一个连接
private function close($accept) {
$index = array_search($accept, $this->_socket_connect);
socket_close($accept);
unset($this->_socket_connect[$index]);
echo "client num is ".(count($this->_socket_connect)-1)."\n";
}
function decode($buffer) {
$len = $masks = $data = $decoded = null;
$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 $decoded;
}
// 返回帧信息处理
        function frame($s) 
        {
            $a = str_split($s, 125);
            if (count($a) == 1) {
                return "\x81" . chr(strlen($a[0])) . $a[0];
            }
            $ns = "";
            foreach ($a as $o) {
                $ns .= "\x81" . chr(strlen($o)) . $o;
            }
            return $ns;
        }
        
        // 返回数据
        function send($client, $msg)
        {
            $msg = $this->frame($msg);
            socket_write($client, $msg, strlen($msg));
        }
public function run(){
while(true){
$this->socketAccept();
usleep(500);//让CPU调整一下
}
}
}
//调用SocketServer类
$socketServer = new SocketServer('127.0.0.1',2008);
$socketServer->socketConnect();
$socketServer->run();
?>

客户端:

<html>
   <head>
       <meta charset="utf-8">
   <title>客户端测试</title>
   </head>
   <body>
      hello world!
  <button onclick="sendMessage()">发送消息</button>
   </body>
   <script type="text/javascript">
        var ws = new WebSocket("ws://127.0.0.1:2008");
ws.onopen = function(){
console.log("success");
};
ws.onerror = function(){
console.log("error");
};
ws.onclose = function(){
    console.log('close');
};
ws.onmessage = function(event){
    console.log(event.data);
};
var i = 0;
function sendMessage(){
   ws.send("ssss"+(i++));
}
   </script>
</html>

核心知识点:

socket_select 

① 做阻塞用的,他的第一个参数read在执行前是要监听的所有连接,当他返回的时候只有活跃连接留下了,第二个参数关不关心写操作,我们写NULL表示我们不关心,第三个参数是说你要在read数组中排除掉哪些不监听,第四个参数0包括大于0表示多少秒后返回,也就0代表立马返回,1代表秒后返回,如果是NULL,就代表监听到返回期间一直阻塞!

② 知识点二就是说socket_select返回的活跃的read参数数组中,如果有当前服务端监听的连接的话,就代表有新建立的连接,因为只有新建立的连接才会使服务端的监听连接活跃起来,其他的比如一个连接断开了,是当前他存在的连接活跃,这时候是没有消息的代表他断开了,如果有消息就代表他给服务端发送了消息


只要深刻体会了这个函数的意义及新建连接时候是让listen的活跃的逻辑,就基本可以完成整个编程成了!


有什么不足,希望大家指教

博文地址:https://blog.ahamu.cn/blog/detail.html?id=85
   
推荐文章
  • 1
    sysbench
    2020/07/08
  • 2
    phper转java记录篇-spring boot
    2020/06/10
  • 3
    thinkphp5.0使用路由之后,post请求的
    2020/05/19
  • 4
    springboot单元测试aop失效
    2020/05/15
  • 5
    脑海中的JVM
    2020/05/12
  • 6
    IDEA搜索插件时显示search results
    2020/05/12
  • 7
    spring boot 配置加载源码查找
    2020/04/20
  • 8
    通过javap命令分析java汇编指令
    2020/04/16
  • 9
    IDEA小知识:查看JVM内存使用情况的步骤
    2020/04/16
  • 10
    springboot-加载自定义的properti
    2020/04/14
  • 11
    Jenkins执行shell脚本无法启动子进程解决
    2020/04/03
  • 12
    mac idea激活找专业的
    2020/04/02
  • 13
    Jenkins + DockerSwarm 实现弹
    2020/03/31
  • 14
    mac swarm学习过程
    2020/03/31
  • 15
    spring cloud
    2020/03/18
  • 16
    JAVA开发中遇到的问题记录002
    2020/03/12
  • 17
    JAVA开发中遇到的问题记录001
    2020/03/07
  • 18
    php -i查看信息
    2020/02/23
  • 19
    phpStorm中使用xdebug工具调试dock
    2019/12/09
  • 20
    讲的比较好的B+树执行原理的文章
    2019/12/09
最喜标签
NYOJ 面试 AJAX ping CentOS 灰度算法 YII2