道者编程


PHP SOCKET编程基础

先说点屁话:SOCKET是死马?中文称为:套接字,这个翻译非常变态,老子以前就被这个翻译坑了,一直搞不清楚到底是什么,套接字是个死马意思?一点都不形象。在英语里面SOCKET:插座的意思。

先看看SOCKET是怎么定义的:socket是由IP端口组成,通过某种协议实现不同计算机之间的通信。

既然SOCKET翻译过来是插座的意思,这就好理解了,服务器就是一个大的排插,链接就通过排插,那么现在如果链接需要什么东西?
1:服务端(被链接方)得有一个插孔。
2:客户端(链接方)得有一个插头。
有了这两个还是不行啊,服务器你得告诉我地址吧?我链接哪里的地址啊,这就是IP地址,IP地址就是服务器地址,但是服务器这么大,这么多服务,我要链接哪一个?好,我给你开一个插孔吧,这就是端口,但是插孔有三孔的,两孔的,圆孔的,你总得告诉我型号吧,这不就是协议吗?其实这里协议比喻还是不太形象,协议好比发快递的时候,你用的是顺丰,还是三通一达。

一:TCP/IP:

1:概述:TCP/IP:TCP/IP是互联网基础通信协议体系,它不是一个协议,是一个协议族,里面包括了TCP,UDP,HTTP,FTP,IMCP等等协议。

2:TCP/IP协议分层:ISO-OSI七层协议,TCP/IP四层。

为什么TCP/IP协议族只分4层,因为网络接口层下面属于硬件范畴了,和TCP/IP几乎没什么关系。

TCP/IP:由低到高:

(1)网络接口层:

为网络传输做准备,数据要在网络中传播,数据必须符合网络传输形态格式,第一步就是构成数据链路数据单元(帧),包括链路管理、差错控制、流量控制等(传输层也有流量控制)。

(2)网络层:

IP协议就在这里,主要功能为寻址与路由:寻址就是根据目标IP知道数据发到哪里,路由就是各个中间节点在原地址和目标IP地址之间找到合适的转发路径。

分段与重组:分段如果IP数据报大小超过了网络的最大传输单元(MTU),就需要分段。重组数据达到目标主机后,恢复原来的IP数据

(3)运输层:TCP,UDP,运输层虽然说也是协议,确切的说是一种运输方式可能更恰当。

(4)应用层:HTTP,FTP,SMTP等,为什么要用应用层协议?因为没有应用层协议,数据就无法识别,也就没什么意义,诸如数据的格式、大小、含义都不清楚。我们平常说的自定义协议就是指在应用层的自定义协议。

从上图协议分层看,似乎没看见SOCKET在哪里?我们把上面的TCP/IP协议分层图再稍微修改一下:

SOCKET编程:从图上我们就知道什么是SOCKET编程了,SOCKET不是什么协议,SOCKET编程就是对复杂的TCP/IP进行封装,比如如何链接、监听、关闭等等,和业务逻辑没毛关系。比如workerman用纯php写的socket框架,swoole用C编写的PHP socket框架……

二:PHP SOCKET编程方法:

PHP的SOCKET编程有两种种方法:

1:用C扩展php,比如swoole,门槛很高。

2:PHP自身语言编写,PHP提供了两种类型的socket:socket 和 stream_socket,两个互相不兼容。

(1):socket:更加底层,和C的SOCKET接口差不多,使用较麻烦,而且需要安装,编译php的时候需要加:--enable-sockets,方法以:socket_XXXX开头。

(2):stream_socket:PHP自己的接口,不需要安装,拿来即用,方便,缺点就是支持不够,一些参数无法自定义。方法以:stream_socket_XXXX开头,workerman就是基于这个来封装的。当 stream_socket无法设置某些参数的时候,通过socket_import_stream将stream_socket转换成底层sockets,然后通过socket_set_option设置stream_socket的socket选项。

三:PHP SOCKET实例

1:TCP流程步骤:

 服务端:socket->bind->listen->accept->send/recv->closesocket
 1:创建socket
 2:将创建的socket绑定到本地的IP和端口(bind)
 3:设为监听模式,监听链接请求状态(listen)
 4:等待客服端请求,线程进入阻塞态睡眠状态;当有请求过来,内核返回一个新socket描述符,用它与客户端连接(accept)
 5:返回的socket描述符和客户端通信(send/recv)
 6:循环第4步
 7:关闭socket(closesocket)
基本代码:
<?php
/** TCP Server **/
/*
 第1步:创建一个TCP socket
 AF_INET:IPv4网络协议;
 SOCK_STREAM:套接字类型TCP;
 SOL_TCP:TCP协议
 正确时返回一个套接字,失败时返回 FALSE
 */
set_time_limit(0); //防止超时
$socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);

if($socket == NULL){ //如果语法错误 
	exit(-1);
}
if (FALSE === $socket){ //创建失败
    $errcode = socket_last_error();
    die("socket create fail: " . socket_strerror($errcode));
}

//第2步:绑定;把IP和端口绑定到创建的socket
if(FALSE === socket_bind($socket, '0.0.0.0',23)){
	$errcode = socket_last_error();
    die("socket bind fail: " . socket_strerror($errcode));
}

/*
 第3步:监听,监听socket上的连接
 这个10是 int $backlog 参数,表示每一个端口最大的监听队列的长度
 服务端有一个队列,客户端的请求先放在队列里面,比如我这里设置为 10,
 那么如果并发来20个,那么 其他10个就先拒绝,客户端可能会收到一个错误提示:econnnrejected,
 如果是TCP,忽略该请求,服务端不会发送RST(连接重置)和FIN(关闭连接),那么客户端没有收到响应,就会重新发送SYN连接。
 在linux上该值受限于somaxconn 如果超过linux 的somaxconn设置,那么该值就是:somaxconn值
 */
if(FALSE === socket_listen($socket,10)){ //允许10个客户端排队
	$errcode = socket_last_error();
    die("socket listen fail: " . socket_strerror($errcode));
}
echo "TCP Service Start:\n"; //服务成功开启

/*
第4步:等待连接
等待接受一个Socket连接,成功时返回新的socket资源,该新的socket资源用于通信
*/
while(true){ //循环连接
	$conn = socket_accept($socket);

	if($conn === FALSE){
		die("accept() failed: reason: " . socket_strerror(socket_last_error()) . "\n");
	}
	$msg=iconv('utf-8', 'gbk', "连接成功"); 
	socket_write($conn, "$msg\n\r"); //返回给客户端数据:连接成功
	echo "$msg\n"; //服务端提示

	while (true) { //连接不断的情况下,重复发送
		//第5步:读取客户端发送的信息
		$input = @socket_read($conn, 1024); //1024为长度
		$input = trim($input); //去掉字符串空格
		if($input === FALSE || $input ==''){
			break; //跳出
		}
		//第6步:处理客户端数据
		socket_write($conn, $input, strlen ($input)); //返回给客户端数据
		echo "$input\n"; //server提示
		
	};
};

/*
第7步:关闭socket
需要关闭两个socket资源
1:最开始创建的socket资源,用于绑定和监听
2:socket_accept连接后产生的新的socket资源,用于通信
*/
socket_close($conn); //关闭连接
socket_close($socket); //关闭服务
 
客服端:socket->connect->send/recv->closesocket
 1:创建socket
 2、发出连接请求(connect)
 3、于服务器端进行通信(send/recv)
 4、关闭socket(closesocket)
<?php
/** TCP Client **/
//第1步:创建一个TCP socket,和server一样

$socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);

//第2步:连接
socket_connect($socket, '127.0.0.1', 23);


while ($buff = socket_read($socket, 1024)) { //循环读取服务端返回的信息
    echo("Response data:" . $buff . "\n"); // 服务端响应的数据
    echo("Send data:\n"); 
    $msg = fgets(STDIN); //输入待发送的数据
    socket_write($socket, $msg,strlen($msg)); //发送信息到服务端
}   
socket_close($socket);  //关闭连接

 上面这个例子,功能实现了一个简单的TCP连接,客户端可以重复发送,但是有个问题,如果某个客户端在连接,其他的客户端要等到该连接断开才能操作,也就是一次只能处理一个连接和传输,这肯定不行,要解决这个问题,有三个办法:

a:多进程:PHP是支持多进程的,主进程负责监听连接,子进程负责数据传输,需要开启扩展,--enable-pcntl,WorkerMan就是多进程的。

b:IO多路复用机制:PHP的socket_select函数可以干这个,安装Event扩展 或者 libevent扩展,PHP7只能用 Event。

c:多线程:需要安装扩展,pthread 扩展, --enable-maintainer-zts

以下为IO多路复用,支持多个客户端同时连接,把上面的例子改一下:

用了socket_select函数,底层是select模型

<?php
/** TCP Server **/
/*
 第1步:创建一个TCP socket
 AF_INET:IPv4网络协议;
 SOCK_STREAM:套接字类型TCP;
 SOL_TCP:TCP协议
 正确时返回一个套接字,失败时返回 FALSE
 */
set_time_limit(0); //防止超时
$socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);

if($socket == NULL){ //如果语法错误 
    exit(-1);
}
if (FALSE === $socket){ //创建失败
    $errcode = socket_last_error();
    die("socket create fail: " . socket_strerror($errcode));
}

//第2步:绑定;把IP和端口绑定到创建的socket
if(FALSE === socket_bind($socket, '0.0.0.0',23)){
    $errcode = socket_last_error();
    die("socket bind fail: " . socket_strerror($errcode));
}

/*
 第3步:监听,监听socket上的连接
 这个10是 int $backlog 参数,表示每一个端口最大的监听队列的长度
 服务端有一个队列,客户端的请求先放在队列里面,比如我这里设置为 10,
 那么如果并发来20个,那么 其他10个就先拒绝,客户端可能会收到一个错误提示:econnnrejected,
 如果是TCP,忽略该请求,服务端不会发送RST(连接重置)和FIN(关闭连接),那么客户端没有收到响应,就会重新发送SYN连接。
 在linux上该值受限于somaxconn 如果超过linux 的somaxconn设置,那么该值就是:somaxconn值
 */
if(FALSE === socket_listen($socket,10)){ //允许10个客户端排队
    $errcode = socket_last_error();
    die("socket listen fail: " . socket_strerror($errcode));
}
echo "TCP Service Start:\n"; //服务成功开启

//把所有的客户端连接放在这里
$clients = array($socket);

/*
第4步:等待连接
等待接受一个Socket连接,成功时返回新的socket资源,该新的socket资源用于通信
*/
while(true){ //循环连接
    // 创建一个副本,保证$clients不会被socket_select修改
    $read = $clients;
    //socket_select 构建多路IO复用,$read会保存多个客户端socket
    if (socket_select($read, $write = null, $except = null, 0) < 1) {
        continue;
    }
    //检查是否有客户端连接
    if (in_array($socket, $read)) {
        // 接受客户端连接,把连接放到clients数组
        $clients[] = $newsock = socket_accept($socket);
            
        // 向客户端发送:已连接
        socket_write($newsock, iconv('utf-8', 'gbk', "已连接")."\n");
        
        //server提示:已连接
        socket_getpeername($newsock, $ip); //获取客户端IP地址
        echo iconv('utf-8', 'gbk', "已连接").": {$ip}\n"; //输出已连接的客户端
            
        // 删除已经连接的socket
        $key = array_search($socket, $read);
        unset($read[$key]);
    }

    //连接不断的情况下,重复发送
    foreach ($read as $conn) {
        //第5步:读取客户端发送的信息
        $input = @socket_read($conn, 1024); //1024为长度
        $input = trim($input); //去掉字符串空格
        if($input === FALSE || $input ==''){ //如果客户端断开
            $key = array_search($conn, $clients);
            unset($clients[$key]);
            echo iconv('utf-8', 'gbk', "客户端已断开")."\n";
            continue;
        }
        //第6步:处理客户端数据
        socket_write($conn, $input, strlen ($input)); //返回给客户端数据
        echo "$input\n"; //server提示
    }
};

/*
第7步:关闭socket
需要关闭两个socket资源
1:最开始创建的socket资源,用于绑定和监听
2:socket_accept连接后产生的新的socket资源,用于通信
*/
socket_close($conn); //关闭连接
socket_close($socket); //关闭服务

2:UDP流程步骤:

 服务端步骤:socket->bind->recvfrom->closesocket
 1:创建socket
 2:将创建的socket绑定到本地的IP和端口(bind)
 3:等待接受客户端数据(recvfrom)
 4:关闭socket(closesocket)

 客服端:socket->connect->sendto->closesocket
 1:创建socket
 2、发送数据(sendto)
 3、关闭socket(closesocket)

TCP和UDP的步骤不一样,UDP少了一些步骤,主要是两者传输方式不一样,关于TCP和UDP的介绍和区别,网上有很多。


最新评论:
1楼 广东省广州市 电信 发表于 2019-08-02 17:52:11
总结的 很好,这个快餐吃的值!
共有 1 条记录  首页 上一页 下一页 尾页 1
我要评论:

看不清楚


道知

about me

身体是革命的本钱,健身,养生,运动,公众号都有!

链接