前言
服务器和客户端保持长连接通信,实现方式比较多。有很多成熟的框架可以完成,底层无非都是对Socket流的封装和使用。近来在新项目中使用了WebSocket来实现,实际在产品环境中使用过一段时间后,发现服务器和客户端(Android)有出现过连接断开的情况,但是都通过我们的重连机制重连了,很稳定。在此本来想写点对WebSocket的总结,却又引出了TCP、Http等一系列的网络协议,索性就先对它们的原理和设计都回顾总结一遍。
背景
在Linux系统中有这样一个概念:一切都是文件。所有系统配置都需要在开机过程中从配置文件中读取,如果希望永久修改一个系统属性,最终的办法就是修改相应的配置文件。
将文件这个概念适用在网络通信中,我们的本地缓存区的数据通过网络通信协议发送到网络远端的缓冲区,交由远端服务器处理,然后将处理结果由远端缓冲区发送到本地缓存区。这里的缓存区,实际上就相当于一个文件。只不过一个是本地文件,一个是远端文件。
而网络通信协议,通常我们会使用TCP/IP,当然,我们并不直接使用它们,而是通过Socket来进行操作。注意,Socket并不是一种协议,更多的是为了方便开发者提供友好的编程方式。Socket通过门面模式实现了对TCP/IP的封装,它实际对上层提供了一系列的接口,以方便我们在自己的应用中操作,而不必理会复杂的TCP/IP处理。这一网络层次属于数据传输层。
再往上层,就是应用层。应用层典型的协议就是Http,它依托TCP/IP协议,实现了从request到response经典的C/S模式,是一种询问、应答模式,客户端主动询问,服务器被动应答。这样,从request到完成response,一个通信过程就结束了,生命周期比较短。而在实际需求中,有时服务器和客户端需要保持在任意时刻的实时同步通信。为了弥补Http的不足,有些方案比如polling,但是会浪费更多的资源。
这样,通常都是会自己封装Socket。Socket实际上是可以进行全双工通信的,底层使用TCP搞事情。而WebSocket。它是Html5中的协议,属于应用层协议,底层同样使用TCP/IP,但是实现了高效地长连接。
下面,我们就来讨论一下这些概念。
TCP
众所周知TCP提供了面向连接的可靠的连接方式。它将通过三次握手建立连接,然后通过四次挥手断开连接。还有校验机制(报文数据的可靠)、滑动窗口机制(流控,防止无效的丢包)、排序机制(数据整体的稳定)等等这些机制,都将有力保证网络通信的可靠性。这个可靠性不是说100%不会丢包,而是依赖以上完善的保证机制。
首先,让我们来看看TCP的数据格式。
数据格式,是上层协议的基础。
可以看到,标志这这段报文用于何种用途的内容包括:
URG = 1时,紧急指针字段有效,告诉系统此报文段有紧急数据,应尽快传送,忽略排队顺序。
ACK = 1时,确认号字段有效。
PSH = 1时,系统立即创建报文段并发送,接收端接收后立即交付应用进程,而忽略缓存是否装满。
RST = 1时,表示TCP连接出现严重错误,必须释放连接,并重新建立连接。
SYN = 1,ACK = 0时,表示连接请求; SYN = 1,ACK = 1时,表示确认对方的连接请求。
FIN = 1时,表示数据发送完毕,可以释放连接。
其他内容,例如源端口号、目的端口号用于标志由上层哪个应用来发送、接收数据,而序号和确认号主要用于连接建立和拆除连接。过程如下:
Http
该协议属于应用层协议,它是站在比传输层高一层的协议上,属于基于TCP的面向事务的协议。下图能更好的说明它们之间的关系。
由上图可见,体现了浓厚的分层思想。每一层专注于自己的职责(单一原则),处理自己的逻辑,然后交由上一层处理,层次之间通过约定好的接口进行协作。
实际上,正如我们网购的过程。一般比较贵重的物品,商家为了防止物品在运输途中损坏,会进程层层包装,然后贴上我们的地址、联系电话,交给快递员传输物品。快递系统按照地址和联系方式找到我们,然后将物品交到我们手上,我们拆开层层包装,最终取到了自己的货物。
同理,Http实际上就是对数据的第一层封装,加上了Http头部信息,用于告知对方一些信息:协议版本、支持语言、数据有效期、是否缓存数据等等,实际上更像是对用于信息的配置信息。
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)
Gecko/20050225 Firefox/1.0.1
Connection: Keep-Alive
通常,TCP首部目的端口为80端口的数据为浏览器接收。它拿到数据之后,解析Http头部信息,然后使用了里面的用户数据。
Http报文有两种形式:请求报文和响应报文。客户单请求数据,服务器端响应数据,这样,一件事务就完成了,TCP也随之结束。
WebSocket
WebSocket于Http一样,属于应用层协议。事实上,它将借助Http实现底层Socket的连接。在连接的时候,实现协议升级,有Http协议升级到WebSocket协议。你可能很好奇这个过程,我们借助于OkHttp中的WebSocket实现来看看这个具体步骤:
public void connect(OkHttpClient client) {
client = client.newBuilder()
.protocols(ONLY_HTTP1)
.build();
final int pingIntervalMillis = client.pingIntervalMillis();
//通过对Http头部属性的设置,与服务器对接,从Http协议升级到ws协议
final Request request = originalRequest.newBuilder()
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.build();
call = Internal.instance.newWebSocketCall(client, request);
call.enqueue(new Callback() {
@Override public void onResponse(Call call, Response response) {
try {
checkResponse(response);
} catch (ProtocolException e) {
failWebSocket(e, response);
closeQuietly(response);
return;
}
//继续使用在Http连接中建立的Socket流,将其封装成ws流
// Promote the HTTP streams into web socket streams.
StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
streamAllocation.noNewStreams(); // Prevent connection pooling!
Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);
// Process all web socket messages.
try {
listener.onOpen(RealWebSocket.this, response);
String name = "OkHttp WebSocket " + request.url().redact();
//周期性发送PING(控制帧)消息,用于心跳检测,如果心跳发送失败,则执行onFailure回调
//同时对向服务器设置发送消息功能初始化
initReaderAndWriter(name, pingIntervalMillis, streams);
streamAllocation.connection().socket().setSoTimeout(0);
//读取来自服务器的消息,主要包括两种,PONG消息和message
//PONG消息是一种控制帧,主要用于对客户端发送PING消息的回应;
//而message实际上就是服务器要发送给客户端的消息实体了
loopReader();
} catch (Exception e) {
failWebSocket(e, null);
}
}
@Override public void onFailure(Call call, IOException e) {
failWebSocket(e, null);
}
});
}
我们可以看到:
- websocket首先借助http协议(通过在http头部设置属性,请求和服务器进行协议升级,升级协议为websocket的应用层协议,当然,前提是服务器支持这种协议升级),建立好和服务器之间的数据流,数据流之间底层还是依靠TCP协议;
- websocket会接着使用这条建立好的数据流和服务器之间保持通信;
- PING和PONG属于控制帧,之所以区别于一般数据帧,是因为控制帧数据量更加精简,用于检测心跳,判定连接连接情况,就不需要那么大的数据结构;
- 通过websocket,发送数据帧或者接受数据帧,实现客户端和服务器的全双工通信;
- 当然,由于复杂的网络环境,数据流可能会断开,在实际使用过程中,我们在onFailure或者onClosing回调方法中,实现重连;
- 现在,客户端和服务器可以实现长连接了,不管你是想要即时通信,还是想干点别的什么。当然,它只是实现了这个功能,面对具体业务,还需要定义自己的通信规则。
总结
到这里,这几个协议基本上就介绍完了。不仅仅是为了更好的使用它们,还需要注意它们的协议的设计思想,对我们架构一个新的工程,我觉得会很有帮助。比如,上面提到了面向接口的编程方式,单一职责原则,分层思想。
总结到这里,水平有限,如发现错误,欢迎指出。下一步,我想写点ws的实际应用。
参考链接:
https://jerryc8080.gitbooks.io/understand-tcp-and-udp/
https://blog.piasy.com/2016/07/11/Understand-OkHttp/
https://www.wolfcstech.com/2017/02/23/OkHttp%E5%AE%9E%E7%8E%B0%E5%88%86%E6%9E%90%E4%B9%8BWebsocket/