1. 本文主要內容
初次接觸網絡通信部分內容時踩了不少坑,歷經磕絆總算摸索出了靠譜有效的實現異步通信的解決方法,在此做簡單記錄,之後若有需要可以節約這段時間。
首先進行一下簡要說明:
- 異步網絡通信指的是客戶端在發送請求等待接收數據時,不必一直阻塞直到收到服務端回覆才能進行下一步處理,在發送完數據之後就可以執行其他操作了,等到接收到相應回覆,再通過回調函數來處理消息內容。與之相對的就是同步消息收發策略。
- 不管是何種語言,誰是服務端,誰是客戶端,其收發消息時的流程步驟都是類似的,大體包括:根據ip和端口號創建socket,發送方對將數據打包寫入socket,接收方解析數據並處理。
- TCP之類的可靠傳輸協議,只能保證數據完整,有序的從傳輸方到接收方,並不管數據本身是什麼,該交給那個方法處理,想要解析收到的數據,只有靠自己定義消息格式並建立相應的打包及解析方法才能識別具體是什麼內容,對應哪條回覆等等之類。
在我之前想用它來進行通信時,曾天真地以爲我這兒把消息寫到網絡流裏去了,那兒就應該收到這條消息的內容,可以直接處理了。
但實際上,雖然數據可以有序、完整的被接收,但是並不會每次就正好接收到一條另一端發送的完整消息,而是存在分包、粘包的情況,因此,無論是服務端,還是客戶端,都要對該問題進行處理。
在本文中,以TCP爲通信協議,c#端爲客戶端,向python服務端發起連接請求,然後服務器進行回覆,客戶端對回覆內容再進行處理,那麼該簡單處理流程如下文所示。
2. 通信策略
- 爲了讓接收方知道這條消息的起止位置,在消息的頭部加入一個4字節整型,用來標識這段消息的長度,假如我們需要發送字符串
Hello world!
消息給服務器,那麼我們寫入socket
中的數據應如下表所示(實際傳輸的就最後一行的字節數組):
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
消息長度 | 內容 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
16 | H | e | l | l | o | w | o | r | l | d | ! | ||||
16 | 0 | 0 | 0 | 72 | 101 | 108 | 108 | 111 | 32 | 119 | 111 | 114 | 108 | 100 | 33 |
- 接收方就需要根據首部的長度,判斷當前消息的完整內容,如果當前接收內容小於消息長度,就繼續接收直到這個消息完整,若大於消息長度,那麼將這條消息的內容提取出來,將後面的消息進行合併再重新解析。
3. C#端收發數據
以下是c#端建立連接以及收發數據的示意代碼:
// NetworkDemo.cs
using System.Collections;
using System;
using System.IO;
using System.Net.Sockets;
public class NetworkDemo
{
public String host = "127.0.0.1";
public Int32 port = 2000;
internal Boolean socketReady = false;
byte[] receivedBuff;
int receivedBuffSize = 2048;
TcpClient tcpSocket;
NetworkStream netstream;
BinaryWriter writer;
const int msgHeadLen = sizeof(int);
int currDataLen = 0; // 報文長度
int currReceLen = 0; // 報文內容接受不完整 currReceLen < currDataLen
public NetworkDemo()
{
SetupSocket();
receivedBuff = new byte[receivedBuffSize];
Receive(receivedBuff, 0, receivedBuffSize, UnPackRawData);
}
public void SetupSocket()
{
try
{
tcpSocket = new TcpClient(host, port);
netstream = tcpSocket.GetStream();
writer = new BinaryWriter(netstream);
socketReady = true;
}
catch (Exception e)
{
Console.WriteLine("Socket error:" + e);
}
}
public void CloseSocket()
{
if (!socketReady)
return;
writer.Close();
netstream.Close();
tcpSocket.Close();
socketReady = false;
}
private bool IsSocketReady()
{
// 如果連接尚未創立,嘗試建立新連接
if (!socketReady)
{
SetupSocket();
}
return socketReady;
}
public void Send(byte[] data, int len)
{
if (!IsSocketReady())
return;
if (netstream.CanWrite)
{
writer.Write(len + sizeof(int));
writer.Write(data, 0, len); // 發送data[0 : len]
writer.Flush();
}
}
public void Receive(byte[] rec_buffer, int offset, int len, AsyncCallback callback)
{
// 異步讀取消息,callback函數是回調函數,收到數據後會調用它
if (IsSocketReady())
netstream.BeginRead(rec_buffer, offset, len, callback, null);
}
public void UnPackRawData(IAsyncResult result)
{
// 從netstream中讀到的數據個數
int receiveLen = netstream.EndRead(result);
receiveLen += currReceLen; // 和之前未處理完的數據拼接
currReceLen = 0; // 每次有效的數據都是從頭開始
// 前4個字節是消息長度
while (receiveLen > currReceLen)
{
// 循環處理直至沒有消息或只有一個不完整的消息
if (receiveLen - currReceLen < msgHeadLen)
{
// 報文頭部接收不完整,將它拷貝到頭部
// 重置已接收消息長度,並退出循環
Array.Copy(receivedBuff, currReceLen, receivedBuff, 0, receiveLen-currDataLen);
currReceLen = receiveLen - currReceLen;
break;
}
else
{
// 頭部完整,先解析報文長度
currDataLen = BitConverter.ToInt32(receivedBuff, currReceLen);
if (currDataLen > receivedBuffSize)
{
// 當前消息大於緩衝區長度,待處理
Console.WriteLine("消息長度大於緩衝區長度");
}
else if (currReceLen + currDataLen > receiveLen)
{
// 接收到的數據仍然不完整,繼續異步調用接收數據
Array.Copy(receivedBuff, currReceLen, receivedBuff, 0, receiveLen - currDataLen);
currReceLen = receiveLen - currReceLen;
break;
}
else if (currReceLen + currDataLen == receiveLen)
{
// 已接收到報文的長度和 報文的原始長度正好相等,將數據解析成事件
// 添加到事件隊列中,並重新異步接受新數據
ShowReceivedData(receivedBuff, currReceLen+msgHeadLen, currDataLen- msgHeadLen);
currReceLen = 0;
break;
}
else
{
// 接收到不止一條消息,逐個處理
ShowReceivedData(receivedBuff, currReceLen + msgHeadLen, currDataLen - msgHeadLen);
currReceLen += currDataLen;
}
}
}
// 不管有沒有分包,繼續接收(從之前已經緩衝位置開始)
Receive(receivedBuff, currReceLen, receivedBuffSize - currReceLen, UnPackRawData);
}
public void ShowReceivedData(byte[] data, int offset, int len)
{
Console.WriteLine(System.Text.Encoding.ASCII.GetString(data, offset, len));
}
}
using System;
using System.Text;
class Program
{
static void Main(string[] args)
{
double last_send_time = 0;
double send_delay_time = 3;
string message = "Hello world!";
byte[] send_buff = new byte[message.Length];
Encoding.UTF8.GetBytes(message).CopyTo(send_buff, 0);
byte[] buf2 = BitConverter.GetBytes(16);
NetworkDemo network = new NetworkDemo();
while(true)
{
if (DateTime.Now.ToOADate() > last_send_time + send_delay_time)
{
last_send_time = DateTime.Now.ToOADate();
network.Send(send_buff, send_buff.Length);
}
}
}
}
4. python端收發數據
待更。。。