文件下载响应头设置

本文对比了下载文件头几种不同设置方式的兼容性,并提供了测试方法。

如果想让一个文件调用浏览器下载功能进行下载,而不是直接在浏览器中打开,则需要设置响应头的Content-Disposition字段。
下载响应头的设置在不同浏览器中的兼容性各不一样,下面将探索各个浏览器之间的不同,以及找出一种可以兼容各大主流浏览器的方案。
测试环境说明(可根据兼容性要求自行测试):
Chrome 57,系统版本MacOS 10.12.3
Safari 10.0.3,系统版本MacOS 10.12.3
Firefox 52,系统版本Windows 2003
Edge 14,系统版本Windows 10
IE 11,系统版本Windows Server 2008
IE10,系统版本Windows Server 2008
IE9,系统版本Windows Server 2008

服务端代码如下:

$fnType = $_GET['fntype'];
if ($fnType == 'ascii') {
	$filename = 'English Filename.txt';
} else {
	$filename = '中文文件名.txt';
}
$urlencode = isset($_GET['urlencode']);
if ($urlencode) {
	$filename = rawurlencode($filename);
}
$outputType = isset($_GET['output']) ? $_GET['output'] : null;
if ($outputType == 'old') {
	header("Content-Disposition: attachment; filename=$filename");
} else if ($outputType == 'new') {
	header("Content-Disposition: attachment; filename*=UTF8''$filename");
} else {
	header("Content-Disposition: attachment; filename=$filename; filename*=UTF8''$filename");
}
if (isset($_GET['withcontenttype'])) {
	header('Content-Type: text/plain');
} else {
	header('Content-Type: ');
}

查看响应头方法:
curl -I url

一、纯ASCII字符文件名兼容性
测试地址:https://demo.lyz810.com/downloadHeader/?fntype=ascii&output=old
响应头:
Content-Disposition: attachment; filename=English Filename.txt
结果:
Chrome:English Filename.txt
Safari:English Filename.txt
Firefox:English(由于文件名含有空格,空格后面的字符都被Firefox忽略了)
Edge:English Filename.txt
IE 11:English Filename.txt
IE10:English Filename.txt
IE9:English Filename.txt

二、纯ASCII字符文件名url编码
测试地址:https://demo.lyz810.com/downloadHeader/?fntype=ascii&output=old&urlencode=1
响应头:
Content-Disposition: attachment; filename=English%20Filename.txt
结果:
Chrome:English Filename.txt
Safari:English%20Filename.txt
Firefox:English%20Filename.txt
Edge:English Filename.txt
IE 11:English Filename.txt
IE 10:English Filename.txt
IE 9:English Filename.txt

三、中文文件名,utf-8不进行url编码
测试地址:https://demo.lyz810.com/downloadHeader/?output=old
响应头:
Content-Disposition: attachment; filename=中文文件名.txt
结果:
Chrome:中文文件名.txt
Safari:中文文件名.txt
Firefox:中文文件名.txt
Edge:涓枃鏂囦欢鍚_txt.txt
IE 11:涓枃鏂囦欢鍚_txt
IE 10:downloadHeader(url的pathname部分)
IE 9:涓枃鏂囦欢鍚_txt

四、中文文件名,utf-8进行url编码
测试地址:https://demo.lyz810.com/downloadHeader/?output=old&urlencode=1
响应头:
Content-Disposition: attachment; filename=%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt
结果:
Chrome:中文文件名.txt
Safari:%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt
Firefox:%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt
Edge:中文文件名.txt
IE 11:中文文件名.txt
IE 10:中文文件名.txt
IE 9:中文文件名.txt

五、中文文件名,使用filename*并进行url编码
测试地址:https://demo.lyz810.com/downloadHeader/?output=new&urlencode=1
响应头:
Content-Disposition: attachment; filename*=UTF8”%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt
结果:
Chrome:中文文件名.txt
Safari:downloadHeader.txt
Firefox:中文文件名.txt
Edge:downloadHeader
IE 11:downloadHeader
IE 10:downloadHeader
IE 9:downloadHeader

六、中文文件名,同时使用filname*和filename,并进行url编码
测试地址:https://demo.lyz810.com/downloadHeader/?urlencode=1
响应头:
Content-Disposition: attachment; filename=%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt; filename*=UTF8”%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt
结果:
Chrome:中文文件名.txt
Safari:%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.txt
Firefox:中文文件名.txt
Edge:downloadHeader
IE 11:downloadHeader
IE 10:downloadHeader
IE 9:downloadHeader

七、结论
1.IE系列(包括Edge)对于中文,只支持urlencode的方式,不能识别filename*
2.Firefox支持不编码的中文和filename*
3.Chrome支持各种上面测试的各种类型
4.Safari仅支持ISO格式的中文,此文中并未给出测试实例,请参考http://lgbolgger.iteye.com/blog/2108396

WebSocket服务端开发(九)-WebSocketServer类事件

本文介绍WebSocketServer类中的事件。

WebSocketServer类支持以下事件:
onstarted:
服务器启动后触发,socket_listen成功执行后,服务器进入启动状态

onconnected:
与客户端建立连接完成后触发,此时连接已放入socket连接池中

onUpgradePartReceive:
当收到部分WebSocket握手时触发,仅当收到的握手包不完整时会触发该事件

onHandShakeFailure:
在握手失败后触发

onHandShakeSuccess:
握手成功后触发

ondisconnected:
关闭客户端连接后触发,此时还未将连接从socket连接池中移除,当disconnect函数的第二个参数slient为true时不触发该事件

onAfterRemoveSocket:
在移除socket连接后触发

onafterhealthcheck:
在健康检查完成后触发,默认该事件会断开不健康的连接

onerror:
遇到socket错误时触发

onshutdown:
服务器关闭时触发

  function onstarted($serverSocket) {
    if ($this->debug) {
      printf('Server start at %s', date('Y-m-d H:i:s') . "\n");
    }
  }

  function onconnected($socket) {
    if ($this->debug) {
      printf('Socket connect at %s-%s', date('Y-m-d H:i:s'), $socket . "\n");
    }
  }

  function onUpgradePartReceive($socketId) {
    if ($this->debug) {
      $buffer = $this->socketListMap[$socketId]['buffer'];
      printf('Receive Upgrade Part at %s-%s%s(%d bytes)' . "\n", date('Y-m-d H:i:s'), $socketId . "\n", $buffer, strlen($buffer));
    }
  }

  function onHandShakeFailure($socketId) {
    if ($this->debug) {
      printf('HandShake Failed at %s-%s', date('Y-m-d H:i:s'), $socketId . "\n");
    }
  }

  function onHandShakeSuccess($socketId) {
    if ($this->debug) {
      printf('HandShake Success at %s-%s', date('Y-m-d H:i:s'), $socketId . "\n");
    }
  }

  function ondisconnected($socketId) {
    if ($this->debug) {
      printf('Socket disconnect at %s-%s', date('Y-m-d H:i:s'), $socketId . "\n");
    }
  }

  function onAfterRemoveSocket($socketId) {
    if ($this->debug) {
      printf('[onAfterRemoveSocket]remove:' . $socketId . ',left:' . implode('|', array_keys($this->socketListMap)) . "\n");
    }
  }

  function onafterhealthcheck($unhealthyList) {
    foreach ($unhealthyList as $socketId) {
      $this->disconnect($socketId);
    }
  }

  function onerror($errCode, $socketId) {
    switch ($errCode) {
    case 10053:
      $this->disconnect($socketId);
      break;
    default:
      if ($this->debug) {
        echo 'Socket Error:' . $errorCode . "\n";
      }
      break;
    }
  }

  function onshutdown() {
    if ($this->debug) {
      printf('Server shutdown!');
    }
  }