基於TCP/IP協議的聊天例項丨首篇介紹及資料包指令碼介紹
首篇介紹
本節包括:
1、資料包指令碼
2、以及對資料包指令碼的測試,檢驗資料包是否能正常工作
聊天例項實現流程:功能及實現流程連結
注:本例項與上鍊接內例項相互獨立,上鍊接為給讀者認識、以及學習TCP/IP提供參考
根據socket通訊基本流程圖,總結通訊的基本步驟:
伺服器端:
第一步:建立一個用於監聽連線的Socket對像;
第二步:用指定的埠號和伺服器的ip建立一個EndPoint對像;
第三步:用socket對像的Bind()方法繫結EndPoint;
第四步:用socket對像的Listen()方法開始監聽;
第五步:接收到客戶端的連線,用socket對像的Accept()方法建立一個新的用於和客戶端進行通訊的socket對像;
第六步:通訊結束後一定記得關閉socket;
客戶端:
第一步:建立一個Socket對像;
第二步:用指定的埠號和伺服器的ip建立一個EndPoint對像;
第三步:用socket對像的Connect()方法以上面建立的EndPoint對像做為引數,向伺服器發出連線請求;
第四步:如果連線成功,就用socket對像的Send()方法向伺服器傳送資訊;
第五步:用socket對像的Receive()方法接受伺服器發來的資訊 ;
第六步:通訊結束後一定記得關閉socket;
資料包
在傳輸網路資料時,接收方一次收到的長度可能是不確定的,比如客戶端傳送100個位元組給伺服器,伺服器一次收到100個位元組,也可能先收到20個,再收到80個。為知道到底一個數據的長度是多長的,我們建立資料包類,用於管理序列化的資料流,序列化、反序列化物件
啟動VS,新建C#類庫(也可是普通Console工程,只是不能生成dll),使用.NET Framework2.0,也可使用更高版本,取決於Unity支援的最高版本。
以下是NetPacket.cs指令碼:
using System; using System.IO; using System.Net.Sockets; using System.Runtime.Serialization.Formatters.Binary; using System.Text; namespace UnityNetwork { public class NetPacket { //32位整型佔4個位元組 public const int INT32_LEN = 4; //位元組陣列bytes中“頭”的長度,佔4個位元組,即32位整型,它的值就是最後的bodyLengtht的長度 public const int headerLength = 4; //最多傳輸512位元組 public const int max_body_length = 512; public int bodyLength { get; set; } //總長 public int Length { get { return headerLength + bodyLength; } } //儲存資料的byte陣列 public byte[] bytes { get; set; } //傳送這個資料包的socket public Socket socket; //網路傳輸過程中已存輸出的位元組長度。因為資料可能一次收到,也可能分幾次收到 public int readLength { get; set; } //建構函式 public NetPacket() { readLength = 0; bodyLength = 0; bytes = new byte[headerLength + max_body_length]; } //建立一個建構函式副本 public NetPacket(NetPacket p) { bodyLength = p.bodyLength; bytes = new byte[p.bytes.Length]; //copyTo方法和copy方法一樣,其作用是將一個數組的內容複製給另一個數組,並從指定的索引處開始複製,其格式語法為: //< 被複制的陣列 >.CopyTo(< 目標陣列 >,< 複製的起始索引 >) p.bytes.CopyTo(bytes, 0); readLength = p.readLength; socket = p.socket; } public void Reset() { readLength = 0; bodyLength = 0; } //在位元組陣列中寫入一個訊息識別符號,標識當前資料包的作用,這個函式要最先呼叫 //當將所有資料寫入之後,注意最後要使用EncodeHeader函式將資料的長度寫到byte最前面的4個位元組中 public void BeginWrite(string msg) { //初始化體長為0 bodyLength = 0; WriteString(msg); } //寫字串 public void WriteString(string str) { //GetByteCount:可以獲得將字串或者字串陣列轉換成位元組陣列的位元組陣列的長度 int len = Encoding.UTF8.GetByteCount(str); WriteInt(len); if (bodyLength + len > max_body_length) return; //將s轉換為UTF8編碼的位元組陣列 //public override int GetBytes (string s, int charIndex, int charCount, byte[] bytes, int byteIndex); //s:要編碼的字符集的 charIndex:第一個要編碼的字元的索引 charCount:要編碼的字元的數目 //bytes:要包含所產生的位元組序列的位元組陣列 byteIndex:要開始寫入所產生的位元組序列的索引位置 Encoding.UTF8.GetBytes(str, 0, str.Length, bytes, headerLength + bodyLength); bodyLength += len; } //寫整型 public void WriteInt(int number) { if (bodyLength + INT32_LEN > max_body_length) return; //BitConverter:把一些基本的資料型別記憶體中的表示形式轉換成以位元組陣列的形式,後面跟的要轉換的格式 byte[] bs = BitConverter.GetBytes(number); bs.CopyTo(bytes, headerLength + bodyLength); bodyLength += INT32_LEN; } //寫入byte陣列 public void WriteStream(byte[] bs) { WriteInt(bs.Length); if (bodyLength + bs.Length > max_body_length) return; //壓入資料流 bs.CopyTo(bytes, headerLength + bodyLength); bodyLength += bs.Length; } //直接寫入一個序列化的物件 public void WriteObject<T>(T t) { byte[] bs = Serialize<T>(t); WriteStream(bs); } //開始讀取 public void BeginRead(out string msg) { bodyLength = 0; ReadString(out msg); } //讀取字串 public void ReadString(out string str) { str = ""; int len = 0; ReadInt(out len); if (bodyLength + len > max_body_length) return; str = Encoding.UTF8.GetString(bytes, headerLength + bodyLength, (int)len); bodyLength += len; } //讀取int public void ReadInt(out int number) { number = 0; if (bodyLength + INT32_LEN > max_body_length) return; number = System.BitConverter.ToInt32(bytes, headerLength + bodyLength); bodyLength += INT32_LEN; } //讀取byte陣列 public byte[] ReadStream() { int size = 0; ReadInt(out size); if (bodyLength + size > max_body_length) return null; byte[] bs = new byte[size]; for(int i = 0; i < size; i++) { bs[i] = bytes[headerLength + bodyLength + i]; } bodyLength += size; return bs; } //直接將讀取的byte陣列反序列化 public T ReadObj<T>() { byte[] bs = ReadStream(); if (bs == null) return default(T); return Deserialize<T>(bs); } //將資料長度轉化為4個位元組存在byte陣列的最前端 public void EncodeHeader() { byte[] bs = BitConverter.GetBytes(bodyLength); bs.CopyTo(bytes, 0); } //從byte陣列最前面解析出資料的長度,並儲存在bodyLength //當在網路上接收資料時,我們先接收資料包最前面的4個位元組 //然後使用DecodeHeader即可計算出後面的資料長度,再繼續接收後面的資料 public void DecodeHeader() { bodyLength = System.BitConverter.ToInt32(bytes, 0); } //序列化物件,這裡使用的是C#自帶的序列化類,也可以替換成JSON等序列化方式 //泛型:https://blog.csdn.net/weixin_38239050/article/details/80173874 public byte[] Serialize<T>(T t) { using (MemoryStream stream=new MemoryStream()) { try { //建立序列化類 BinaryFormatter bf = new BinaryFormatter(); //序列化到stream bf.Serialize(stream, t); stream.Seek(0, SeekOrigin.Begin); return stream.ToArray(); } catch(Exception ex) { Console.WriteLine(ex.Message); return null; } } } //反序列化物件,這裡使用C#自帶序列化類,也可用JSON等方式 public T Deserialize<T>(byte[] bs) { using (MemoryStream stream=new MemoryStream()) { try { BinaryFormatter bf = new BinaryFormatter(); T t = (T)bf.Deserialize(stream); return t; } catch (Exception ex) { Console.WriteLine("Deserialize:" + ex.Message); return default(T); } } } } }
對資料包的測試
上面資料包指令碼用類庫寫的,類庫是無法直接啟動除錯的,本例項類庫用來生成dll檔案,供其他專案指令碼引用使用。
因此,
1、我們在VS解決方案右鍵新增“專案”—“控制檯應用”,並將其設為啟動專案
2、在資料包類庫專案中點選“生成”—“解決方案”,此時在類庫資料夾的bin—Debug資料夾下,就生成了該指令碼的dll檔案
3、控制檯應用指令碼,右鍵“新增”—“引用”—“瀏覽”—dll檔案
4、此時,在該控制檯指令碼,即可using 類庫dll檔案的名稱空間了,若無第2、3步,則無法using引用
以下是測試指令碼:
using System;
using UnityNetwork;
namespace MainProgram
{
class Program
{
static void Main(string[] args)
{
String id = "test";
int number = 100;
string msg = "hello,world";
//寫資料
NetPacket p=new NetPacket();
p.BeginWrite(id);
p.WriteInt(number);
p.WriteString(msg);
p.EncodeHeader();
//讀資料
NetPacket p2 = new NetPacket();
p.BeginRead(out id);
p.ReadInt(out number);
p.ReadString(out msg);
Console.WriteLine("id:{0},number:{1},msg:{2}", id, number, msg);
}
}
}