3

TCP与应用层协议 - zko0

 1 year ago
source link: https://www.cnblogs.com/zko0/p/17179667.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1、小故事与前言

最近返校做毕业设计,嵌入式和服务器需要敲定一个通信协议。

我问:"服务端选什么协议,MQTT或者Websocket?"

他来句:"TCP"

我又确认了一遍:"用什么通信协议?"

他又来了句:"TCP"

给我气笑了。最后在我的坚持下,他才勉强以他啥都能做,以我为准的理由妥协了。

简单回想盘点下问题:

  1. TCP如何处理消息半包,粘包
  2. TCP如何确认应用状态
  3. TCP如何处理服务认证问题

他的回答:

  1. 消息发送,服务端加个标识字符检验,进行数据帧分割
  2. 发个心跳包

2、TCP与应用层协议

TCP位于传输层,而这位大哥想做的就是基于Socket去完成网络消息传输。(Socket本身并不是协议,而是一个调用接口,通过Socket,我们才能使用TCP/IP协议)

MQTT,Websocket属于应用层协议,基于TCP。

一.消息半包、粘包

首先,如果你使用过Java的原生Socket API,并进行过实验,你就能知道Socket是无法处理半包与粘包的。

什么是半包与粘包?可以参考我的Netty进阶文章

测试演示:

服务端代码:

缩小服务端的接收缓冲区为20字节,客户端10次发送"abc"数据。

请不要拿readUTF方法进行测试,sendUTF与readUTF是配套使用的,jdk原生进行了半包粘包的处理,对于发送消息添加了额外的消息用于处理问题。

如readLine等方法都是基于特定条件的处理了半包粘包问题(如本方法为换行划分一个消息帧),不适用于通用消息的网络传输。

import java.io.DataInputStream;import java.io.IOException;import java.io.InputStream;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.net.Socket; /** * @author zko0 * @date 2023/3/5 0:02 * @description */public class SimpleSocketServer { public static void main(String[] args) { DataInputStream dataInputStream=null; ServerSocket serverSocket=null; Socket accept=null; try { serverSocket= new ServerSocket(8081); System.out.println("socket服务创建"); //accept accept = serverSocket.accept(); System.out.println("client连接"); //只设置接收缓冲区大小 accept.setReceiveBufferSize(10); InputStream inputStream = accept.getInputStream(); dataInputStream= new DataInputStream(inputStream); while (true){ //创建byte数组用于接收数据 byte[] bytes=new byte[10]; dataInputStream.read(bytes); System.out.println(new String(bytes)); } } catch (IOException e) { e.printStackTrace(); }finally { try { if (dataInputStream!=null)dataInputStream.close(); if (serverSocket!=null)serverSocket.close(); if (accept!=null)accept.close(); } catch (IOException e) { e.printStackTrace(); } } }}

客户端代码:

public class SimpleSocketClient { public static void main(String[] args) { Socket socket =null; DataOutputStream dataOutputStream=null; try{ socket=new Socket("127.0.0.1", 8081); System.out.println("socket连接"); OutputStream outputStream= socket.getOutputStream(); dataOutputStream= new DataOutputStream(outputStream); for (int i = 0; i < 10; i++) { dataOutputStream.writeBytes("abc"); } dataOutputStream.flush(); while (true); }catch (IOException e) { e.printStackTrace(); } }}

测试结果:

如下图所示,Server接收Client消息存在着半包粘包问题,一次消息abc无法被完整接收,在服务端第一次接收到了a,第二次接收到了bc+abc+ab一次完整+两次一半的消息。

很明显,如果服务端直接通过获取的消息进行处理,是存在问题的,对于TCP消息,需要进行半包和粘包的处理,才能使用消息。

image-20230305012503986
image-20230305012503986

③问题解决

正如那位“高含金量选手”所说,要对半包粘包进行处理。但是这种处理不仅仅是直接使用标识符就可以的,对于多出的额外信息或者不足的消息,是需要保留来拼凑出完整消息的。这部分你可以参考一下Java中readLine()方法

解决方式:

  1. 使用标识符进行消息帧分割

    这种方式只适用于特定场景,因为在发送的消息中如果存在你的标识符,那么在进行帧解码的时候,就会将这部分内容作为标识符将字符串分割。由于分割位置错误,一次消息你可能会得到分割后的多个错误消息。

    就比如readLine()使用于读取一行数据。

  2. 使用短连接

    这种方式可以解决粘包的问题,但是无法解决半包问题。因为一次发送后,连接就会中断。那么Server获取的数据只会小于单次发送的数据。

  3. 定长帧解码器

    在发送数据中,定义协议。如设置前5个字节代表发送数据的长度。

    如果发送abc三字节内容,发送数据为:0 0 0 0 3 97 98 99

    服务器读取前5个字节,就知道需要再读取3字节内容,为一次完整数据。如果单次只读取了2字节,那么会等待1个字节内容,拼凑出最后的3字节,单次完整消息。

    你可以参考readUTF()方法,思路相同。

如果你了解TCP,你就会知道TCP协议是自带心跳的。

KEEP_ALIVE
在TCP协议里,本身的心跳包机制SO_KEEPALIVE,系统默认设置的是7200秒的启动时间,默认是关闭的

有三个参数可以设置:p_keepalive_time、tcp_keepalive_probes、tcp_keepalive_intvl

分别表示连接闲置多久才开始发心跳包、连发几次心跳包没有回应表示连接已断开、心跳包之间间隔时间。

但是众所周知,几乎所有基于TCP协议的应用层协议,都自定义了心跳报文:如WebSocket,MQTT

Socket中Client关闭,Server能自动感受到连接断开啊?:

当SocketClient应用关闭,系统调用Socket的close、或者进程结束。操作系统会发送FIN数据包给服务器,所以Sevrer能够关闭TCP连接。

如果你在Client运行中,拔掉网线或者关闭电源。这时Client是没有机会发送FIN数据包的,这时Server就无法感知到Client已经断开了连接。

为什么TCP自带心跳还需要应用层定义心跳:

  1. TCP心跳机制是传输层实现的,只要当前连接是可用的,对端就会ACK我们的心跳,而对于当前对端应用是否能正常提供服务,TCP层的心跳机制是无法获知的
  2. tcp_keepalive_time参数的设置是秒级,对于极端情况,我们可能想在毫秒级就检测到连接的状态,这个TCP心跳机制就无法办到
  3. 通用性,应用层心跳功能不依赖于传输层协议,如果有一天我们想将传输层协议由TCP改为UDP,那么传输层不提供心跳机制了,应用层的心跳是通用的,此时或许只用修改少量地方代码即可;
  4. 如果连接中存在Sock Proxy或者NAT,他们可能不会处理TCP的Keep Alive包

三.应用报文

这部分比较偏向应用层内容,仅为个人感想。

在应用层协议中,协议报文都是对应用每个功能定制,且缩小了数据量的。

以MQTT3.1举例

MQTT第一次报文为CONNETCT报文。在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接 。

报文内容:

  1. 固定头:1--->确定了报文的类型:CONNECT

    • 第6位:用户名标志 User Name Flag
    • 第7位:密码标志 Password Flag
  2. 有效载荷:CONNECT报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。

仅仅是一个客户端连接报文,就设计的非常全面且优雅,且完全契合应用目的。

个人思考后,个人认为将应用层协议分为通用协议特定系统协议俩种。

通用协议:如HTTP协议,Websocket,MQTT协议,搭载用户自定义数据体进行通讯

特定系统协议:如Mysql协议,Redis的RESP协议,都是基于特定应用自己开发的一套网络协议

对于应用开发,个人认为通用协议更为合理,不仅进行了上述众多问题的处理。而且还有对应协议的应用意义:如身份验证等。

实在难以想象那种代码水平是否能将Socket自定义协议与消息处理写出基本的骨架出来。希望技术人员专注于技术,共同进步,不要盲目自大。

如果你有较好思考,欢迎指点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK