什么是WebSocket?看过html5的同学都知道,WebSocket protocol 是HTML5一种新的协议。它是实现了浏览器与服务器全双工通信(full-duplex)。HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并达到实时通讯。现在我们来探讨一下html5的WebSocket

概念

HTML5作为下一代WEB标准,拥有许多引人注目的新特性,如Canvas、本地存储、多媒体编程接口、WebSocket 等等。今天我们就来看看具有“Web TCP”之称的WebSocket.

WebSocket的出现是基于Web应用的实时性需要而产生的。这种实时的Web应用大家应该不陌生,在生活中都应该用到过,比如新浪微博的评论、私信的通知,腾讯的WebQQ等。让我们来回顾下实时 Web 应用的窘境吧。

在WebSocket出现之前,一般通过两种方式来实现Web实时用:轮询机制和流技术;其中轮询有不同的轮询,还有一种叫Comet的长轮询。

轮询:这是最早的一种实现实时 Web 应用的方案。客户端以一定的时间间隔向服务端发出请求,以频繁请求的方式来保持客户端和服务器端的同步。这种同步方案的缺点是,当客户端以固定频率向服务 器发起请求的时候,服务器端的数据可能并没有更新,这样会带来很多无谓的网络传输,所以这是一种非常低效的实时方案。

长轮询:是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者 时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提 高。

流:常就是在客户端的页面使用一个隐藏的窗口向服务端发出一个长连接的请求。服务器端接到这个请求后作出回应并不断更新连接状态以保证客户端和服务 器端的连接不过期。通过这种机制可以将服务器端的信息源源不断地推向客户端。这种机制在用户体验上有一点问题,需要针对不同的浏览器设计不同的方案来改进 用户体验,同时这种机制在并发比较大的情况下,对服务器端的资源是一个极大的考验。

上述方式其实并不是真正的实时技术,只是使用了一种技巧来实现的模拟实时。在每次客户端和服务器端交互的时候都是一次 HTTP 的请求和应答的过程,而每一次的 HTTP 请求和应答都带有完整的 HTTP 头信息,这就增加了每次传输的数据量。但这些方式最痛苦的是开发人员,因为不论客户端还是服务器端的实现都很复杂,为了模拟比较真实的实时效果,开发人员 往往需要构造两个HTTP连接来模拟客户端和服务器之间的双向通讯,一个连接用来处理客户端到服务器端的数据传输,一个连接用来处理服务器端到客户端的数 据传输,这不可避免地增加了编程实现的复杂度,也增加了服务器端的负载,制约了应用系统的扩展性。

基于上述弊端,实现Web实时应用的技术出现了,WebSocket通过浏览器提供的API真正实现了具备像C/S架构下的桌面系统的实时通讯能 力。其原理是使用JavaScript调用浏览器的API发出一个WebSocket请求至服务器,经过一次握手,和服务器建立了TCP通讯,因为它本质 上是一个TCP连接,所以数据传输的稳定性强和数据传输量比较小。

WebSocket 协议

WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

下面我们来详细介绍一下 WebSocket 协议,由于这个协议目前还是处于草案阶段,版本的变化比较快,我们选择目前最新的 draft-ietf-hybi-thewebsocketprotocol-17 版本来描述 WebSocket 协议。因为这个版本目前在一些主流的浏览器上比如 Chrome,、FireFox、Opera 上都得到比较好的支持。通过描述可以看到握手协议

客户端发到服务器的内容:

 代码如下 复制代码

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13

从服务器到客户端的内容:

 代码如下 复制代码

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    Sec-WebSocket-Protocol: chat

这些请求和通常的 HTTP 请求很相似,但是其中有些内容是和 WebSocket 协议密切相关的。我们需要简单介绍一下这些请求和应答信息,”Upgrade:WebSocket”表示这是一个特殊的 HTTP 请求,请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议。其中客户端的Sec-WebSocket-Key和服务器端的Sec-WebSocket-Accept就是重要的握手认证信息了,这些内容将在服 务器端实现的博文中讲解。

相信通过上文的讲解你应该对WebSocket有了个初步认识了,如果有任何疑问欢迎交流。


客户端

如概念篇中介绍的握手协议,客户端是由浏览器提供了API,所以只要使用JavaScript来简单调用即可,而服务器端是要自己实现的,服务器端将在下个博文来讲。

 代码如下 复制代码
WebSocket JavaScript 接口定义:

[Constructor(in DOMString url, optional in DOMString protocol)]
interface WebSocket {
readonly attribute DOMString URL;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSED = 2;
readonly attribute unsigned short readyState;
readonly attribute unsigned long bufferedAmount;
 
// networking
attribute Function onopen;
attribute Function onmessage;
attribute Function onclose;
boolean send(in DOMString data);
void close();
};
WebSocket implements EventTarget;

简单了解下接口方法和属性:

readyState表示连接有四种状态:
CONNECTING (0):表示还没建立连接;
OPEN (1): 已经建立连接,可以进行通讯;
CLOSING (2):通过关闭握手,正在关闭连接;
CLOSED (3):连接已经关闭或无法打开;
url是代表 WebSocket 服务器的网络地址,协议通常是”ws”或“wss(加密通信)”,send 方法就是发送数据到服务器端;
close 方法就是关闭连接;
onopen连接建立,即握手成功触发的事件;
onmessage收到服务器消息时触发的事件;
onerror异常触发的事件;
onclose关闭连接触发的事件;

JavaScript调用浏览器接口实例如下:

 代码如下 复制代码

var wsServer = 'ws://localhost:8888/Demo'; //服务器地址
var websocket = new WebSocket(wsServer); //创建WebSocket对象
websocket.send("hello");//向服务器发送消息
alert(websocket.readyState);//查看websocket当前状态
websocket.onopen = function (evt) {
//已经建立连接
};
websocket.onclose = function (evt) {
//已经关闭连接
};
websocket.onmessage = function (evt) {
//收到服务器消息,使用evt.data提取
};
websocket.onerror = function (evt) {
//产生异常
}; 

服务器端

握手协议的客户端数据已经由浏览器代劳了,服务器端需要我们自己来实现,目前市场上开源的实现也比较多如:

Kaazing WebSocket Gateway(一个 Java 实现的 WebSocket Server);
    mod_pywebsocket(一个 Python 实现的 WebSocket Server);
    Netty(一个 Java 实现的网络框架其中包括了对 WebSocket 的支持);
    node.js(一个 Server 端的 JavaScript 框架提供了对 WebSocket 的支持);
    WebSocket4Net(一个.net的服务器端实现);

其实在目前的.net4.5框架中已经实现了WebSocket,不用官方实现,我们自己来写个简单的。服务器端需要根据协议来握手、接收和发送。

握手

首先我们再来回顾下握手协议:

客户端发到服务器的内容:

 代码如下 复制代码
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

从服务器到客户端的内容:

 代码如下 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

关键是服务器端Sec-WebSocket-Accept,它是根据Sec-WebSocket-Key计算出来的:

取出Sec-WebSocket-Key,与一个magic string “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 连接成一个新的key串;
将新的key串SHA1编码,生成一个由多组两位16进制数构成的加密串;
把加密串进行base64编码生成最终的key,这个key就是Sec-WebSocket-Key;

实例代码如下:

 代码如下 复制代码

/// <summary>
/// 生成Sec-WebSocket-Accept
/// </summary>
/// <param name="handShakeText">客户端握手信息</param>
/// <returns>Sec-WebSocket-Accept</returns>
private static string GetSecKeyAccetp(byte[] handShakeBytes,int bytesLength)
{
string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, bytesLength);
string key = string.Empty;
Regex r = new Regex(@"Sec-WebSocket-Key:(.*?)rn");
Match m = r.Match(handShakeText);
if (m.Groups.Count != 0)
{
key = Regex.Replace(m.Value, @"Sec-WebSocket-Key:(.*?)rn", "$1").Trim();
}
byte[] encryptionString = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
return Convert.ToBase64String(encryptionString);
}

如果握手成功,将会触发客户端的onopen事件。

解析接收的客户端信息

接收到客户端数据解析规则如下:

1byte
bit: frame-fin,x0表示该message后续还有frame;x1表示是message的最后一个frame
3bit: 分别是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
4bit: frame-opcode,x0表示是延续frame;x1表示文本frame;x2表示二进制frame;x3-7保留给非控制frame;x8表示关 闭连接;x9表示ping;xA表示pong;xB-F保留给控制frame
2byte
1bit: Mask,1表示该frame包含掩码;0,表示无掩码
7bit、7bit+2byte、7bit+8byte: 7bit取整数值,若在0-125之间,则是负载数据长度;若是126表示,后两个byte取无符号16位整数值,是负载长度;127表示后8个 byte,取64位无符号整数值,是负载长度
3-6byte: 这里假定负载长度在0-125之间,并且Mask为1,则这4个byte是掩码
7-end byte: 长度是上面取出的负载长度,包括扩展数据和应用数据两部分,通常没有扩展数据;若Mask为1,则此数据需要解码,解码规则为1-4byte掩码循环和数据byte做异或操作。

解析代码如下,但没有处理多帧和不包含掩码的包:

 代码如下 复制代码

/// <summary>
/// 解析客户端数据包
/// </summary>
/// <param name="recBytes">服务器接收的数据包</param>
/// <param name="recByteLength">有效数据长度</param>
/// <returns></returns>
private static string AnalyticData(byte[] recBytes, int recByteLength)
{
if (recByteLength < 2) { return string.Empty; }
 
bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最后一帧
if (!fin){
return string.Empty;// 超过一帧暂不处理
}
 
bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩码
if (!mask_flag){
return string.Empty;// 不包含掩码的暂不处理
}
 
int payload_len = recBytes[1] & 0x7F; // 数据长度
 
byte[] masks = new byte[4];
byte[] payload_data;
 
if (payload_len == 126){
Array.Copy(recBytes, 4, masks, 0, 4);
payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 8, payload_data, 0, payload_len);
 
}else if (payload_len == 127){
Array.Copy(recBytes, 10, masks, 0, 4);
byte[] uInt64Bytes = new byte[8];
for (int i = 0; i < 8; i++){
uInt64Bytes[i] = recBytes[9 - i];
}
UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);
 
payload_data = new byte[len];
for (UInt64 i = 0; i < len; i++){
payload_data[i] = recBytes[i + 14];
}
}else{
Array.Copy(recBytes, 2, masks, 0, 4);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 6, payload_data, 0, payload_len);
 
}
 
for (var i = 0; i < payload_len; i++){
payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);
}
return Encoding.UTF8.GetString(payload_data);
}

发送数据至客户端

服务器发送的数据以0x81开头,紧接发送内容的长度(若长度在0-125,则1个byte表示长度;若长度不超过0xFFFF,则后2个byte 作为无符号16位整数表示长度;若超过0xFFFF,则后8个byte作为无符号64位整数表示长度),最后是内容的byte数组。

代码如下:

 代码如下 复制代码

/// <summary>
/// 打包服务器数据
/// </summary>
/// <param name="message">数据</param>
/// <returns>数据包</returns>
private static byte[] PackData(string message)
{
byte[] contentBytes = null;
byte[] temp = Encoding.UTF8.GetBytes(message);
 
if (temp.Length < 126){
contentBytes = new byte[temp.Length + 2];
contentBytes[0] = 0x81;
contentBytes[1] = (byte)temp.Length;
Array.Copy(temp, 0, contentBytes, 2, temp.Length);
}else if (temp.Length < 0xFFFF){
contentBytes = new byte[temp.Length + 4];
contentBytes[0] = 0x81;
contentBytes[1] = 126;
contentBytes[2] = (byte)(temp.Length & 0xFF);
contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF);
Array.Copy(temp, 0, contentBytes, 4, temp.Length);
}else{
// 暂不处理超长内容
}
 
return contentBytes;
}

这里只是简单介绍,下节来做个完整的实例。 

说是完整的实例,其实并不完整,这里需要说明,这个实例并没有实现并发,也没考虑到算法和资源管理,所谓的完整是有客户端和服务器端,并且能跑起来演示。直接上菜,关于理论请看前面三篇博文,TCP请另看相关知识。

客户端代码:

 代码如下 复制代码

<html>
<head>
<meta charset="UTF-8">
<title>Web sockets test</title>
<script src="jquery-min.js" type="text/javascript"></script>
<script type="text/javascript">
var ws;
function ToggleConnectionClicked() {
try {
ws = new WebSocket("ws://10.9.146.31:1818/chat");//连接服务器
ws.onopen = function(event){alert("已经与服务器建立了连接rn当前连接状态:"+this.readyState);};
ws.onmessage = function(event){alert("接收到服务器发送的数据:rn"+event.data);};
ws.onclose = function(event){alert("已经与服务器断开连接rn当前连接状态:"+this.readyState);};
ws.onerror = function(event){alert("WebSocket异常!");};
} catch (ex) {
alert(ex.message);
}
};
 
function SendData() {
try{
ws.send("beston");
}catch(ex){
alert(ex.message);
}
};
 
function seestate(){
alert(ws.readyState);
}
</script>
</head>
<body>
<button id='ToggleConnection' type="button" οnclick='ToggleConnectionClicked();'>连接服务器</button><br /><br />
<button id='ToggleConnection' type="button" οnclick='SendData();'>发送我的名字:beston</button><br /><br />
<button id='ToggleConnection' type="button" οnclick='seestate();'>查看状态</button><br /><br />
</body>
</html>

服务器端代码:

 代码如下 复制代码

using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
 
namespace WebSocket
{
class Program
{
static void Main(string[] args)
{
int port = 1818;
byte[] buffer = new byte[1024];
 
IPEndPoint localEP = new IPEndPoint(IPAddress.Any, port);
Socket listener = new Socket(localEP.Address.AddressFamily,SocketType.Stream, ProtocolType.Tcp);
try{
listener.Bind(localEP);
listener.Listen(10);
 
Console.WriteLine("等待客户端连接....");
Socket sc = listener.Accept();//接受一个连接
Console.WriteLine("接受到了客户端:"+sc.RemoteEndPoint.ToString()+"连接....");
//握手
int length = sc.Receive(buffer);//接受客户端握手信息
sc.Send(PackHandShakeData(GetSecKeyAccetp(buffer,length)));
Console.WriteLine("已经发送握手协议了....");
//接受客户端数据
Console.WriteLine("等待客户端数据....");
length = sc.Receive(buffer);//接受客户端信息
string clientMsg=AnalyticData(buffer, length);
Console.WriteLine("接受到客户端数据:" + clientMsg);
 
//发送数据
string sendMsg = "您好," + clientMsg;
Console.WriteLine("发送数据:“"+sendMsg+"” 至客户端....");
sc.Send(PackData(sendMsg));
Console.WriteLine("演示Over!");
 
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
 
/// <summary>
/// 打包握手信息
/// </summary>
/// <param name="secKeyAccept">Sec-WebSocket-Accept</param>
/// <returns>数据包</returns>
private static byte[] PackHandShakeData(string secKeyAccept)
{
var responseBuilder = new StringBuilder();
responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + Environment.NewLine);
responseBuilder.Append("Upgrade: websocket" + Environment.NewLine);
responseBuilder.Append("Connection: Upgrade" + Environment.NewLine);
responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine + Environment.NewLine);
//如果把上一行换成下面两行,才是thewebsocketprotocol-17协议,但居然握手不成功,目前仍没弄明白!
//responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine);
//responseBuilder.Append("Sec-WebSocket-Protocol: chat" + Environment.NewLine);
 
return Encoding.UTF8.GetBytes(responseBuilder.ToString());
}
 
/// <summary>
/// 生成Sec-WebSocket-Accept
/// </summary>
/// <param name="handShakeText">客户端握手信息</param>
/// <returns>Sec-WebSocket-Accept</returns>
private static string GetSecKeyAccetp(byte[] handShakeBytes,int bytesLength)
{
string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, bytesLength);
string key = string.Empty;
Regex r = new Regex(@"Sec-WebSocket-Key:(.*?)rn");
Match m = r.Match(handShakeText);
if (m.Groups.Count != 0)
{
key = Regex.Replace(m.Value, @"Sec-WebSocket-Key:(.*?)rn", "$1").Trim();
}
byte[] encryptionString = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
return Convert.ToBase64String(encryptionString);
}
 
/// <summary>
/// 解析客户端数据包
/// </summary>
/// <param name="recBytes">服务器接收的数据包</param>
/// <param name="recByteLength">有效数据长度</param>
/// <returns></returns>
private static string AnalyticData(byte[] recBytes, int recByteLength)
{
if (recByteLength < 2) { return string.Empty; }
 
bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最后一帧
if (!fin){
return string.Empty;// 超过一帧暂不处理
}
 
bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩码
if (!mask_flag){
return string.Empty;// 不包含掩码的暂不处理
}
 
int payload_len = recBytes[1] & 0x7F; // 数据长度
 
byte[] masks = new byte[4];
byte[] payload_data;
 
if (payload_len == 126){
Array.Copy(recBytes, 4, masks, 0, 4);
payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 8, payload_data, 0, payload_len);
 
}else if (payload_len == 127){
Array.Copy(recBytes, 10, masks, 0, 4);
byte[] uInt64Bytes = new byte[8];
for (int i = 0; i < 8; i++){
uInt64Bytes[i] = recBytes[9 - i];
}
UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);
 
payload_data = new byte[len];
for (UInt64 i = 0; i < len; i++){
payload_data[i] = recBytes[i + 14];
}
}else{
Array.Copy(recBytes, 2, masks, 0, 4);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 6, payload_data, 0, payload_len);
 
}
 
for (var i = 0; i < payload_len; i++){
payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);
}
return Encoding.UTF8.GetString(payload_data);
}
 
 
/// <summary>
/// 打包服务器数据
/// </summary>
/// <param name="message">数据</param>
/// <returns>数据包</returns>
private static byte[] PackData(string message)
{
byte[] contentBytes = null;
byte[] temp = Encoding.UTF8.GetBytes(message);
 
if (temp.Length < 126){
contentBytes = new byte[temp.Length + 2];
contentBytes[0] = 0x81;
contentBytes[1] = (byte)temp.Length;
Array.Copy(temp, 0, contentBytes, 2, temp.Length);
}else if (temp.Length < 0xFFFF){
contentBytes = new byte[temp.Length + 4];
contentBytes[0] = 0x81;
contentBytes[1] = 126;
contentBytes[2] = (byte)(temp.Length & 0xFF);
contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF);
Array.Copy(temp, 0, contentBytes, 4, temp.Length);
}else{
// 暂不处理超长内容
}
 
return contentBytes;
}
}
}

运行效果:

使用的浏览器:

疑问:如实例中

 代码如下 复制代码
responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine + Environment.NewLine);
//如果把上一行换成下面两行,才是thewebsocketprotocol-17协议,但居然握手不成功,目前仍没弄明白!
//responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine);
//responseBuilder.Append("Sec-WebSocket-Protocol: chat" + Environment.NewLine);

这是为什么呢?看到这篇博文的兄弟希望能够给我解惑!

连接键盘 功能

什么是”连接键盘“功能

”连接键盘“功能其实就是开通了网页版的微信,利用键盘可以快速录入文字聊天。

连接方法很简单,用手机打开微信点击右上角魔术棒,会弹出三个选项,只用选择连接键盘就可以了,这时用浏览器打开wx.qq.com然后用手机扫描网页中的二维码即可打开网页版微信,而这时也就可以直接利用电脑键盘实现快速聊天了。

WebSocket-Server里项目含义如下:

Mobile:手机模拟器,与手机通讯服务器进行UDP通讯,负责提示打开的页面地址,并输入GUID(相当于二维码)与页面进行绑定;
MobileServer:手机通讯服务器,负责接收手机信息(比如微信的账户信息以及二维码信息),此处接收GUID。并转发至WebSocket通讯服务器;
WebSocket:WebSocket通讯服务器,与手机通讯服务器和页面的WebSocket进行通讯;

WebSocket-Client里项目含义如下:

test.html:测试的web页面(类似微信的wx.qq.com);
jquery-1.8.0.min.js:jquery框架;

实现原理

页面test.html生成GUID并存储在WebSocket,手机模拟器输入GUID并传至WebSocket服务器,在WebSocket服务器检索页面Socket信息并通讯。

注意事项

如果自己测试请根据上述步骤先启动手机通讯服务器和WebSocket通讯服务器;
把所有是“10.9.146.31”的字符串更换为自己的IP;

转载于:https://www.cnblogs.com/zxtceq/p/6860000.html

WebSocket使用教程 - 带完整实例--网址:https://my.oschina.net/u/1266171/blog/357488相关推荐

  1. JsTree 最详细教程及完整实例

    JsTree 最详细教程及完整实例 JsTree是一个jquery的插件,它提交一个非常友好并且强大的交互性的树,并且是完全免费或开源的(MIT 许可).Jstree技持Html 或 json格式的的 ...

  2. [PHP实战]ThinkPHP入门-1[windows]入门教程(带完整图文) + WampServer环境配置

    ThinkPHP入门 0.介绍 0.1 ThinkPHP介绍 0.2 WampServer介绍 0.3 完整案例 1.环境 2.安装步骤 2.1 安装wampserver 2.2 安装好Wamp后访问 ...

  3. Navicat使用教程:所有实例的运行状况和性能概述

    2019独角兽企业重金招聘Python工程师标准>>> 下载Navicat Monitor最新版本 Navicat Monitor 是一套安全.简单而且无代理的远程服务器监控工具.它 ...

  4. Spring WebSocket初探2 (Spring WebSocket入门教程)

    2019独角兽企业重金招聘Python工程师标准>>> WebSocket前端准备 SockJS: SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 W ...

  5. 【打卡定制版】11月最新H5定制版早起打卡支付已接带完整搭建教程

    csdn下载地址: https://download.csdn.net/download/dujiangdu123/13104895 蓝奏云下载地址: https://youyacao.lanzous ...

  6. Yii2搭建后台并实现rbac权限控制完整实例教程

    分享到 一键分享 QQ空间 新浪微博 百度云收藏 人人网 腾讯微博 百度相册 开心网 腾讯朋友 百度贴吧 豆瓣网 搜狐微博 百度新首页 QQ好友 和讯微博 更多... 百度分享 photoshop教程 ...

  7. 基于python的图书管理系统测试步骤_Django admin实现图书管理系统菜鸟级教程完整实例...

    Django 有着强大而又及其易用的admin后台,在这里,你可以轻松实现复杂代码实现的功能,如搜索,筛选,分页,题目可编辑,多选框. 简单到,一行代码就可以实现一个功能,而且模块之间耦合得相当完美. ...

  8. 全网最全面工作流引擎Flowable完整教程之多实例会签

    Flowable完整教程之多实例会签 前言 1.BladeX流程设计器 1.1.BladeX工作流设计 1.2.parallel多实例流程设计 1.3. BladeX多实例任务节点参数设置 2.部署测 ...

  9. 最新H5定制版早起打卡支付已接带完整搭建教程

    介绍 单独定制的版本,并未流通过,UI很漂亮,专业的早起打开平台,和之前的那个打卡套理财的, 不是一个版本,运营逻辑也完全不同.支付已经接好派特了,简单替换下秘钥就可以了. 带完整搭建教程,萌新都可以 ...

  10. 响应式自助建站系统源码 完全开源带完整教程

    分享一个响应式的自助建站系统源码,拥有700多套网站模板,傻瓜式一键自助建站,功能强大,还带完整的教程. 春哥团队帮企建站宝源码系统,网站全部采用最新的Html5技术开发,真正做到响应式,全网合一!而 ...

最新文章

  1. 一个基于Spring Boot的API、RESTful API项目骨架
  2. MySQL数据库内连接查询inner join...on
  3. oracle服务器双机,双机热备Oracle数据库服务器操作实战
  4. php获取网站根目录
  5. 【CKFinder】解决上传中文名文件乱码和文件重命名的问题
  6. 计算机会计的专业知识竞赛,会计知识大赛主持词范文
  7. 开源数据库在平安的应用实践
  8. java编译运行_如何编译运行一个简单的java程序
  9. Java实现简易的文本编辑器
  10. 小米手机文件 ftp服务器,小米手机与电脑FTP连接(不用每一次都输入ftp地址)...
  11. Ubuntu 9.04 解决没有声音的问题 (Realtek声卡)
  12. oracle 简版客户端instantclient使用 oledb ODAC组件使用
  13. 如何申请小程序账号及上线一个体验版小程序
  14. mysql数据库的单引号用法_数据库SQL语句单引号、双引号的用法
  15. Redis 到底是怎么实现“附近的人”这个功能的呢?
  16. 如果你相中上了一个程序员小伙
  17. 中国大学MOOC-陈越、何钦铭-数据结构-2020春期末考试【个人完整题解记录-判断选择部分】
  18. tflearn的VocabularyProcessor用法:建立中文词汇表和把文本转为词ID序列
  19. 如何恢复录音删除的录音文件_手机通话录音后!点击这个按钮,就能将录音文件一键转为文字...
  20. vscode中添加好看字体的方法

热门文章

  1. SSH框架 openSessionInView的配置
  2. HTTP缓存原理及相关知识(1)
  3. CSS3 实现图片上浮动画
  4. 设置MyEclipse编码、补全快捷键、字体大小
  5. 【老孙随笔】求职,不要无的放矢
  6. 为你的软件选择正确的许可证方案
  7. Docker镜像的创建、存出、载入
  8. building for iOS simulator, but linking in object file built for tvOS, for architecture x86_64
  9. 你见过哪些操蛋的代码?切勿模仿! 否则后果自负
  10. 怎么用追 MM 来理解 23 种设计模式?