文章:https://blog.csdn.net/fff058/article/details/75267672

要理解socket就要先理解http和tcp的区别,简单说就是一个是短链,一个是长链,一个是去服务器拉数据,一个是服务器可以主动推数据。
而socket就是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
-来自网络
那么就需要服务端和客户端双向编程,本文使用php+js方式实现

客户端

使用现代浏览器的WebSocket API,microsoft websocket lib
js实现:

  1. var wsServer = 'ws://127.0.0.1:8080';
  2. var ws = new WebSocket(wsServer); // 向指定服务器发送一个握手请求,如果服务器返回合法的header则握手成功
  3. ws.onmessage = function (evt) { // 通过监听onmessage时间来处理服务器发来的消息
  4. // do sth here
  5. }

php实现:
思路:
1.监听:挂起一个进程来监听来自客户端的请求
2.握手:对第一次合法的请求发送合法的header回去
3.保持连接:有新消息就广播出去,直到与客户端断开来连接
4.接受另一个请求,重复步骤2,3
代码:

  1. public function start_server() {
  2. $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
  3. //允许使用本地地址
  4. socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE);
  5. socket_bind($this->socket, $this->host, $this->port);
  6. //最多10个人连接,超过的客户端连接会返回WSAECONNREFUSED错误
  7. socket_listen($this->socket, $this->maxuser);
  8. // 执行进程
  9. while(TRUE) {
  10. $this->cycle = $this->accept;
  11. $this->cycle[] = $this->socket;
  12. //阻塞用,有新连接时才会结束
  13. socket_select($this->cycle, $write, $except, null);
  14. foreach ($this->cycle as $k => $v) {
  15. if($v === $this->socket) {
  16. if (($accept = socket_accept($v)) < 0) {
  17. continue;
  18. }
  19. //如果请求来自监听端口那个套接字,则创建一个新的套接字用于通信
  20. $this->add_accept($accept);
  21. continue;
  22. }
  23. $index = array_search($v, $this->accept);
  24. if ($index === NULL) {
  25. continue;
  26. }
  27. if (!@socket_recv($v, $data, 1024, 0) || !$data) {//没消息的socket就跳过
  28. $this->close($v);
  29. continue;
  30. }
  31. if (!$this->isHand[$index]) {
  32. $this->upgrade($v, $data, $index);
  33. if(!empty($this->function['add'])) {
  34. call_user_func_array($this->function['add'], array($this));
  35. }
  36. continue;
  37. }
  38. $data = $this->decode($data);
  39. if(!empty($this->function['send'])) {
  40. call_user_func_array($this->function['send'], array($data, $index, $this));
  41. }
  42. }
  43. sleep(1);
  44. }
  45. }
  46. //增加一个初次连接的用户
  47. private function add_accept($accept) {
  48. $this->accept[] = $accept;
  49. $index = array_keys($this->accept);
  50. $index = end($index);
  51. $this->isHand[$index] = FALSE;
  52. }
  53. //关闭一个连接
  54. private function close($accept) {
  55. $index = array_search($accept, $this->accept);
  56. socket_close($accept);
  57. unset($this->accept[$index]);
  58. unset($this->isHand[$index]);
  59. if(!empty($this->function['close'])) {
  60. call_user_func_array($this->function['close'], array($this));
  61. }
  62. }
  63. //响应升级协议
  64. private function upgrade($accept, $data, $index) {
  65. if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$data,$match)) {
  66. $key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
  67. $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
  68. "Upgrade: websocket\r\n" .
  69. "Connection: Upgrade\r\n" .
  70. "Sec-WebSocket-Accept: " . $key . "\r\n\r\n"; //必须以两个回车结尾
  71. socket_write($accept, $upgrade, strlen($upgrade));
  72. $this->isHand[$index] = TRUE;
  73. }
  74. }

关键位置:
1.while(true) 执行进程,否则执行一次后进程就退出了
2.socket_select 和 socket_accept 的使用
3.客户端第一次请求时握手
流程解释:
当有一个新的客户端请求到达,用socket_accept创建一个资源,并加入到$this->accept连接池里面。并将它的标示isHand设为false,那么下次循环(因为$this->cycle[] = $this->socket;$this->cycle有变化,所以socket_select会返回)的时候就会执行upgrade握手。然后等待它的新消息即可。

函数解释

1.socket_select 是同时多连接的关键
socket_select ($sockets, $write = NULL, $except = NULL, NULL);
$sockets是一个存放文件描述符的数组,当它有变化(就是有新消息到或者有客户端连接/断开)时,socket_select函数才会返回,继续往下执行。
$write是监听是否有客户端写数据,传入NULL是不关心是否有写变化。
$except是$sockets里面要被排除的元素,传入NULL是”监听”全部。
最后一个参数是超时时间
如果为0:则立即结束
如果为n>1: 则最多在n秒后结束,如遇某一个连接有新动态,则提前返回
如果为null:如遇某一个连接有新动态,则返回

2.socket_accept
此函数接受唯一参数,即前面socket_create创建的socket文件(句柄)。返回一个新的资源,或者FALSE。本函数将会通知socket_listen(),将会传入一个连接的socket资源。一旦成功建立socket连接,将会返回一个新的socket资源,用于通信。如果有多个socket在队列中,那么将会先处理第一个。关键就是这里:如果没有socket连接,那么本函数将会等待,直到有新socket进来。
如果前面不用socket_select在没有socket的时候阻塞住程序,那么就卡在这里永远无法结束了