利用Swoft实现PHP+websocket直播,即时通讯代码
PHP.swoft 框架部分
用swoft框架做webSocket服务器 linux 环境
- 一、推荐用 composer 安装 swoft 框架。 这里只做概述不讲详细安装步骤
-
composer create-project swoft/swoft swoft
安装完后进入目录
- 二、安装好后需要引入 WebSocket 服务
-
composer require swoft/websocket-server
- 三、 配置ws端口文件 .env
-
一般就在 swoft/.env 这里找到 #HTTP 部分
HTTP_PORT这个是项目运行后,监听socket端口# HTTP HTTP_HOST=0.0.0.0 HTTP_PORT=8188 HTTP_MODE=SWOOLE_PROCESS HTTP_TYPE=SWOOLE_SOCK_TCP
- 四、创建控制器 websocket 控制器
-
php bin/swoft gen:websocket Camera --prefix /Camera
生成的代码一般在 swoft/app/WebSocket/xxx.php/*** Class CameraController* @package App\WebSocket* @WebSocket("/con_camera/camera")*/ class CameraController implements HandlerInterface
注意
@WebSocket("/con_camera/camera")
这里是访问的路由需要自己改/*** @param Server $server* @param Request $request* @param int $fd*/ public function onOpen(Server $server, Request $request, int $fd) {$server->push($fd, '{"type":"id", "id":"'.$fd.'"}'); }/*** @param Server $server* @param Frame $frame* @return mixed*/ public function onMessage(Server $server, Frame $frame) {// 这里是广播。 自己发出的内容,不广播给自己\Swoft::$server->sendToSome($frame->data, [], [$frame->fd]);// $server->push($frame->fd, $frame->data); }/*** @param Server $server* @param int $fd* @return mixed*/ public function onClose(Server $server, int $fd) {$server->close($fd);// do something. eg. record log }
后台运行swoft项目
php bin/swoft ws:restart -d
结束用php bin/swoft ws:stop
- 五、测试代码
-
大家可以在 function onOpen函数内 做打印
/*** @param Server $server* @param Request $request* @param int $fd*/public function onOpen(Server $server, Request $request, int $fd){var_dump($fd,'这里是测试部分');$server->push($fd, '{"type":"id", "id":"'.$fd.'"}');}
测试方法很多,我只用 websocket 测试 谷歌浏览器
<!DOCTYPE html> <html> <head><meta charset="utf-8"></head> <body> <script type="text/javascript"> function websocketOpen () {if ("WebSocket" in window) {alert("您的浏览器支持 WebSocket!")return false;} else {// 浏览器不支持 WebSocket123alert("您的浏览器不支持 WebSocket!");return true;} }if(websocketOpen()){console.log('链接失败') }let ws = new WebSocket("wss://localhost.com/con_camera/camera");ws.onopen = function (evt) {alert('连接成功') }ws.onmessage = function (evt) {let received_msg = evt.dataconsole.log(received_msg); }ws.onclose = function (){alert('关闭链接') }</script> </body> </html>
new WebSocket(“wss://localhost.com/con_camera/camera”)
这里 wss代表服务器配置了https证书,如果服务器没有配置证书需要用 ws
以上内容是PHP部分,没有多少需要编辑的。php.swoft只做转发和广播
webRtc部分
首先需要了解WebSocket,MediaDevices,RTCPeerConnection,信令服务器作用
WebSocket
基本都知道吧,不懂的看 官方文档
WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
基本理解成 它是一个双向管道
MediaDevices 媒体设备
简单的理解成。调用媒体设备,获取 摄像头媒体设备 输入的信息 官方文档
RTCPeerConnection
简单理解成本地到远端的连接 官方文档
官方推荐引入Adapter.js保持浏览器的兼容性
信令服务器
作为webRTC中极为重要的一部分,会话管理需要建立服务器端与客户端之间的连接。
有人就问了:webRTC建立的是点对点连接,流数据是从浏览器直接传输到另一个浏览器,不需要服务器周转,怎么还需要建立服务器端与客户端连接呢?
这是个很好的问题!尽管webRTC建立的是P2P连接,但由于流数据传输需要一条信道,而这个信道则是由信令服务器提供的。而在webRTC中并没有这一过程,所以需要我们手动建立信号的传递和交涉过程。
信令服务器的实现软件有很多,我们这里用coturn官方文档
参考原文:https://blog.csdn.net/vainfanfan/article/details/82632737
我们组合一下信息
- 主播与客户端建立websocket通道
- 通过web获取流媒体MediaDevices
- 主播生成流媒体通道插座信息,通过websocket传输给客户,客户受到信息后设置插销信息
与主播建立流媒体通道RTCPeerConnection。并且把主播把流媒体通过流媒体通道传给客户端 - 这中间通过coturn信令服务器把流媒体穿透给外网,让客户端可以在外网获取
步骤很简单,但是中间遇到很多坑,我会把遇到的问题,也列出来
实现MediaDevices获取流媒体
- 需要的文件
- jquery-3.2.1.min.js
- adapter.js这个最好用迅雷下载
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="description" content="php web-rtc例子,一对一聊天-基于workerman"><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"><meta itemprop="description" content="Video chat using the reference WebRTC application"><meta itemprop="name" content="AppRTC"><meta name="mobile-web-app-capable" content="yes"><meta id="theme-color" name="theme-color" content="#1e1e1e"><title>webRtc视频通话</title>
</head><body><div class="videos"><video id="localVideo" autoplay></video><video id="remoteVideo" autoplay class="hidden"></video>
</div><script src="jquery-3.2.1.min.js"></script>
<script src="adapter.js"></script>
<script type="text/javascript">var localVideo = document.getElementById('localVideo');var remoteVideo = document.getElementById('remoteVideo');navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;navigator.mediaDevices.getUserMedia({// audio: true,video: true}).then(function (stream) {localVideo.srcObject = stream;localStream = stream;localVideo.addEventListener('loadedmetadata', function () {console.log('视频加载成功')});}).catch(function (e) {alert(e);});
</script>
</body>
</html>
我的电脑摄像头坏了
实现链接媒体通道RTCPeerConnection 在局域网内可测试
用上面的代码修改下
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="description" content="php web-rtc例子,一对一聊天-基于workerman"><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"><meta itemprop="description" content="Video chat using the reference WebRTC application"><meta itemprop="name" content="AppRTC"><meta name="mobile-web-app-capable" content="yes"><meta id="theme-color" name="theme-color" content="#1e1e1e"><title>webRtc视频通话</title>
</head><body><div class="videos"><video id="localVideo" autoplay></video><video id="remoteVideo" autoplay class="hidden"></video>
</div><script src="jquery-3.2.1.min.js"></script>
<script src="adapter.js"></script>
<script type="text/javascript">var localVideo = document.getElementById('localVideo');var remoteVideo = document.getElementById('remoteVideo');// 流媒体对象navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// 流媒体通道配置var configuration = {iceTransportPolicy:"all",iceCandidatePoolSize:"0"};// 协商次数var answer = 1;/*流媒体获取
*/function openMediaSetSend() {navigator.mediaDevices.getUserMedia({// audio: true,video: true}).then(function (stream) {localVideo.srcObject = stream;localStream = stream;localVideo.addEventListener('loadedmetadata', function () {// 广播客户端 准备连接 publish('client-call', null)});}).catch(function (e) {alert(e);});}/*websocketOpen
*/function websocketOpen () {if ("WebSocket" in window) {alert("您的浏览器支持 WebSocket!")return false;} else {// 浏览器不支持 WebSocket123alert("您的浏览器不支持 WebSocket!");return true;}}if(websocketOpen()){console.log('链接失败')}let ws = new WebSocket("wss://www.cwtui.com/con_camera/camera");//发送消息function publish(event, data) {ws.send(JSON.stringify({cmd:'publish',// subject: subject, // 房间号 暂时不做这个event:event,data:data}));}ws.onopen = function (evt) {openMediaSetSend() // 广播客户端 准备连接}ws.onmessage = function(e){let package = JSON.parse(e.data)let data = package.data;switch (package.event) {case 'client-call':// 主播模块client_call();break;case 'client-answer':// 更改与连接关联的 远程描述pc.setRemoteDescription(data).then().catch(e=>{alert(e)})break;case 'client-offer':// 粉丝模块client_offer(data)break;case 'client-candidate':/*将新接收的候选服务器传递到浏览器的ICE代理服务器。值为空字符串(“”),则表示已传递所有远程候选对象(候选对象结束)在协商过程中,您的应用程序可能会收到许多候选项,您可以通过这种方式将这些候选项传递给ICE代理,从而允许它构建一个潜在连接方法的列表。*/pc.addIceCandidate(new RTCIceCandidate(data)).then().catch(e=>{alert(e)})break;}};ws.onclose = function (){alert('关闭链接')}/*流媒体通道创建
*/function icecandidate(localStream) {// 创建链接pc = new RTCPeerConnection(configuration);/**onicecandidate属性 是一个事件处理程序 当调用 RTCPeerConnection.setlocaldescription() 或将 RTCIceCandidate 添加到 RTCPeerConnection 时应将候选对象传输到 远程客户端 以便 ICE代理与远程客户端 协商 */pc.onicecandidate = event => {if (event.candidate) {publish('client-candidate', event.candidate);}}var tracks = localStream.getTracks();// 将一个媒体流 添加到一组流中,这些新流将被传输给 客户端for(const val of tracks){pc.addTrack(val, localStream);}pc.ontrack = event => {$('#remoteVideo').removeClass('hidden');$('#localVideo').remove();alert(event.streams)remoteVideo.srcObject = event.streams[0];};}// 主播 开始准备 插座 信息function client_call() {// 创建流媒体通道icecandidate(localStream);// 生成插座pc.createOffer({ // 创建一个 SDP 提供一个新的 webrtc 连接到远程节点 offerToReceiveAudio: false,offerToReceiveVideo: true}).then(function (desc) {// 更改与连接关联的本地描述 设置后 onicecandidate 协商 pc.setLocalDescription(desc).then(function () {publish('client-offer', pc.localDescription);}).catch(function (e) {alert(e);});}).catch(function (e) {alert(e);});}// 粉丝端 收到消息 开始链接function client_offer(data) {// 创建流媒体通道icecandidate(localStream);// 更改与连接关联的 远程描述pc.setRemoteDescription(data).then(function (){if (answer) {answer = 0;/*方法在WebRTC 连接的要约/答复 协商期间,为从粉丝户端接收到的要约创建一个SDP信息。答案包含关于已经附加到会话的任何媒体、浏览器支持的编解码器和选项以及已经收集到的ICE候选项的信息。答案是交付给返回的承诺,然后应该发送到报价源继续谈判过程。*/return pc.createAnswer();}return false;}).then( desc => {if(desc !== false){// 更改与连接关联的本地描述 设置后 onicecandidate 协商 createAnswerpc.setLocalDescription(desc).then(function (){publish('client-answer', pc.localDescription);})}}).catch( e => {alert(e)})}</script>
</body>
</html>
主播页面和粉丝页面不同的地方
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="description" content="php web-rtc例子,一对一聊天-基于workerman"><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"><meta itemprop="description" content="Video chat using the reference WebRTC application"><meta itemprop="name" content="AppRTC"><meta name="mobile-web-app-capable" content="yes"><meta id="theme-color" name="theme-color" content="#1e1e1e"><title>webRtc主播</title>
</head><body><div class="videos"><video id="localVideo" autoplay></video><video id="remoteVideo" autoplay class="hidden"></video>
</div><script src="jquery-3.2.1.min.js"></script>
<script src="adapter.js"></script>
<script type="text/javascript">var localVideo = document.getElementById('localVideo');var remoteVideo = document.getElementById('remoteVideo');// 流媒体对象navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// 流媒体通道配置var configuration = {iceTransportPolicy:"all",iceCandidatePoolSize:"0"};// 协商次数var answer = 1;/*流媒体获取
*/function openMediaSetSend() {navigator.mediaDevices.getUserMedia({// audio: true,video: true}).then(function (stream) {localVideo.srcObject = stream;localStream = stream;localVideo.addEventListener('loadedmetadata', function () {// 广播客户端 准备连接 publish('client-call', null)});}).catch(function (e) {alert(e);});}/*websocketOpen
*/function websocketOpen () {if ("WebSocket" in window) {alert("您的浏览器支持 WebSocket!")return false;} else {// 浏览器不支持 WebSocket123alert("您的浏览器不支持 WebSocket!");return true;}}if(websocketOpen()){console.log('链接失败')}let ws = new WebSocket("wss://www.cwtui.com/con_camera/camera");//发送消息function publish(event, data) {ws.send(JSON.stringify({cmd:'publish',// subject: subject, // 房间号 暂时不做这个event:event,data:data}));}ws.onopen = function (evt) {openMediaSetSend() // 广播客户端 准备连接}ws.onmessage = function(e){let package = JSON.parse(e.data)let data = package.data;switch (package.event) {case 'client-call':// 主播模块client_call();break;case 'client-answer':// 更改与连接关联的 远程描述pc.setRemoteDescription(data).then().catch(e=>{alert(e)})break;}};ws.onclose = function (){alert('关闭链接')}/*流媒体通道创建
*/function icecandidate(localStream) {// 创建链接pc = new RTCPeerConnection(configuration);/**onicecandidate属性 是一个事件处理程序 当调用 RTCPeerConnection.setlocaldescription() 或将 RTCIceCandidate 添加到 RTCPeerConnection 时应将候选对象传输到 远程客户端 以便 ICE代理与远程客户端 协商 */pc.onicecandidate = event => {if (event.candidate) {publish('client-candidate', event.candidate);}}var tracks = localStream.getTracks();// 将一个媒体流 添加到一组流中,这些新流将被传输给 客户端for(const val of tracks){pc.addTrack(val, localStream);}pc.ontrack = event => {$('#remoteVideo').removeClass('hidden');$('#localVideo').remove();// alert(event.streams)remoteVideo.srcObject = event.streams[0];};}// 主播 开始准备 插座 信息function client_call() {// 创建流媒体通道icecandidate(localStream);// 生成插座pc.createOffer({ // 创建一个 SDP 提供一个新的 webrtc 连接到远程节点 offerToReceiveAudio: false,offerToReceiveVideo: true}).then(function (desc) {// 更改与连接关联的本地描述 设置后 onicecandidate 协商 pc.setLocalDescription(desc).then(function () {publish('client-offer', pc.localDescription);}).catch(function (e) {alert(e);});}).catch(function (e) {alert(e);});}</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="description" content="php web-rtc例子,一对一聊天-基于workerman"><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"><meta itemprop="description" content="Video chat using the reference WebRTC application"><meta itemprop="name" content="AppRTC"><meta name="mobile-web-app-capable" content="yes"><meta id="theme-color" name="theme-color" content="#1e1e1e"><title>webRtc粉丝</title>
</head><body><div class="videos"><video id="localVideo" autoplay></video><video id="remoteVideo" autoplay class="hidden"></video>
</div><script src="jquery-3.2.1.min.js"></script>
<script src="adapter.js"></script>
<script type="text/javascript">var localVideo = document.getElementById('localVideo');var remoteVideo = document.getElementById('remoteVideo');// 流媒体对象navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// 流媒体通道配置var configuration = {iceTransportPolicy:"all",iceCandidatePoolSize:"0"};// 协商次数var answer = 1;/*websocketOpen
*/function websocketOpen () {if ("WebSocket" in window) {alert("您的浏览器支持 WebSocket!")return false;} else {// 浏览器不支持 WebSocket123alert("您的浏览器不支持 WebSocket!");return true;}}if(websocketOpen()){console.log('链接失败')}let ws = new WebSocket("wss://www.cwtui.com/con_camera/camera");//发送消息function publish(event, data) {ws.send(JSON.stringify({cmd:'publish',// subject: subject, // 房间号 暂时不做这个event:event,data:data}));}ws.onopen = function (evt) {publish('client-call', null) // 广播客户端 准备连接}ws.onmessage = function(e){let package = JSON.parse(e.data)let data = package.data;switch (package.event) {case 'client-offer':// 粉丝模块if(answer){client_offer(data)}break;case 'client-candidate':/*将新接收的候选服务器传递到浏览器的ICE代理服务器。值为空字符串(“”),则表示已传递所有远程候选对象(候选对象结束)在协商过程中,您的应用程序可能会收到许多候选项,您可以通过这种方式将这些候选项传递给ICE代理,从而允许它构建一个潜在连接方法的列表。*/pc.addIceCandidate(new RTCIceCandidate(data)).then().catch(e=>{alert(e)})break;}};ws.onclose = function (){alert('关闭链接')}/*流媒体通道创建
*/function icecandidate() {// 创建链接pc = new RTCPeerConnection(configuration);/**onicecandidate属性 是一个事件处理程序 当调用 RTCPeerConnection.setlocaldescription() 或将 RTCIceCandidate 添加到 RTCPeerConnection 时应将候选对象传输到 远程客户端 以便 ICE代理与远程客户端 协商 */pc.ontrack = event => {$('#remoteVideo').removeClass('hidden');$('#localVideo').remove();// alert(event.streams)remoteVideo.srcObject = event.streams[0];};}// 粉丝端 收到消息 开始链接function client_offer(data) {// 创建流媒体通道icecandidate();// 更改与连接关联的 远程描述pc.setRemoteDescription(data).then(function (){answer = 0;/*方法在WebRTC 连接的要约/答复 协商期间,为从粉丝户端接收到的要约创建一个SDP信息。答案包含关于已经附加到会话的任何媒体、浏览器支持的编解码器和选项以及已经收集到的ICE候选项的信息。答案是交付给返回的承诺,然后应该发送到报价源继续谈判过程。*/return pc.createAnswer();}).then( desc => {if(desc !== false){// // 更改与连接关联的 远程描述pc.setLocalDescription(desc).then(function (){publish('client-answer', pc.localDescription);})}}).catch( e => {alert(e)})}</script>
</body>
</html>
- 区别
- 主播不需要 与粉丝端建立握手机制 client-candidate
- 不需要 client-candidate 添加 ICE代理服务器连接方法的列表
- 粉丝端需要只有一次创建 流媒体通道
- 粉丝端,不需要获取媒体流
- 粉丝端,不需要与远程端协商
接下来需要配置信令服务器,打通内外网的差异
配置一下页面的信令服务器
改下上面的代码
var configuration = {iceServers:[{urls:["turn:www.cwtui.com:3478"], //地址username:"hu", // 账号credential:"123456" // 密码}],iceTransportPolicy:"all",iceCandidatePoolSize:"0"
};
重点注意下 turn 协议和 stun 这两个协议 我们配置的协议是turn
安装信令服务器与配置
coturn 下载页面
cd coturn
./configure
make
sudo make install
查看是否安装成功
使用命令:which turnserver
我的配置
listening-port=3478
tls-listening-port=5349
listening-ip=172.17.142.214 // 内网ip
relay-device=eth0
relay-ip=172.17.142.214 // 内网ip
external-ip=47.94.228.114/172.17.142.214 // 外网ip/内网ip
relay-threads=5
min-port=49152
max-port=65535
fingerprint
lt-cred-mech
user=hu:123456
realm=www.cwtui.com
cert=/etc/turn_server_cert.pem
pkey=/etc/turn_server_pkey.pem
pidfile="/var/run/turnserver.pid"
cli-password=qwerty
参考原文: https://www.pressc.cn/967.html
参考原文: https://blog.csdn.net/bvngh3247/article/details/80742396
- 启动命令
- turnserver -a -f -v -r www.cwtui.com
- turnserver -a -f --user=hu:123456 -v -r www.cwtui.com
- 启动参数解析
- -a 长期验证机制
- -o 如果指定 -o 则后台运行
- -f 使用指纹
- -v
- -r realm组别
- –user 指定账户运行
参考原文:https://www.cnblogs.com/mobilecard/p/6542294.html
希望朋友给点个赞~~~~ 谢谢大家看到这里
利用Swoft实现PHP+websocket直播,即时通讯代码相关推荐
- springboot框架下利用websocket实现即时通讯
springboot框架下利用websocket实现即时通讯(文章末尾有git项目打包文件,直接下载使用即可) 用websocket实现简单的在线聊天,先画个时序图,直观感受下流程 SystemCon ...
- SpringCloud工作笔记060---SpringBoot中使用WebSocket实现即时通讯_实现呼叫中心业务封装
JAVA技术交流QQ群:170933152 ---------------我们主要用这个来转接,呼叫中心发过来的分机电话 呼叫中心:呼叫过程,首先呼叫中心运营商给个,api手册,api会规定,首先登陆 ...
- 基于springboot+h5+websocket的即时通讯客服系统和百度实时语音转译(语音在线识别)
本文章由本人原创 下载链接:https://download.csdn.net/download/u014191624/51948075 这是一个基于springboot+h5+websocket的即 ...
- 一对一语聊直播源码视频交友系统,一对一直播即时通讯IM产品。
一对一语聊直播源码视频交友系统,一对一直播即时通讯IM产品. 匹配聊天 开启速度匹配,匹配逻辑异性匹配.原始码,回调,API和SDK等接口调用正常双端经过测试完美正常跑通! 纯原生开发 开发语言 后端 ...
- Java聊天室程序源码 Java即时通讯代码 Java局域网聊天系统 Java即时通讯 Java聊天系统
Java聊天室程序源码 Java即时通讯代码 Java局域网聊天系统 Java即时通讯 Java聊天系统 public Swingtest002() {// 设置标题setTitle("请 ...
- Websocket实现即时通讯
前言 关于我和WebSocket的缘:我从大二在计算机网络课上听老师讲过之后,第一次使用就到了毕业之后的第一份工作.直到最近换了工作,到了一家是含有IM社交聊天功能的app的时候,我觉得我现在可以谈谈 ...
- java node websocket_nodejs怎么实现webSocket接口即时通讯服务?
websocket 工作方式:广播和收听 # 使用场景:金融/聊天室/-- websocket:服务器支持广泛(node/java/php/c#--) 原生API操作复杂 可以使用第三方模块:sock ...
- 私有化部署|短视频|带直播|即时通讯|IM|聊天app|支持二开丨视频会议丨支付红包
1.私聊.群聊 2.网页导航:镶嵌所需网址: 3.短视频.视频会议.视频直播. 4.附近人.公众号 5.多消息类型:文字表情.语音.图片视频.名片.文件等 6.消息功能:已读通知.撤回.删除.复制.转 ...
- Springboot集成WebSocket实现即时通讯
在SpringBoot的pom.xml文件里添加依赖: <!-- websocket --> <dependency><groupId>org.springfram ...
最新文章
- DL之MobileNetV2:MobileNetV2算法的架构详解(包括ReLu的意义)
- Citrix Netscaler负载均衡算法
- 多层陶瓷电容器用处_【科普】片状多层陶瓷电容器的封装方法,你了解吗?
- Sublime Text for Mac 最新版安装后,无法搜索到Install Package的解决办法
- 2021汉语言文学对高考成绩查询,2021汉语言文学专业就业前景怎么样
- 【Java】为什么java构造函数的构造器只能在第一行写this() 或者super() ?
- .NET Windows编程系列笔记(一)
- IOS开发UI控件UIScrollView和Delegate的使用
- 1 Hello World,JavaFX Style
- JavaScript ECMAScript版本介绍
- acm常见错误-持续更新
- 再论:男人有多大责任和感恩代表着有多大的驾驭能力和事业能力
- 计算机word设置信纸,一分钟教你学会用Word做信纸和公章!
- free pascal语言学习笔记(一)
- (dfs)[USACO3.4]“破锣摇滚”乐队 Raucous Rockers
- js区号插件(全国电话区号)
- 一篇就明白什么是H3C?
- Linux用户态与内核态通信的几种方式(待完善)
- 《华尔街》观后笔记7——阳光交易
- Java内存相关的常用命令