物联网行业智能硬件之间的通信、异构系统之间的对接、中间件的研发、以及各种即时聊天软件等,都会涉及自定义协议。

为了满足不同的业务场景的需要, 应用层之间通信需要实现各种各样的网络协议。以异构系统的对接为例。在早期,我们使用 Web Service 来解决异构系统的对接,后来我们逐渐使用 MQ、RPC 等方式来实现异构系统的通信和整合。

Web Service 是使用 SOAP 协议通过 HTTP 进行传输。MQ 有很多常用的消息队列协议,例如 AMQP、MQTT、STOMP 等,而新兴的消息队列,如 Kafka 和 ZeroMQ,它们并没有严格遵循 MQ 规范,而是基于TCP/IP 协议自行封装了一套协议,并通过 TCP 进行传输。另外,像 Dubbo 这样的 RPC 框架,本身支持多种协议。其自身的 Dubbo 协议也是阿里巴巴自己实现的应用层协议,并通过 TCP 进行传输。

因此,设计好一款合理的、可扩展的自定义协议,可以打通不同的异构系统,亦或者可以作为一款 RPC 框架的基石。今天我将手把手带你设计一个高效、可扩展、易维护的自定义通信协议,以及如何使用 Netty 实现该协议的 TCP 服务端。

为什么需要自定义通信协议?

我们在开发一款工业自动化的智能硬件时,通常需要一台上位机(一般是一款桌面端应用程序)来控制不同的硬件设备。上位机可以独立存在,也可以由 Web 后台发送指令到上位机,再通过上位机来控制智能硬件,以此来完成业务上的操作。

从上位机到 Web 后台之间的通信,可能是由一个 TCP 长连接(也可能是 WebSocket 长连接)来进行维护,而上位机到各个硬件设备之间也可能通过长连接来维护,当然也可以是串口、MQTT、CoAP 等协议,这主要取决于所连接的设备。从 Web 后台到上位机再到智能硬件,假如都使用了 TCP 长连接,那么后两者甚者可以使用 TCP 透传。

无论是 TCP 的长连接,还是 WebSocket 的长连接,本质都是基于 TCP 的连接,为此我们需要使用 Socket 编程,通俗地说可以认为它是对 TCP 协议的具体实现。此外,我们所熟知的中间件、网络游戏、智能硬件、金融等领域也都会涉及 Socket 相关的编程。在使用 Socket 编程时,我们经常会听到别人提起“自定义协议”。事实上,目前已经有了很多标准的协议,那我们为何还需要“自定义”呢?

我们先从下面这张 OSI 七层模型的图开始,快速回顾一下网络通信的面貌。

TCP/IP 协议将 OSI 七层模型进行了简化,变成四层模型。

在 TCP/IP 协议中从应用层到网络接口层,每一层传输的数据包都会包含两部分内容:一部分是协议所要用到的首部,另一部分是从上一层传过来的数据。下图展示了 TCP/IP 包的全貌。

我们所熟知的各种网络应用程序都是在应用层上使用的,TCP/IP 协议的应用层为我们提供了多种常见的应用层协议,例如 HTTP、SSH、Telnet、FTP 等。正是有了这些协议,各种网络应用程序才可以为我们服务。

另外,应用层也支持给我们的程序“量身”制定协议,也就是支持“自定义协议”。当常用的应用层协议不满足我们的应用开发时,例如扩展性不够、安全性不足、不能针对特定领域、无法追求极致的性能等,就需要“自定义协议”。

如何设计自定义通信协议?

TCP 是一种流模式的协议,在实现自定义协议时,我们会遇到诸如以下的问题:

1.应用程序如何知道业务数据是全部接收完毕的,如何解决拆包和粘包问题?

2.如何实现请求/响应机制?

3.如何解决超时问题和实际应用的通信需求?

4.如何定义消息指令或报文类型?

……

自定义通信协议

为了解决上述的问题,首先我们介绍一种比较通用的 TCP 通信协议,其协议结构如下:

+--------------+---------------+------------+---------------+-----------+-

| 魔数(4)| version(1)|序列化方式(1)|command(1)|SerialNo(2)|数据长度(4)|数据(n)   |

+--------------+---------------+------------+---------------+-----------+-

下面我们对这个协议中的内容展开介绍。

魔数:4 个字节,为了防止该 TCP 端口被意外调用。我们在收到报文后取前 4 个字节与魔数比对,如果不相同则直接拒绝并关闭连接。魔数可以随意定义,比如采用 20200803 作为魔数,它的 16 进制是 0x1343d63。

版本号:1 个字节,仅表示协议的版本号,便于协议升级时使用。

序列化方式:1 个字节,表示如何将 Java 对象转化为二进制数据,以及如何反序列化。

指令:1 个字节,也可以叫报文类型,表示该消息的意图,如登录、心跳、升级,以及不同的业务指令等。最多可支持 256 种指令(-127 到 127)。

SerialNo:2 个字节,表示整个任务的 id 或者任务的流水号,便于进行追踪。最多支持 2^16 位(-32,768 到 32,767)。

数据长度:4 个字节,表示该字段后数据部分的长度。类似于 HTTP 协议的报文头中的 Content-Length  这个字段。最多支持 2^32 位。

数据:具体的数据内容。

根据上述设计的通信协议,定义一个报文类 Message,它代表通信协议的报文,如下所示:

public abstract class Message<T extends MessageBody> {private MessageHeader messageHeader;private T messageBody;public T getMessageBody() {return messageBody;

}

}

Message 参考 TCP 协议,将其抽象成由 Header 和 Payload 组成(即首部和数据块)。其中,报文的 Header 部分共 9 个字节,包含魔数、版本号、序列化方式、指令、SerialNo,结构如下:

+--------------+---------------+------------+---------------+-----------+

| 魔数(4)       | version(1)    |序列化方式(1)      | command(1)           |SerialNo(2)|

+--------------+---------------+------------+---------------+-----------+

因此可以定义一个如下的 Header 类:


public class MessageHeader {

private int magicNumber; // 魔数

private int version = 1; // 版本号,当前协议的版本号为 1

private int serializeMethod; // 序列化方式,默认使用 json

private int command;      // 消息的指令


private long serialNo;    // 任务的流水号

}

每个 Payload 都是报文的具体内容,即协议体。它可以是一个字符串也可以是一个复杂的对象,因此我们定义一个空接口用于表示 Payload,所有的 Payload 都需要实现该接口:

public abstract class MessageBody {
}

考虑到需要预留和扩展性,以避免在将来报文经常性地被修改,可以给 Payload 增加一个预留的属性 extra ,它是一个 Map 类型。因此,再定义一个基类的 BasePayload,我们也可以在 Header 中额外定义一个字段作为一个预留字段。

按照上述的设计,该协议的报文头/首部只有 9 个字节,相比于 HTTP 协议的报文头还是少了很多,极大地精简了传输内容。这也是为什么后端的 RPC 框架通常会采用自定义 TCP 协议进行通信。

Packet 的一次完整旅行

介绍完自定义通信协议后,我们来看看 Packet 在一个 TCP 服务中是怎样经历一次完整的旅行的。

(1)定义指令集

在业务系统中,我们通常需要定义很多个指令,一个指令对应一个 Packet。Header 的 command 字段用来区分不同的指令。

在 Packet 的 Header 中,command 定义了 1 个字节,表示它支持  256 种指令。所以,我们可以定义一个最多包含 256 个指令的指令集 Commands,其定义方式如下:

 /** 指令集*/
public interface Command {/*** 心跳包*/final Byte HEART_BEAT = 0;/*** 登录请求*/final Byte LOGIN_REQUEST = 1;/*** 登录响应*/final Byte LOGIN_RESPONSE = 2;/*** 消息请求*/final Byte MESSAGE_REQUEST = 3;/*** 消息响应*/

final Byte MESSAGE_RESPONSE = 4;

}

当然,如果觉得 256 个指令不够,修改协议 Header 中 command 的字节数即可。

下面以心跳的 Packet 为例,首先定义一个 HeartBeatPacket:

public class HeartBeatPacket extends Packet {private String msg = "ping-pong";public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}@Overridepublic Byte getCommand() {return HEART_BEAT;}
}

心跳包一般是由 TCP 客户端发起,经由 TCP 服务端接收后,进行响应并返回给客户端。它像心跳一样每隔固定时间发送一次,以此来告诉服务端,这个客户端还活着。

(2) 定义序列化方式

心跳包的内容很小,可以使用 JSON 进行解析。但是对于图片、视频、日志文件等比较大的内容,可能需要使用 Java 自带的序列化方式,或由 Kryo、Hessian、FST、Protobuf 等框架实现对象的序列化和反序列化。

因此,我们定义一个序列化方式的常量列表,代码如下:

* 定义序列化算法*/
public interface SerializeAlgorithm {/*** json序列化标识*/

byte json = 1;

byte binary = 2;


byte fst = 3;

}

上面的代码表示目前只支持这些序列化方式,后续可以不断添加新的序列化方式。

再定义一个序列化接口,每种序列化方式需要一个相应的实现,代码如下:

** Serializer,用来指定序列化算法,用于序列化对象*/
public interface Serializer {/*** @return 序列化算法*/byte getSerializerAlgorithm();/*** 将对象序列化成二进制**/byte[] serialize(Object object);/*** 将二进制反序列化为对象*/<T> T deSerialize(Class<T> clazz, byte[] bytes);
}

由于,存在多个序列化方式,可以考虑设计一个序列化的工厂类SerializerMap,通过工厂类来获取指令所需要的序列化实现。

private static final Map<Byte, Serializer> serializerMap;

serializerMap = new HashMap<Byte, Serializer>();
Serializer serializer = new JsonSerializer();
serializerMap.put(serializer.getSerializerAlgorithm(), serializer);

(3)定义 Packet 的工厂类

最初,我们将 Packet 抽象成 Header 和 Payload 两部分,因此 Packet 的生成也包含了两部分的生成。

前面,我们定义了一些客户端、服务端的指令,也知道不同的指令对应不同的 Packet。因此,可以通过指令来生成对应的 Payload。Header 中本身就包含了 command,唯一需要注意的就是序列化方式 serializeMethod,不同的 command 对应唯一的 serializeMethod。

下面是 Packet 的工厂类,用于生成 Payload 和 Header:

** 编解码对象*/
public class PacketCodeC {/*** 魔数*/public static final int MAGIC_NUMBER = 0x88888888;public static PacketCodeC instance = new PacketCodeC();/*** 采用单例模式*/public static PacketCodeC getInstance(){return instance;}private static final Map<Byte, Class<? extends Packet>> packetTypeMap;
static {packetTypeMap = new HashMap<Byte,Class<? extends Packet>>();packetTypeMap.put(HEART_BEAT, HeartBeatPacket.class);packetTypeMap.put(LOGIN_REQUEST, LoginRequestPacket.class);packetTypeMap.put(LOGIN_RESPONSE, LoginResponsePacket.class);packetTypeMap.put(MESSAGE_REQUEST, MessageRequestPacket.class);packetTypeMap.put(MESSAGE_RESPONSE, MessageResponsePacket.class);}private PacketCodeC(){

}

}

(4)实现报文的 encode、decode

到了这里,我们的工作还差了报文的 encode、decode。我们可以定义一个报文的管理类 PacketManager,用于对报文进行 encode、decode。

/*** 编码** 魔数(4字节) + 版本号(1字节) + 序列化算法(1字节) + 指令(1字节) + 数据长度(4字节) + 数据(N字节)*/
public ByteBuf encode(ByteBufAllocator alloc, Packet packet){//创建ByteBuf对象ByteBuf buf = alloc.ioBuffer();return encode(buf,packet);

}

public ByteBuf encode(ByteBuf buf,Packet packet){
//序列化java对象
byte[] objBytes = serializer.serialize(packet);
//实际编码过程,即组装通信包
//魔数(4字节) + 版本号(1字节) + 序列化算法(1字节) + 指令(1字节) + 数据长度(4字节) + 数据(N字节)
buf.writeInt(MAGIC_NUMBER);
buf.writeByte(packet.getVersion());
buf.writeByte(serializer.getSerializerAlgorithm());

buf.writeByte(packet.getCommand());


buf.writeShort(serialNo);

buf.writeInt(objBytes.length);
buf.writeBytes(objBytes);
return buf;
}
public ByteBuf encode(Packet packet){
return encode(ByteBufAllocator.DEFAULT, packet);
}
/**
* 解码
*

* 魔数(4字节) + 版本号(1字节) + 序列化算法(1字节) + 指令(1字节)+序列 2

+ 数据长度(4字节) + 数据(N字节)

*/
public Packet decode(ByteBuf buf){
//魔数校验(handler单独处理)
buf.skipBytes(4);
//版本号校验(暂不做)
buf.skipBytes(1);
//序列化算法
byte serializeAlgorithm = buf.readByte();
//指令

byte command = buf.readByte();

//序列号

buf.readShort(serialNo);

//数据长度
int length = buf.readInt();
//数据
byte[] dataBytes = new byte[length];
buf.readBytes(dataBytes);
Class<? extends Packet> packetType = getRequestType(command);
Serializer serializer = getSerializer(serializeAlgorithm);
if(packetType != null && serializer != null){
return serializer.deSerialize(packetType,dataBytes);
}
return null;
}

encode() 方法是将 Packet 对象组装成 Netty 的 ByteBuf 对象,组装的方式完全是按照自定义的 TCP 协议来,顺序千万不能错,否则 decode() 无法解析。

需要注意的是,不同的报文可能会采用不同的序列化方式。需要从 Packet 的 Header 中读取 serializeMethod ,然后从工厂类 SerializerMap 中获取对应的序列化实现 serializer。

这样,客户端和服务端就可以进行交互了,TCP 的报文也可以在我们的 TCP 服务中完成一次完整的旅行。

TCP 服务端的设计

服务端采用 Netty 框架,我们使用的是 Netty 的主从多线程 Reactor 模型。Reactor 模型是 Netty 实现高性能的基础,Netty 的 Reactor 模型分为三种:

1.单线程模型、2.多线程模型、3.主从多线程模型。

主从多线程模型由多个 Reactor 线程组成,MainReactor 负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。SubReactor 分配线程池中的 I/O 线程与其连接绑定,负责连接生命周期内所有的 I/O 事件。主从多线程模型可以利用 CPU 的多核来提升系统的吞吐量,因此这也是 Netty 推荐使用的模型。

我们需要在服务端定义 boss 和 worker 这两个 Reactor。其中,boss 是主 Reactor,worker 是从 Reactor。它们分别使用不同的 NioEventLoopGroup,主 Reactor 负责处理 Accept 然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。

@ChannelHandler.Sharable
public class Server {private static Logger logger = LoggerFactory.getLogger(Server.class);private static int port = 8888;public static void main(String[] strings){port = StringUtil.isNullOrEmpty(System.getProperty("port")) ? port : Integer.parseInt(System.getProperty("port"));NioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(boss,worker).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {nioSocketChannel.pipeline().addLast(new ServerIdleHandler());nioSocketChannel.pipeline().addLast(new MagicNumValidator());nioSocketChannel.pipeline().addLast(PacketCodecHandler.getInstance());nioSocketChannel.pipeline().addLast(LoginRequestHandler.getInstance());nioSocketChannel.pipeline().addLast(HeartBeatHandler.getInstance());nioSocketChannel.pipeline().addLast(AuthHandler.getInstance());nioSocketChannel.pipeline().addLast(ServerHandler.getInstance());}});ChannelFuture future = bootstrap.bind(port);future.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture channelFuture) throws Exception {if (channelFuture.isSuccess()){logger.info("server started! using port {} " , port);}else {logger.info("server start failed! using port {} " , port);channelFuture.cause().printStackTrace();System.exit(0);}}});}
}

ChannelHandler 的使用

从上述代码中,可以看到 worker 处理了各种各样的 Handler。其中,ServerIdleHandler 继承 Netty 自带的 IdleStateHandler 类,用于检测连接的有效性。如果 150秒内没有收到心跳,则断开连接。

* 心跳检测,150s没收到心跳包的话,断开连接*/
public class ServerIdleHandler extends IdleStateHandler {private static Logger logger = LoggerFactory.getLogger(ServerIdleHandler.class);private static int HERT_BEAT_TIME = 150;public ServerIdleHandler() {super(0, 0, HERT_BEAT_TIME);}@Overrideprotected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {logger.info("{}内没有收到心跳,关闭连接...",HERT_BEAT_TIME);ctx.channel().close();}
}

MagicNumValidator:用于 TCP 报文的魔数校验。

public class MagicNumValidator extends LengthFieldBasedFrameDecoder {private static final int LENGTH_FIELD_OFFSET = 7;private static final int LENGTH_FIELD_LENGTH = 4;public MagicNumValidator() {super(Integer.MAX_VALUE, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH);}@Overrideprotected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {//魔数校验不通过if(in.getInt(in.readerIndex()) != MAGIC_NUMBER){ctx.channel().close();return null;}return super.decode(ctx, in);}
}

PacketCodecHandler:解析报文的 Handler。PacketCodecHandler 继承自 ByteToMessageCodec ,它是用来处理 byte-to-message 和 message-to-byte,便于解码字节消息成 POJO 或编码 POJO 消息成字节。

这一步非常关键。因为 TCP 作为传输层的协议,无法理解上层业务数据的具体含义,它根据 TCP 缓冲区的实际情况进行数据包的划分。在业务上认为是一个完整的包,很可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包进行发送,这就是所谓的 TCP 粘包和拆包问题。在这一步,我们通过自定义的编解码器解决了粘包和拆包问题。

在这里,我们看到 PacketCodecHandler 使用上面提到的报文管理类 PacketManager 的 encode()、decode() 方法来完成编解码的过程。

@ChannelHandler.Sharable
public class PacketCodecHandler extends MessageToMessageCodec<ByteBuf,Packet> {private PacketCodecHandler(){}private static PacketCodecHandler instance = new PacketCodecHandler();public static PacketCodecHandler getInstance(){return instance;}protected void encode(ChannelHandlerContext ctx, Packet packet, List<Object> list) throws Exception {ByteBuf byteBuf = ctx.channel().alloc().ioBuffer();PacketCodeC.getInstance().encode(byteBuf,packet);list.add(byteBuf);}protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) throws Exception {list.add(PacketCodeC.getInstance().decode(buf));}
}

HeartBeatHandler:心跳的 Handler,接收 TCP 客户端发来的"ping",然后给客户端返回"pong"。

public class HeartBeatPacket extends Packet {private String msg = "ping-pong";public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}@Overridepublic Byte getCommand() {return HEART_BEAT;}
}

ResponseHandler:通用的处理接收 TCP 客户端发来业务指令的 Handler,可以根据对应的指令去查询对应的 Handler,并对这些命令进行响应。

最后,我们在 ResponseHandler 中,看到还有一个 ThreadPool,它是一个业务线程池。但是在我们所定义的 TCPServer 中, worker 本身使用了一个线程池,为何还需要一个业务线程池呢?

业务线程池的使用

Netty 的 Reactor 线程模型适合处理耗时短的任务场景,对于耗时较长的 ChannelHandler 来说,维护一个业务线程池是一个比较好的做法。将编解码后的数据封装成任务放入线程池中,避免 ChannelHandler 阻塞而造成 EventLoop 不可用。

如果有复杂且耗时的业务逻辑,我推荐的做法是在 ChannelHandler 处理器中自定义新的业务线程池,从而将这些耗时的操作提交到业务线程池中执行。

例如定义一个业务线程池,代码如下:


@Slf4j

public final class ThreadPoolFactoryUtils {

/**
* 通过 threadNamePrefix 来区分不同线程池(我们可以把相同 threadNamePrefix 的线程池看作是为同一业务场景服务)。
* key: threadNamePrefix
* value: threadPool
*/
private static final Map<String, ExecutorService> THREAD_POOLS = new ConcurrentHashMap<>();
private ThreadPoolFactoryUtils() {
}
public static ExecutorService createCustomThreadPoolIfAbsent(String threadNamePrefix) {
CustomThreadPoolConfig customThreadPoolConfig = new CustomThreadPoolConfig();
return createCustomThreadPoolIfAbsent(customThreadPoolConfig, threadNamePrefix, false);
}
public static ExecutorService createCustomThreadPoolIfAbsent(String threadNamePrefix, CustomThreadPoolConfig customThreadPoolConfig) {
return createCustomThreadPoolIfAbsent(customThreadPoolConfig, threadNamePrefix, false);
}
public static ExecutorService createCustomThreadPoolIfAbsent(CustomThreadPoolConfig customThreadPoolConfig, String threadNamePrefix, Boolean daemon) {
// 存在则取出,不存新建一个
ExecutorService threadPool = THREAD_POOLS.computeIfAbsent(threadNamePrefix, k -> createThreadPool(customThreadPoolConfig, threadNamePrefix, daemon));
// 如果 threadPool 被 shutdown 的话就重新创建一个
if (threadPool.isShutdown() || threadPool.isTerminated()) {
THREAD_POOLS.remove(threadNamePrefix);
threadPool = createThreadPool(customThreadPoolConfig, threadNamePrefix, daemon);
THREAD_POOLS.put(threadNamePrefix, threadPool);
}
return threadPool;
}
/**
* shutDown 所有线程池
*/
public static void shutDownAllThreadPool() {
log.info("call shutDownAllThreadPool method");
THREAD_POOLS.entrySet().parallelStream().forEach(entry -> {
ExecutorService executorService = entry.getValue();
executorService.shutdown();
log.info("shut down thread pool [{}] [{}]", entry.getKey(), executorService.isTerminated());
try {
executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("Thread pool never terminated");
executorService.shutdownNow();
}
});
}
private static ExecutorService createThreadPool(CustomThreadPoolConfig customThreadPoolConfig, String threadNamePrefix, Boolean daemon) {
ThreadFactory threadFactory = createThreadFactory(threadNamePrefix, daemon);
return new ThreadPoolExecutor(customThreadPoolConfig.getCorePoolSize(), customThreadPoolConfig.getMaximumPoolSize(),
customThreadPoolConfig.getKeepAliveTime(), customThreadPoolConfig.getUnit(), customThreadPoolConfig.getWorkQueue(),
threadFactory);
}
/**
* 创建 ThreadFactory 。如果threadNamePrefix不为空则使用自建ThreadFactory,否则使用defaultThreadFactory
*
* @param threadNamePrefix 作为创建的线程名字的前缀
* @param daemon 指定是否为 Daemon Thread(守护线程)
* @return ThreadFactory
*/
public static ThreadFactory createThreadFactory(String threadNamePrefix, Boolean daemon) {
if (threadNamePrefix != null) {
if (daemon != null) {
return new ThreadFactoryBuilder()
.setNameFormat(threadNamePrefix + "-%d")
.setDaemon(daemon).build();
} else {
return new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").build();
}
}
return Executors.defaultThreadFactory();
}

}

业务线程池的使用很简单,在 businessAction() 方法中, block 参数是一个 Lambda 表达式,用于执行耗时的业务逻辑,通过 Header 中的 command 来查找服务端所对应使用的 ChannelHandler ,并作出响应。

针对特别复杂的业务,还可以根据业务的特点拆分出多个业务线程池。这样做的好处是:即使某个业务逻辑出现异常造成线程池资源耗尽,也不会影响到其他业务逻辑,从而提高应用程序整体的可用性。也做到了线程池的隔离。

总结,只有充分理解各个硬件、各个软件系统可实现的功能,才能设计出合理的自定义协议。反之,理解了一些常用的中间件相关协议,也可以帮助我们深入理解这些中间件,甚至还可以实现各个中间件的代理功能。

实际上对于其他的高级语言实现自定义协议也是类似的。当你真正理解了自定义 TCP 协议,以后再遇到新的协议,例如自定义的串口协议,会更容易理解。

如何自定义TCP通信协议相关推荐

  1. Socket通用TCP通信协议设计及实现(防止粘包,可移植,可靠)

    Socket通用TCP通信协议设计及实现(防止粘包,可移植,可靠) 引文 我们接收Socket字节流数据一般都会定义一个数据包协议.我们每次开发一个软件的通信模块时,尽管具体的数据内容是不尽相同的,但 ...

  2. QT -- TcpSocket实例,使用Qt中的tcp通信协议,构建客户端和服务端,实现局域网通信软件功能

    Qt中使用Tcp构建通信客户端实现聊天信息发送连接等 1.简介 2.项目创建和界面构建 1)流程图 2)项目构建 3)界面构建 3.代码设计 1)项目pro添加 2)客户端设计 a. clientwi ...

  3. Qt之超简单的TCP通信(自定义TCP通信类,含源码+注释)

    文章目录 一.TCP通信示例图 二.TCP使用前的准备 三.自定义TCP通信类的两种方法 四.源码(含注释) TCP Server CTcpServer.h CTcpServer.cpp TCP Cl ...

  4. 多人在线斗地主游戏开发——自定义TCP网络通信协议包格式

    什么叫做通信协议?为什么制定通信协议? 怎么制定通信协议? 不知道大家有没有迷茫过这个问题,反正我是有的,,, 想我在刚接触网络编程的时候,是linux下用socket懵懵懂懂地按照pdf书籍上的代码 ...

  5. 自定义串口通信协议,如何实现?

    关注+星标公众号,不错过精彩内容 作者 | strongerHuang 微信公众号 | 嵌入式专栏 有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学. 同时,偶尔有读者 ...

  6. 如何实现自定义串口通信协议?

    有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学. 同时,偶尔有读者问关于串口自定义通信协议相关的问题,今天就来写写串口通信协议,并不是你想想中的那么难? 1什么通信协 ...

  7. 如何自定义一个通信协议

    借鉴简单的OSI和TCP/IP通信模型来讨论如何自定义一个适应自己的通信协议 文章目录 @[toc] 1.前言 2.经典的OSI七层模型 2.1.TCP/IP模型解析 2.1.1.整体介绍 2.2.2 ...

  8. 通信教程 | 自定义串口通信协议

    关注+星标公众号,不错过精彩内容 作者 | strongerHuang 微信公众号 | strongerHuang 有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学. ...

  9. 西门子 PLC TCP 通信协议

    flexmanager PLC 通信协议 http://www.flexem.cn/Index.html PLC与PC网络通信实验 https://blog.csdn.net/loblab/artic ...

  10. Modbus TCP通信协议详解

    一.Modbus TCP通信概述 MODBUS/TCP是简单的.中立厂商的用于管理和控制自动化设备的MODBUS系列通讯协议的派生产品,显而易见,它覆盖了使用TCP/IP协议的"Intran ...

最新文章

  1. 中文语境下的手机号识别
  2. Python 或将成为法国高中的官方编程教学语言
  3. [译] 第五天: GruntJS - 为你解决繁琐重复的任务
  4. cdh 安装_使用Cloudera的CDH部署Hadoop:第三步,安装管理平台和数据库
  5. 微信开发同步微信服务器图片到本地,逻辑处理。
  6. robotframework 测试工具添加PDF文件内容匹配插件
  7. python后台架构Django教程——admin管理员站点
  8. 第三次课堂总结--付胤
  9. SitePoint播客#114:在WordCamp Raleigh上直播第1部分
  10. 【网络编程】【SCTP】SCTP协议介绍,四次握手,三次挥手
  11. C语言中%d,%o,%f,%e,%x的意义
  12. 如何解决上传到github上的图片显示不出来的问题
  13. Redisson(4)分布式锁之RedLock
  14. 对Scrollbar实现平时隐藏,滑动时出现
  15. 重装fedora17之后的一些配置
  16. Swiperjs插件轮播滑动卡顿优化
  17. DVWA——暴力破解
  18. oracle修改表某列字段长度
  19. 分享一些省心的PPT模板下载网站资源(附5G优质PPT模板)
  20. qq第三方登录的调用

热门文章

  1. 单机html游戏修改数据,星露谷物语存档修改图文教程 怎么修改游戏数据
  2. 酷柚易汛进销存开源版升级来啦
  3. web前端经典面试题及答案
  4. git bash批量dos2unix
  5. SCT2450QSTE 国产车规AEC-Q100 3.8V-36V 5A 高效同步降压 DCDC 转换器 替代TSP54540
  6. 线性系列DC-DC转换器工作原理
  7. 2019年11月中华人民共和国县以上行政区划代码(用于身份证前六位判断户籍所在地)
  8. 【一键新机】免root/不刷机/拒绝Xposed 实现 Android改机,全新技术分析。
  9. 基于Python的串口调试工具
  10. ArcGIS导出地图后部分图例不显示