Netty、MINA、Twisted一起學系列02:TCP訊息邊界問題及按行分割訊息
文章已獲得作者授權
點選文末左下角“閱讀原文”即可跳轉到原文地址
在TCP連線開始到結束連線,之間可能會多次傳輸資料,也就是伺服器和客戶端之間可能會在連線過程中互相傳輸多條訊息。理想狀況是一方每傳送一條訊息,另一方就立即接收到一條,也就是一次write對應一次read。但是,現實不總是按照劇本來走。
MINA官方文件節選:
TCP guarantess delivery of all packets in the correct order. But there is no guarantee that one write operation on the sender-side will result in one read event on the receiving side. One call of IoSession.write(Object message) by the sender can result in multiple messageReceived(IoSession session, Object message) events on the receiver; and multiple calls of IoSession.write(Object message) can lead to a single messageReceived event.
Netty官方文件節選:
In a stream-based transport such as TCP/IP, received data is stored into a socket receive buffer. Unfortunately, the buffer of a stream-based transport is not a queue of packets but a queue of bytes. It means, even if you sent two messages as two independent packets, an operating system will not treat them as two messages but as just a bunch of bytes. Therefore, there is no guarantee that what you read is exactly what your remote peer wrote.
上面兩段話表達的意思相同:TCP是基於位元組流的協議,它只能保證一方傳送和另一方接收到的資料的位元組順序一致,但是,並不能保證一方每傳送一條訊息,另一方就能完整的接收到一條資訊。有可能傳送了兩條對方將其合併成一條,也有可能傳送了一條對方將其拆分成兩條。所以在上一篇文章(Netty、MINA、Twisted一起學系列01:實現簡單的TCP伺服器)中的Demo,可以說是一個錯誤的示範。不過伺服器和客戶端在同一臺機器上或者在區域網等網速很好的情況下,這種問題還是很難測試出來。
舉個簡單了例子(這個例子來源於Netty官方文件):訊息傳送方傳送了三個字串:
但是接收方收到的可能是這樣的:
那麼問題就很嚴重了,接收方沒法分開這三條資訊了,也就沒法解析了。對此,MINA的官方文件提供了以下幾種解決方案:
1、use fixed length messages
使用固定長度的訊息。比如每個長度4位元組,那麼接收的時候按每條4位元組拆分就可以了。
2、use a fixed length header that indicates the length of the body
使用固定長度的Header,Header中指定Body的長度(位元組數),將資訊的內容放在Body中。例如Header中指定的Body長度是100位元組,那麼Header之後的100位元組就是Body,也就是資訊的內容,100位元組的Body後面就是下一條資訊的Header了。
3、using a delimiter; for example many text-based protocols append a newline (or CR LF pair) after every message
使用分隔符。例如許多文字內容的協議會在每條訊息後面加上換行符(CR LF,即”\r\n”),也就是一行一條訊息。當然也可以用其他特殊符號作為分隔符,例如逗號、分號等等。
當然除了上面說到的3種方案,還有其他方案。有的協議也可能會同時用到上面多種方案。例如HTTP協議,Header部分用的是CR LF換行來區分每一條Header,而Header中用Content-Length來指定Body位元組數。
下面,分別用MINA、Netty、Twisted自帶的相關API實現按換行符CR LF來分割訊息。
MINA
MINA可以使用ProtocolCodecFilter來對傳送和接收的二進位制資料進行加工,如何加工取決於ProtocolCodecFactory或ProtocolEncoder、ProtocolDecoder,加工後在IoHandler中messageReceived事件函式獲取的message就不再是IoBuffer了,而是你想要的其他型別,可以是字串,Java物件。這裡可以使用TextLineCodecFactory(ProtocolCodecFactory的一個實現類)實現CR LF分割訊息。
public class TcpServer {
public static void main(String[] args) throws IOException {
IoAcceptor acceptor = new NioSocketAcceptor();
// 新增一個Filter,用於接收、傳送的內容按照"\r\n"分割
acceptor.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"), "\r\n", "\r\n")));
acceptor.setHandler(new TcpServerHandle());
acceptor.bind(new InetSocketAddress(8080));
}
}
class TcpServerHandle extends IoHandlerAdapter {
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
}
// 接收到新的資料
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
// 接收客戶端的資料,這裡接收到的不再是IoBuffer型別,而是字串
String line = (String) message;
System.out.println("messageReceived:" + line);
}
@Override
public void sessionCreated(IoSession session) throws Exception {
System.out.println("sessionCreated");
}
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("sessionClosed");
}
}
Netty
Netty設計上和MINA類似,需要在ChannelPipeline加上一些ChannelHandler用來對原始資料進行處理。這裡用LineBasedFrameDecoder將接收到的資料按行分割,StringDecoder再將資料由位元組碼轉成字串。同樣,接收到的資料進過加工後,在channelRead事件函式中,msg引數不再是ByteBuf而是String。
public class TcpServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// LineBasedFrameDecoder按行分割訊息
pipeline.addLast(new LineBasedFrameDecoder(80));
// 再按UTF-8編碼轉成字串
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new TcpServerHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
class TcpServerHandler extends ChannelInboundHandlerAdapter {
// 接收到新的資料
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// msg經過StringDecoder後型別不再是ByteBuf而是String
String line = (String) msg;
System.out.println("channelRead:" + line);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("channelActive");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("channelInactive");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Twisted
Twisted的設計和上面兩者的設計不太一樣,所以實現訊息分割也不太一樣。處理事件的類TcpServerHandle不再繼承Protocol,而是繼承Protocol的子類LineOnlyReceiver。接收到新資料的事件方法也不再是dataReceived,而是LineOnlyReceiver提供的lineReceived。看Twisted原始碼的話可以發現LineOnlyReceiver的內部實際上自己實現了dataReceived,然後將其按行分割,有新的一行資料就呼叫lineReceived。
# -*- coding:utf-8 –*-
from twisted.protocols.basic import LineOnlyReceiver
from twisted.internet.protocol import Factory
from twisted.internet import reactor
class TcpServerHandle(LineOnlyReceiver):
# 新的連線建立
def connectionMade(self):
print 'connectionMade'
# 連線斷開
def connectionLost(self, reason):
print 'connectionLost'
# 接收到新的一行資料
def lineReceived(self, data):
print 'lineReceived:' + data
factory = Factory()
factory.protocol = TcpServerHandle
reactor.listenTCP(8080, factory)
reactor.run()
下面用一個Java客戶端對三個伺服器進行測試。
public class TcpClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
OutputStream out = null;
try {
socket = new Socket("localhost", 8080);
out = socket.getOutputStream();
// 請求伺服器
String lines = "床前明月光\r\n疑是地上霜\r\n舉頭望明月\r\n低頭思故鄉\r\n";
byte[] outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush();
} finally {
// 關閉連線
out.close();
socket.close();
}
}
}
MINA伺服器輸出結果:
sessionCreated
messageReceived:床前明月光
messageReceived:疑是地上霜
messageReceived:舉頭望明月
messageReceived:低頭思故鄉
sessionClosed
Netty伺服器輸出結果:
channelActive
channelRead:床前明月光
channelRead:疑是地上霜
channelRead:舉頭望明月
channelRead:低頭思故鄉
channelInactive
Twisted伺服器輸出結果:
connectionMade
lineReceived:床前明月光
lineReceived:疑是地上霜
lineReceived:舉頭望明月
lineReceived:低頭思故鄉
connectionLost
當然,測試的時候也可以將傳送的資料模擬成不按規則分割的情況,下面用一個更變態的客戶端來測試。
public class TcpClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = null;
OutputStream out = null;
try{
socket = new Socket("localhost", 8080);
out = socket.getOutputStream();
String lines = "床前";
byte[] outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush();
Thread.sleep(1000);
lines = "明月";
outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush();
Thread.sleep(1000);
lines = "光\r\n疑是地上霜\r\n舉頭";
outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush();
Thread.sleep(1000);
lines = "望明月\r\n低頭思故鄉\r\n";
outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush();
} finally {
// 關閉連線
out.close();
socket.close();
}
}
}
再次分別測試上面三個伺服器,結果和上面的輸出結果一樣,沒有任何問題。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31558358/viewspace-2305559/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Netty、MINA、Twisted一起學系列03:TCP訊息固定大小的字首(Header)NettyTCPHeader
- Netty、MINA、Twisted一起學系列10:執行緒模型Netty執行緒模型
- Netty、MINA、Twisted一起學系列10:SSL / TLSNettyTLS
- Netty、MINA、Twisted一起學系列05:整合protobufNetty
- Netty、MINA、Twisted一起學系列01:實現簡單的TCP伺服器NettyTCP伺服器
- 基於tcp的應用層訊息邊界如何定義TCP
- Netty、MINA、Twisted一起學系列04:定製自己的協議Netty協議
- 訊息粘包 和 訊息不完整 問題
- netty訊息分發思路Netty
- 請教mina處理訊息?需要建立訊息佇列?佇列
- 訊息佇列系列一:訊息佇列應用佇列
- 如何處理RabbitMQ 訊息堆積和訊息丟失問題MQ
- netty系列之:netty對http2訊息的封裝NettyHTTP封裝
- RabbitMQ訊息佇列(六):使用主題進行訊息分發MQ佇列
- Storm概念學習系列之Stream訊息流 和 Stream Grouping 訊息流組ORM
- RocketMQ 訊息整合:多型別業務訊息-普通訊息MQ多型型別
- RocketMQ 訊息整合:多型別業務訊息——定時訊息MQ多型型別
- 訊息機制篇——初識訊息與訊息佇列佇列
- Kafka叢集訊息積壓問題及處理策略Kafka
- RabbitMQ訊息佇列入門及解決常見問題MQ佇列
- MQ系列:訊息中介軟體執行原理MQ
- RocketMq訊息丟失問題解決MQ
- 訊息佇列常見問題分析佇列
- 淺談訊息佇列及常見的訊息中介軟體佇列
- 解析 RocketMQ 業務訊息——“事務訊息”MQ
- 解析 RocketMQ 業務訊息--“順序訊息”MQ
- 自定義訊息獲取訊息(轉)
- 利用redis的hash結構搭建訊息服務(發訊息,訂閱訊息,消費訊息,退訂)Redis
- RocketMQ 原理:訊息儲存、高可用、訊息重試、訊息冪等性MQ
- 訊息中介軟體—RocketMQ訊息消費(三)(訊息消費重試)MQ
- 訊息中介軟體—RocketMQ訊息傳送MQ
- MQTT-保留訊息和遺囑訊息MQQT
- 自定義訊息和對訊息的理解
- (原創) odoo17中在訊息主題(mail.thread)中傳送訊息時,是否通知訊息作者進行控制OdooAIthread
- SpringCloud 2020.0.4 系列之 Stream 訊息廣播 與 訊息分組 的實現SpringGCCloud
- SQLSTATE 訊息SQL
- Windows訊息Windows
- vue---元件間傳遞訊息(父子傳遞訊息,兄弟傳遞訊息)Vue元件