经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 其他 » TCP/IP和HTTP » 查看文章
TCP与应用层协议
来源:cnblogs  作者:zko0  时间:2023/3/6 9:15:22  对本文有异议

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

  1. import java.io.DataInputStream;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.net.InetSocketAddress;
  5. import java.net.ServerSocket;
  6. import java.net.Socket;
  7. /**
  8. * @author zko0
  9. * @date 2023/3/5 0:02
  10. * @description
  11. */
  12. public class SimpleSocketServer {
  13. public static void main(String[] args) {
  14. DataInputStream dataInputStream=null;
  15. ServerSocket serverSocket=null;
  16. Socket accept=null;
  17. try {
  18. serverSocket= new ServerSocket(8081);
  19. System.out.println("socket服务创建");
  20. //accept
  21. accept = serverSocket.accept();
  22. System.out.println("client连接");
  23. //只设置接收缓冲区大小
  24. accept.setReceiveBufferSize(10);
  25. InputStream inputStream = accept.getInputStream();
  26. dataInputStream= new DataInputStream(inputStream);
  27. while (true){
  28. //创建byte数组用于接收数据
  29. byte[] bytes=new byte[10];
  30. dataInputStream.read(bytes);
  31. System.out.println(new String(bytes));
  32. }
  33. } catch (IOException e) {
  34. e.printStackTrace();
  35. }finally {
  36. try {
  37. if (dataInputStream!=null)dataInputStream.close();
  38. if (serverSocket!=null)serverSocket.close();
  39. if (accept!=null)accept.close();
  40. } catch (IOException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }
  45. }

客户端代码:

  1. public class SimpleSocketClient {
  2. public static void main(String[] args) {
  3. Socket socket =null;
  4. DataOutputStream dataOutputStream=null;
  5. try{
  6. socket=new Socket("127.0.0.1", 8081);
  7. System.out.println("socket连接");
  8. OutputStream outputStream= socket.getOutputStream();
  9. dataOutputStream= new DataOutputStream(outputStream);
  10. for (int i = 0; i < 10; i++) {
  11. dataOutputStream.writeBytes("abc");
  12. }
  13. dataOutputStream.flush();
  14. while (true);
  15. }catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

测试结果:

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

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

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

  2. 可变头:

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

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

3、小结

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

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

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

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

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

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

原文链接:https://www.cnblogs.com/zko0/p/17179667.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号