源码分析

作者: 医学科学  发布:2019-10-01

1.前言

前一段时间想利用fiddlercore截取本地HTTPS的流量做一些分析,按照样例代码的注释学习了一下,没搞清楚怎么实现,后来又在网上查了些资料,对HTTPS的处理提及很少,都没有解决我的问题,主要是HTTPS证书的问题,索性自己研究了一下,终于解决了问题。我会在下篇文章中分享下我的思路,本篇文章先简单分析下fiddlercore自带样例的代码,帮助刚接触fiddlercore的人快速入门,如果有说的不对的地方,欢迎批评指正。

WebSocket

WebSocket是HTML5新增的协议,目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。

为什么传统的HTTP协议不能做到WebSocket实现的功能?这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。

这样一来,要在浏览器中搞一个实时聊天就没法实现了,只能借助Flash这些插件。

HTTP协议其实有2种可以实现的方式:

  • 轮询是指浏览器通过JavaScript启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新消息。这个机制的缺点一是实时性不够,二是频繁的请求会给服务器带来极大的压力。

  • Comet本质上也是轮询,但是在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。这个机制暂时地解决了实时性问题,但是它带来了新的问题:以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。另外,一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发一些ping数据表示连接“正常工作”

以上两种机制都治标不治本,HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方

2.源码分析

首先从官网下载FiddlerCoreAPI

下载下来是一个安装文件,解压后有demo和FiddlerCoreAPI库,打开样例代码工程,开始分析。

从主函数开始。

List<Fiddler.Session> oAllSessions = new List<Fiddler.Session>();

定义了一个fiddler的Session类的List,里面存放的是客户端和服务端的消息。

Fiddler.FiddlerApplication.SetAppDisplayName("FiddlerCoreDemoApp");

命名自己的应用程序。

Fiddler.FiddlerApplication.OnNotification += delegate(object sender, NotificationEventArgs oNEA) { Console.WriteLine("** NotifyUser: " + oNEA.NotifyString); };
Fiddler.FiddlerApplication.Log.OnLogString += delegate(object sender, LogEventArgs oLEA) { Console.WriteLine("** LogString: " + oLEA.LogString); };

这两句,第一句绑定了用户通知事件的函数,具体什么时候触发没仔细研究,第二句绑定了FiddlerApplication.Log触发的事件(FiddlerCore自己的日志系统),后面打印内容都会用到,总之这两句就是把内容打印在控制台上。

Fiddler.FiddlerApplication.BeforeRequest += delegate(Fiddler.Session oS)
{
    // Console.WriteLine("Before request for:t" + oS.fullUrl);
    oS.bBufferResponse = false;
    Monitor.Enter(oAllSessions);
    oAllSessions.Add(oS);
    Monitor.Exit(oAllSessions);

    if ((oS.oRequest.pipeClient.LocalPort == iSecureEndpointPort) && (oS.hostname == sSecureEndpointHostname))
    {
        oS.utilCreateResponseAndBypassServer();
        oS.oResponse.headers.SetStatus(200, "Ok");
        oS.oResponse["Content-Type"] = "text/html; charset=UTF-8";
        oS.oResponse["Cache-Control"] = "private, max-age=0";
        oS.utilSetResponseBody("<html><body>Request for httpS://" + sSecureEndpointHostname + ":" + iSecureEndpointPort.ToString() + " received. Your request was:<br /><plaintext>" + oS.oRequest.headers.ToString());
    }
};

BeforeRequest,顾名思义,就是在客户端发送请求后拦截之,在此函数中可以获取甚至修改请求的内容。

oS.fullUrl为请求的URL。

bBufferResponse这个属性,要设置成true才可以修改服务器响应的内容。

接下来的if判断,是一个例子,如果你访问

Fiddler.FiddlerApplication.Startup(iPort, oFCSF);

开启FiddlerCore在指定端口的监听。

oSecureEndpoint = FiddlerApplication.CreateProxyEndpoint(iSecureEndpointPort, true, sSecureEndpointHostname);

建立在指定端口的HTTPS的监听,这个函数下篇文章中还会说到。

至此,这个样例的主要代码就分析完了,它实现了简单的http请求截获和响应替换,剩余的代码都是些与用户的交互,这里不再赘述。

WebSocket协议

WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接

首先,WebSocket连接必须由浏览器发起,由于请求协议是一个标准的HTTP请求,格式如下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

该请求和普通的HTTP请求有几点不同:

  • GET请求的地址不是类似/path/,而是以ws://开头的地址;
  • 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接;
  • Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
  • Sec-WebSocket-Version指定了WebSocket的协议版本。

随后,服务器如果接受该请求,就会返回如下响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。

版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。

现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。

为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。WebSocket连接建立以后,其实只是简单规定了一下:接下来,咱们通信就不使用HTTP协议了,直接互相发数据吧。

安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。

3.注意

前面代码执行完后,一定要调用Shutdown()函数关闭FiddlerCore应用,不然会导致浏览器还是通过Fiddler代理,上不了网。

Fiddler.FiddlerApplication.Shutdown();

在证书管理器中可以看到FiddlerCore安装了自己的证书。

图片 1

阅读样例代码的注释很有帮助,还有FiddlerCore的帮助文档,里面各个函数的功能说的很详细。

浏览器支持

浏览器得支持这个协议,这样才能发出ws://xxx的请求。

  • Chrome
  • Firefox
  • IE >= 10
  • Sarafi >= 6
  • Android >= 4.4
  • iOS >= 8

服务器支持

由于WebSocket是一个协议,服务器具体怎么实现,取决于所用编程语言和框架本身。Node.js本身支持的协议包括TCP协议和HTTP协议,要支持WebSocket协议,需要对Node.js提供的HTTPServer做额外的开发。已经有若干基于Node.js的稳定可靠的WebSocket实现,我们直接用npm安装使用即可。

使用WebSocket

使用最广泛的WebSocket模块是ws。

添加依赖项:

"dependencies": {
    "ws": "3.0.0"
}

服务器端

下面的代码创建了一个WebSocket服务器实例,在端口8900监听,并响应来自浏览器的connection和message事件。

const WebSocket = require('ws');

// 引用Server类
const WebSocketServer = WebSocket.Server;

// 实例化
const wss = new WebSocketServer({
    port: 8900
});

wss.on('connection', (ws) => {
    console.log(`[Server] connection()`);

    ws.on('message', (message) => {
        console.log(`[Server] Received: ${message}`);

        ws.send(`ECHO: ${message}`, (err) => {
            if (err) {
                console.log(`[Server] error: ${err}`);
            }
        });

    });

});

console.log('ws server started at port 8900');

如果有WebSocket请求接入,wss对象可以响应connection事件来处理这个WebSocket

connection事件中,回调函数会传入一个WebSocket的实例,表示这个WebSocket连接。对于每个WebSocket连接,我们都要对它绑定某些事件方法来处理不同的事件。这里,我们通过响应message事件,在收到消息后再返回一个ECHO: xxx的消息给客户端。

浏览器端

浏览器端如何创建WebSocket并且给服务器发消息? 可以在浏览器端的控制台中写JS代码。

// 打开一个WebSocket
var ws = new WebSocket('ws://localhost:8900/test');

// 响应onmessage事件
ws.onmessage = function (message) {
    console.log(message);
};

// 给服务器发送一个字符串,可以多次send
ws.send('Hello');

当前使用的浏览器是Chrome 59,不支持写法:ws.on('message', (message) => { ... }

执行流程:

  • 首先,浏览器输入:http://localhost:8900/。
  • 打开控制台,将上面的代码一行一行的放到控制台中执行,最后可以看到执行结果:
MessageEvent {isTrusted: true, data: "ECHO: Hello", origin: "ws://localhost:8900", lastEventId: "", source: null…}

查看Network选项卡,可以看到一个ws://localhost:8900/test的网络请求。查看请求的request和response。

图片 2

WebSocket请求头.png

图片 3

WebSocket响应头.png

在浏览器中输入JavaScript代码比较麻烦,我们还可以直接用ws模块提供的WebSocket来充当客户端。换句话说,ws模块既包含了服务器端,又包含了客户端。

ws的WebSocket就表示客户端,它其实就是WebSocketServer响应connection事件时回调函数传入的变量ws的类型。

在Node环境下,ws模块的客户端可以用于测试服务器端代码,否则,每次都必须在浏览器执行JavaScript代码。

服务器端代码server.js:

const WebSocket = require('ws');

const WebSocketServer = WebSocket.Server;

// 实例化
const wss = new WebSocketServer({
    port: 8900
});

wss.on('connection', (ws) => {
    console.log(`[Server] connection()`);

    ws.on('message', (message) => {
        console.log(`[Server] Received: ${message}`);

        // 加点延迟,一收一发,由于客户端最终会主动关闭(总有一方要关闭,否则死循环了),所有最后一次服务器端的发送消息会发送失败。
        setTimeout(() => {
           // 可以对ws.readyState进行判断
            ws.send(`Hello, what is your name?`, (err) => {
                if (err) {
                    console.log(`[Server] error: ${err}`);
                }
            });
        }, 1000);

    });

});

console.log('ws server started at port 8900');

客户端代码client.js:

const WebSocket = require('ws');

let ws = new WebSocket('ws://localhost:8900/ws/chat');

// 设置有限的消息发送次数,然后客户端主动关闭(避免死循环),因此会收不到最后一次服务器端的回应消息。
let count = 0;

// 当连接建立后,执行的回调函数!
// 必须在此回调中执行首次发送消息(总得有一方先发起连接)!否则可能抛异常说连接“not opened”!
ws.on('open', () => {
    console.log(`[Client] open()`);
    console.log(`在on("open")回调方法内部,ws.readyState === WebSocket.OPEN: ${ws.readyState === WebSocket.OPEN}`);
    ws.send('Hello, ws server!');
});

ws.on('message', (message) => {
    console.log(`[Client] Received: ${message}`);

    count++;
    if (count > 3) {
        ws.send('GoodBye!');
        ws.close();
    } else {
        setTimeout(() => {
            ws.send(`Hello, I am Tim - ${count}!`);
        }, 1000);
    }
});


// 此时还没有建立连接,是CONNECTING不是OPEN!
console.log(`在on("open")回调方法外面,ws.readyState === WebSocket.CONNECTING: ${ws.readyState === WebSocket.CONNECTING}`);
// 不能在此处执行首次发送消息!因此此时连接还未建立好!
// ws.send('Hello, ws server!');

依次启动server.js和node.js,执行结果:

服务器端打印:

ws server started at port 8900
[Server] connection()
[Server] Received: Hello, ws server!
[Server] Received: Hello, I am Tim - 1!
[Server] Received: Hello, I am Tim - 2!
[Server] Received: Hello, I am Tim - 3!
[Server] Received: GoodBye!
[Server] error: Error: not opened

客户端打印:

在on("open")回调方法外面,ws.readyState === WebSocket.CONNECTING: true
[Client] open()
在on("open")回调方法内部,ws.readyState === WebSocket.OPEN: true
[Client] Received: Hello, what is your name?
[Client] Received: Hello, what is your name?
[Client] Received: Hello, what is your name?
[Client] Received: Hello, what is your name?

特别注意:必须在ws连接确立后才能发送数据,即在ws.on('open', () => { ... })的回调函数中执行首次发送消息

本文由金沙澳门官网发布于医学科学,转载请注明出处:源码分析

关键词: