Fixed-Length Frames 谈谈网络编程中应用层(基于TCP/UDP)的协议设计
http://blog.sina.com.cn/s/blog_48d4cf2d0101859x.html
谈谈网络编程中应用层(基于TCP/UDP)的协议设计
(2013-04-27 19:11:00)
对于初涉网络编程的开发人员来说,在通信协议的设计上一般会有所困惑。一般的网络编程书籍上也较少涉及这方面的内容。估计是觉得太简单了。这块确实是不难,但如果不了解,又很容易出篓子或者绕弯路。下面我就来谈谈基于TCP/UDP的协议设计。
1、基于TCP的协议设计
TCP是基于流的协议。但大部分网络应用一般会有个更小的处理单元,我们称之为帧(FRAME)。
- 是否分帧
如上所述,大部分网络应用是需要分帧的。举IM为例,用户登录是一个帧,用户发送文本信息是一个帧。少部分应用可以不需要分帧,比如:echo服务器,接收到什么直接回复即可;转发服务器,同样是接收到数据直接转给目标机器;更常见的情况是一个TCP连接只发送/处理一个请求之后就直接关闭,这种也就没必要分帧了。
考虑到除了学习网络编程,没人做echo server。所以只要服务端不是一次连接只处理一个请求,或者纯转发,就应该采用分帧的设计。
- 如何分帧
注意:帧是业务处理的单元,是具体应用Care的,但这不关TCP的事情!初学者往往认为tcp这端 write一次,tcp那端就会read一次,然后惊呼“粘包”、“丢包”,其实这都是程序处理不当。在这边推荐一本书籍《TCP/IP协议详解 卷1》,挺薄的,看完可以减少很多对TCP的错误认识。实际上发送方发送一帧,接收方可能要N次才能读取完成,而且可能同时读到下帧的数据。那要怎么在接收方把一帧数据不多不少的读取出来呢?
常用做法有两个:基于长度和基于终结符(Delimiter)。基于长度,就是在帧前先发送帧的长度,一般用固定长度的字节来发送此长度,比如2个字节(最大帧长不能大于65535),4个字节。(ps:我也见过使用可变长度的字节来发送此长度,比如netty中的ProtobufVarint32FrameDecoder,看代码那是相当的蛋疼,我觉得完全是折腾自己,强烈不推荐。)使用基于长度的分帧方式,接受方处理流程一般是这样:“读取固定长度的字节 -> 解析出帧长 -> 读取帧长字节 -> 处理帧”。
基于终结符(Delimiter),最典型的应用就是HTTP协议了,使用/r/n/r/n作为终结符。使用基于终结符的分帧方式,接收方的处理流程一般是这样:“读数据 -> 在读取的数据中定位终结符 -> 没找到,将数据缓存 -> 继续读数据 -> 定位终结符 -> 找到终结符,将终结符之前的数据作为一帧进行处理”。
使用终结符的方式务必要考虑转义问题,不然在帧的数据中出现终结符,乐子就大了。
注意不管采用哪种方式,在开发的时候都需要考虑最大帧长的问题。不然如果对方说要发送4G长度的帧(恶意or程序错误),真的去new 4G字节的缓存;或者对方一直发送数据,没有终结符。都可能造成程序内存耗尽。
一般来说,基于长度的分帧方式。开发更简单,程序执行效率也更高,使用更广泛些。基于终结符也不是一无是处:可读性更好,容易模拟和测试(如用telnet)。下面重点讨论基于长度的分帧方式。
- 基于长度的的帧设计(length based frame design)
一般来说,我们会将帧分为帧头(frame header,一般是固定长度)和帧体(frame body,一般是可变长度,也有固定长度的)。如上所述,最简单的帧头只要一个字段——帧长。但在实际应用中,一个典型的帧头可能还有以下字段:
a)消息类型(message type):在一个网络应用中,往往有多种类型的帧。比如对于IM,有登陆/登出/发送消息/……。接收方需要根据帧头的消息类型字段,解码出不同种类的消息,交给相应处理模块进行处理。也就是帧的结构是Length-Type-Message,Length-Type可以视为帧头,Message是帧体。消息类型一般也是使用固定长度,比如Length 4个字节,Type 4个字节,那么帧头的长度就是8个字节。接收方处理流程:“读帧头长度字节数据 - 解码帧头获得长度和消息类型 - 读帧体长度字节数据 - 根据消息类型解码消息 - 处理消息”。Length-Type-Message结构的帧设计是使用最广泛的,普适性最好也最精简的设计。
b)请求序列号(serials):这个不是必选项,但我觉得对于非echo式的服务(echo式的服务:总是客户端发送请求-服务端针对该请求应答,应答保证严格按照请求顺序),加上这个字段肯定不后悔。这样对于乱序(如果有消息队列后台线程池,很正常)的执行结果,才能够和请求对上号,从而做出正确的处理。一般来说,高性能的服务端要保证响应的严格有序,是比较麻烦和影响性能的。
c)版本号(version):很多人这么用,但我觉得大部分情况下这不是个好主意。帧头应该放大部分/全部帧都需要的字段。而版本号可能只有少数包如登录会用到,所以放到登录包体里可能更合适。单独维护每个协议的版本工作量会比较大,开发起来会比较繁琐易错。至于担心解码失败,更好的方式是采用类似Protobuf这种可以向下兼容的编解码方案。
注意:在帧头设计时应该要尽可能的精简和通用,因为帧头长度是每个帧都需要的额外开销。如果某个字段(如序列号)只有少数帧会使用到,完全可以放在帧体里去。反之,如果某个字段大部分包都有,却不定义在包头,会导致难以统一处理,增加开发工作量。这些需要根据具体业务需求来进行权衡,没有统一的答案。举个例子,Length-Type-Message结构适用于大部分情况,但如果业务要求每个帧都需要表明操作者,在帧头增加UID字段变成Length-Type-UID-Message,程序的开发会更简单。
- 帧体的设计
帧体就是字段的集合,举个例子,登录帧体包含用户名、密码这两个字段(只是举例,现实的登录包往往复杂得多)。在帧体设计上,大家往往也是八仙过海各显神通。比如基于XML、json,基于字段Pos(举登录包为例,就先写/读用户名,再写/读密码。这种方式不是太好,很难向下兼容:比如登录包需要在用户名和密码间加一个用户状态,如果服务端/客户端没有同步升级,就会斯巴达)。我甚至见过狂野得离谱的直接使用C struct的,这种脑残到爆:兼容性渣不说,类对齐(可以用pragma pack避免不一致)、byte order、机器字长都会造成麻烦。
比较推荐的做法:骚年,用Google Protobuf吧!如果要可读性好,json相比XML更省带宽。
2、基于UDP的协议设计
一般来说,UDP的服务器要比TCP简单得多(不过如果要实现基于UDP的可靠消息传输,就当我没说)。而且udp本来就是基于数据包的协议。write/read是可以一一对应的(不考虑丢包),所以不需要有长度字段/终结符。
但是要注意:为了避免丢包率过高,udp包的长度一般不应该大于1500字节(大概,为了安全起见,我一般保证小于1K嘿),如果数据量较大,就需要分包了,这是比TCP麻烦的地方。
典型的UDP的协议设计就是:Type-Message。Type长度固定,用于说明消息类型;Message是消息体,和tcp的帧体设计同样即可。
https://cseweb.ucsd.edu/classes/fa13/cse123-a/lectures/123-fa13-l4.pdf
https://zhuanlan.zhihu.com/p/21999964
https://www.zhihu.com/question/50422109
https://en.wikipedia.org/wiki/Type-length-value
所有的应用层协议,都是通过TCP或UDP发送某种符合约定的二进制数据,然后在通信的两端对二进制数据进行解析来进行通信的。
http本来就是“超文本传输”协议,所以才会用文本格式的协议。
除了很少一些例外,大规模使用的协议很少会是基于文本的。
你经常接触到的,包括各种聊天工具、各种网络游戏、文件下载/分享的FTP/BT等等等等,都是二进制的
设计成文本是因为他本来就是给文本设计的。大部分应用层协议都不是文本的,比如ftp,rtp之类的。
所有协议都有信令和业务两部分。一般情况下设计,业务部分随应用内容走。例如传图片不可能用文本。 信令部分一般是二进制的,优点是效率高。缺点是扩展性差。 以前的二进制协议是设计死的字段和比特。
现在新的设计方向是tlv格式(type-length-value),扩展性强。效率依然很高。
文本的协议设计方向是用xml格式,扩展性也好。文本协议便于和数据库的联动,尤其是xml格式更好。二进制协议想对接各种数据库需要转换。http恰好需要数据库访问和对接。 从安全性上来说,tlv和xml都有恶意格式和长度溢出等攻击,如果用固定格式的二进制会好一些。
我们会遇到需要使用二进制协议的情况:
- 性能要求非常苛刻,以至于JSON等序列化会严重拖慢性能
- 数据本身带有大量的二进制内容,不适合使用文本格式
- 其他目的(协议保密,反侦测等)
- 对端的软件由其他人开发,已经出于某种目的使用了二进制协议
https://www.jianshu.com/p/222f7f584c1a
org.apache.thrift.transport.TTransportException: Frame size (40792739) larger than max length (16384000)!
异常,所以在代码中会修改一次性传输的大小(1638400000),这个需在客户端和服务端同时设定.服务端
private final static int DEFAULT_PORT = 30002;
private static TServer server = null;
public static void main(String[] args) {
TNonblockingServerSocket socket = null;
try {
socket = new TNonblockingServerSocket(DEFAULT_PORT);
} catch (TTransportException e) {
e.printStackTrace();
}
//多接口的实现
TProcessor tProcessor1 = new TestQry.Processor<TestQry.Iface>(new QueryImp());
TProcessor tProcessor2 = new TestQry1.Processor<TestQry1.Iface>(new QueryImp1());
TThreadedSelectorServer.Args arg = new TThreadedSelectorServer.Args(socket);
TMultiplexedProcessor multiplexedProcessor = new TMultiplexedProcessor();
multiplexedProcessor.registerProcessor("processor1",tProcessor1);
multiplexedProcessor.registerProcessor("processor2",tProcessor2);
arg.processor(multiplexedProcessor);
arg.protocolFactory(new TCompactProtocol.Factory());
//如果传输数据量过大,需要修改这个地方的参数,默认16M
arg.transportFactory(new TFramedTransport.Factory(1638400000));
arg.processorFactory(new TProcessorFactory(multiplexedProcessor));
//监听线程数
arg.selectorThreads(10);
//工作线程数
ExecutorService pool = Executors.newFixedThreadPool(100);
arg.executorService(pool);
arg.getExecutorService();
server = new TThreadedSelectorServer(arg);
System.out.println("Starting server on port " + DEFAULT_PORT + "......");
server.serve();
}
客户端
private final static int DEFAULT_QRY_CODE = 1;
public void startClient() {
TTransport tTransport = null;
try {
tTransport = getTTransport();
} catch (Exception e) {
e.printStackTrace();
}
TProtocol protocol = new TCompactProtocol(tTransport);
//对应的客户端也要用多接口的方式实现
TMultiplexedProtocol q1 = new TMultiplexedProtocol(protocol,"processor1");
TMultiplexedProtocol q2 = new TMultiplexedProtocol(protocol,"processor2");
TestQry.Client client1 = new TestQry.Client(q1);
TestQry1.Client client2 = new TestQry1.Client(q2);
try {
QryResult result = client1.qryTest(DEFAULT_QRY_CODE);
System.out.println("code="+result.code+" msg="+result.msg);
close(tTransport);
} catch (TException e) {
e.printStackTrace();
} } private static TTransport getTTransport() throws Exception{
TTransport tTransport = getTTransport("127.0.0.1",30002,300000);
if(tTransport != null && !tTransport.isOpen()){
tTransport.open();
}
return tTransport;
} private static TTransport getTTransport(String host, int port, int timeout) {
final TSocket tSocket = new TSocket(host,port,timeout);
final TTransport tTransport = new TFramedTransport(tSocket,1638400000);
return tTransport;
}
private void close(TTransport transport){
if(transport !=null && transport.isOpen()){
transport.close();
}
}
Fixed-Length Frames 谈谈网络编程中应用层(基于TCP/UDP)的协议设计的更多相关文章
- [转帖]关于网络编程中MTU、TCP、UDP优化配置的一些总结
关于网络编程中MTU.TCP.UDP优化配置的一些总结 https://www.cnblogs.com/maowang1991/archive/2013/04/15/3022955.html 感谢原作 ...
- c++ 网络编程(一)TCP/UDP windows/linux 下入门级socket通信 客户端与服务端交互代码
原文作者:aircraft 原文地址:https://www.cnblogs.com/DOMLX/p/9601511.html c++ 网络编程(一)TCP/UDP 入门级客户端与服务端交互代码 网 ...
- JAVA之旅(三十二)——JAVA网络请求,IP地址,TCP/UDP通讯协议概述,Socket,UDP传输,多线程UDP聊天应用
JAVA之旅(三十二)--JAVA网络请求,IP地址,TCP/UDP通讯协议概述,Socket,UDP传输,多线程UDP聊天应用 GUI写到一半电脑系统挂了,也就算了,最多GUI还有一个提示框和实例, ...
- Linux 系统编程 学习:008-基于socket的网络编程3:基于 TCP 的通信
背景 上一讲我们介绍了 基于UDP 的通信 这一讲我们来看 TCP 通信. 知识 TCP(Transmission Control Protoco 传输控制协议). TCP是一种面向广域网的通信协议, ...
- 网络编程[第一篇]基于tcp协议的套接字编程
将服务端-客户端的连接比作双方打电话的过程 2019-07-24 一.客户端 主动的一方: 客户端实例化一个socket对象--> 主动像服务端发送连接请求--> (服务端接受请求后即可进 ...
- 网络编程应用:基于TCP协议【实现对象传输】--练习
要求: 基于TCP协议实现,客服端向服务器发送一个对象 服务器接受并显示用户信息 ,同时返回给客户端 "数据已收到" 建一个Student类,属性:name age Student ...
- 网络编程应用:基于TCP协议【实现文件上传】--练习
要求: 基于TCP协议实现一个向服务器端上传文件的功能 客户端代码: package Homework2; import java.io.File; import java.io.FileInputS ...
- 网络编程应用:基于TCP协议【实现一个聊天程序】
要求: 基于TCP协议实现一个聊天程序,客户端发送一条数据,服务器端发送一条数据 客户端代码: package Homework1; import java.io.IOException; impor ...
- C/C++网络编程5——实现基于TCP的服务器端/客户端2
三次握手过程详解: 1:客户端的协议栈向服务器端发送SYN包,并告诉服务器端当前放送序号为j,客户端进入SYNC_SEND状态. 2:服务器端的协议栈收到这个包以后,和客户端进行ACK应答,应答值为j ...
随机推荐
- Java--8--新特性--接口中的变化!!
package InterfaceP; public interface Interface1 { default String getName(){ return "Interface1& ...
- vue-cli3.0 脚手架搭建项目的过程详解
1.安装vue-cli 3.0 ? 1 2 3 npm install -g @vue/cli # or yarn global add @vue/cli 安装成功后查看版本:vue -V(大写的V) ...
- (Linux基础学习)第七章:echo命令
第1节:简单说明功能:显示字符语法:echo [-neE][字符串]说明:echo会将输入的字符串送往标准输出.输出的字符串之间以空白字符隔开,并在最后加上换行号选项:-E(默认)不支持\解释功能-n ...
- SoC的设计变的如此复杂和高成本
当一些硬件IP变成了标准的螺丝钉和螺母的时候,硬件设计的未来就没有了吗? 由于太过于复杂,而整体的毛利率又不高,无法迅速迭代,产生边际效应,也无法迅速扩张. 就成了一个传统行业,从业者也逐渐被时代遗忘 ...
- Kotlin对象表达式深入解析
嵌套类与内部类巩固: 在上一次https://www.cnblogs.com/webor2006/p/11333101.html学到了Kotlin的嵌套类与内部类,回顾一下: 而对于嵌套类: 归根结底 ...
- python中while循环的基本使用
一.while循环 while 条件: 如果条件为True,会一直循环 代码块(循环体) else: 当上面的条件为假.才会执行 执行顺序:判断条件是否为真.如果真,执行循环 ...
- 微信小程序API~地理位置location
(1)使用微信内置地图查看位置 wx.openLocation(Object object) 使用微信内置地图查看位置 参数 Object object 属性 类型 默认值 必填 说明 latitud ...
- 51nod 2488 矩形并的面积
在二维平面上,给定两个矩形,满足矩形的每条边分别和坐标轴平行,求这个两个矩形的并的面积.即它们重叠在一起的总的面积. 收起 输入 8个数,分别表示第一个矩形左下角坐标为(A,B),右上角坐标为(C ...
- 关于使用scipy.stats.lognorm来模拟对数正态分布的误区
lognorm方法的参数容易把人搞蒙.例如lognorm.rvs(s, loc=0, scale=1, size=1)中的参数s,loc,scale, 要记住:loc和scale并不是我们通常理解的对 ...
- 题解 洛谷P1457 【城堡 The Castle】
这道题,看似很烦,无从下手,但其实只要用位运算和联通快就能水过了呀. 首先,输入:似乎大意是把一个数拆成二进数的相加,分别表示\((i,j)\)东南西北是否有墙.\(1\)表示西,\(2\)表示北,\ ...